man 2 write

WRITE(2)                                                             Linux Programmer's Manual                                                            WRITE(2)

NAME
       write - write to a file descriptor	//写入文件描述符

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);

DESCRIPTION
       write() writes up to count bytes from the buffer pointed buf to the file referred to by the file descriptor fd.

       The  number  of  bytes  written  may be less than count if, for example, there is insufficient space on the underlying physical medium, or the RLIMIT_FSIZE
       resource limit is encountered (see setrlimit(2)), or the call was interrupted by a signal handler after having written less than count  bytes.   (See  also
       pipe(7).)

       For  a seekable file (i.e., one to which lseek(2) may be applied, for example, a regular file) writing takes place at the current file offset, and the file
       offset is incremented by the number of bytes actually written.  If the file was open(2)ed with O_APPEND, the file offset is first set to  the  end  of  the
       file before writing.  The adjustment of the file offset and the write operation are performed as an atomic step.

       POSIX requires that a read(2) which can be proved to occur after a write() has returned returns the new data.  Note that not all filesystems are POSIX con‐
       forming.
       //write() 将缓冲区指向的 buf 中的 count 个字节写入文件描述符 fd 所引用的文件。

       //写入的字节数可能少于 count,例如,底层物理介质上的空间不足,或者遇到 RLIMIT_FSIZE 资源限制(参见 setrlimit(2)),或者调用被信号处理程序中断后写入少于 count 个字节。
       // (另请参见管道 (7)。)

       //对于可查找文件(即可以应用 lseek(2) 的文件,例如常规文件),写入发生在当前文件偏移量处,并且文件偏移量增加实际写入的字节数。
       //如果使用 O_APPEND 打开(2)文件,则文件偏移量在写入之前首先设置为文件末尾。
       //文件偏移量的调整和写入操作作为原子步骤执行。

       //POSIX 要求可以证明在 write() 返回后发生的 read(2) 返回新数据。请注意,并非所有文件系统都符合 POSIX。

RETURN VALUE
       On success, the number of bytes written is returned (zero indicates nothing was written).  It is not an error if this number is smaller than the number  of
       bytes requested; this may happen for example because the disk device was filled.  See also NOTES.

       On error, -1 is returned, and errno is set appropriately.

       If  count  is  zero  and  fd  refers  to a regular file, then write() may return a failure status if one of the errors below is detected.  If no errors are
       detected, or error detection is not performed, 0 will be returned without causing any other effect.  If count is zero and fd refers to a file other than  a
       regular file, the results are not specified.
       //成功时,返回写入的字节数(零表示未写入任何内容)。 
       //如果此数字小于请求的字节数,则不是错误; 例如,这可能是因为磁盘设备已满。 另见注释。

        //出错时,返回 -1,并适当设置 errno。

        //如果 count 为零并且 fd 引用常规文件,则如果检测到以下错误之一,则 write() 可能会返回失败状态。 
        //如果没有检测到错误,或者没有进行错误检测,则返回 0,不会产生任何其他影响。 如果 count 为零并且 fd 引用的文件不是常规文件,则不指定结果。

ERRORS
       EAGAIN The  file  descriptor  fd refers to a file other than a socket and has been marked nonblocking (O_NONBLOCK), and the write would block.  See open(2)
              for further details on the O_NONBLOCK flag.
              //文件描述符 fd 指除了socket之外的文件,并且已被标记为非阻塞(O_NONBLOCK),写入将阻塞。 
              //有关 O_NONBLOCK 标志的更多详细信息,请参见 open(2)。

       EAGAIN or EWOULDBLOCK
              The file descriptor fd refers to a socket and has been marked nonblocking (O_NONBLOCK), and the write would block.  POSIX.1-2001 allows either error
              to be returned for this case, and does not require these constants to have the same value, so a portable application should check for both possibil‐
              ities.
              //文件描述符 fd 是一个socket并且已被标记为非阻塞 (O_NONBLOCK),写入将阻塞。
              //这里的阻塞是指:内核写缓冲可能不够空间(参考:https://www.zhihu.com/question/533504297)
              //POSIX.1-2001 允许在这种情况下返回任一错误,并且不要求这些常量具有相同的值,因此可移植应用程序应检查这两种可能性。

       EBADF  fd is not a valid file descriptor or is not open for writing.
       			//fd 不是有效的文件描述符或未打开写入。

       EDESTADDRREQ
              fd refers to a datagram socket for which a peer address has not been set using connect(2).
              //fd 指的是尚未使用 connect(2) 为其设置对等地址的数据报套接字。

       EDQUOT The user's quota of disk blocks on the filesystem containing the file referred to by fd has been exhausted.
       			//包含 fd 引用的文件的文件系统上用户的磁盘块配额已用完。(配额是啥?怎么设置的?)

       EFAULT buf is outside your accessible address space.
       			//buf 在您可访问的地址空间之外。

       EFBIG  An attempt was made to write a file that exceeds the implementation-defined maximum file size or the process's file size limit, or  to  write  at  a
              position past the maximum allowed offset.
              //尝试写入超过实现定义的最大文件大小或进程的文件大小限制的文件,或写入超过最大允许偏移量的位置。

       EINTR  The call was interrupted by a signal before any data was written; see signal(7).
       //在写入任何数据之前调用被信号中断; 见信号(7)。

       EINVAL fd is attached to an object which is unsuitable for writing; or the file was opened with the O_DIRECT flag, and either the address specified in buf,
              the value specified in count, or the current file offset is not suitably aligned.
              //fd 附着在不适合书写的对象上; 
              //或者文件是使用 O_DIRECT 标志打开的,并且 buf 中指定的地址、count 中指定的值或当前文件偏移量未适当对齐。

       EIO    A low-level I/O error occurred while modifying the inode.
       		//修改 inode 时发生低级 I/O 错误。

       ENOSPC The device containing the file referred to by fd has no room for the data.
       //包含 fd 引用的文件的设备没有数据空间。

       EPERM  The operation was prevented by a file seal; see fcntl(2).
       //文件封印阻止了该操作; 参见 fcntl(2)。

       EPIPE  fd is connected to a pipe or socket whose reading end is closed.  When this happens the writing process will also receive a SIGPIPE signal.   (Thus,
              the write return value is seen only if the program catches, blocks or ignores this signal.)
              //fd 连接到读取端关闭的管道或套接字。 
              //当这种情况发生时,写入过程也会收到一个 SIGPIPE 信号。 (因此,只有当程序捕获、阻塞或忽略此信号时,才会看到写入返回值。)

       Other errors may occur, depending on the object connected to fd.

CONFORMING TO
       SVr4, 4.3BSD, POSIX.1-2001.

       Under SVr4 a write may be interrupted and return EINTR at any point, not just before any data is written.

NOTES
       A  successful  return  from  write() does not make any guarantee that data has been committed to disk.  In fact, on some buggy implementations, it does not
       even guarantee that space has successfully been reserved for the data.  The only way to be sure is to call fsync(2) after you are  done  writing  all  your
       data.

       If a write() is interrupted by a signal handler before any bytes are written, then the call fails with the error EINTR; if it is interrupted after at least
       one byte has been written, the call succeeds, and returns the number of bytes written.

       On Linux, write() (and similar system calls) will transfer at most 0x7ffff000 (2,147,479,552) bytes, returning the number of  bytes  actually  transferred.
       (This is true on both 32-bit and 64-bit systems.)
       //write() 的成功返回并不能保证数据已提交到磁盘。 
       //事实上,在一些有缺陷的实现中,它甚至不能保证已成功为数据保留空间。 
       //唯一可以确定的方法是在完成所有数据的写入后调用 fsync(2)。

        //如果 write() 在写入任何字节之前被信号处理程序中断,则调用失败并返回错误 EINTR; 
        //如果在至少写入一个字节后中断,则调用成功,并返回写入的字节数。

        //在 Linux 上,write()(和类似的系统调用)将最多传输 0x7ffff000 (2,147,479,552) 字节,返回实际传输的字节数。
        //(在 32 位和 64 位系统上都是如此。)

BUGS
       According to POSIX.1-2008/SUSv4 Section XSI 2.9.7 ("Thread Interactions with Regular File Operations"):

           All  of  the following functions shall be atomic with respect to each other in the effects specified in POSIX.1-2008 when they operate on regular files
           or symbolic links: ...

       Among the APIs subsequently listed are write() and writev(2).  And among the effects that should be atomic across threads (and processes)  are  updates  of
       the  file offset.  However, on Linux before version 3.14, this was not the case: if two processes that share an open file description (see open(2)) perform
       a write() (or writev(2)) at the same time, then the I/O operations were not atomic with respect updating the file offset, with the result that  the  blocks
       of data output by the two processes might (incorrectly) overlap.  This problem was fixed in Linux 3.14.
       //根据 POSIX.1-2008/SUSv4 Section XSI 2.9.7(“线程与常规文件操作的交互”):

            //以下所有函数在对常规文件或符号链接进行操作时,在 POSIX.1-2008 中指定的效果中相互之间应是原子的: ...

        //随后列出的 API 包括 write() 和 writev(2)。 
        //跨线程(和进程)应该是原子的影响之一是文件偏移量的更新。 
        //但是,在 3.14 之前的 Linux 上,情况并非如此:如果共享打开文件描述(参见 open(2))的两个进程同时执行 write()(或 writev(2)),则 I /O 操作在更新文件偏移方面不是原子的,因此两个进程输出的数据块可能(不正确地)重叠。 
        //此问题已在 Linux 3.14 中修复。

SEE ALSO
       close(2), fcntl(2), fsync(2), ioctl(2), lseek(2), open(2), pwrite(2), read(2), select(2), writev(2), fwrite(3)

COLOPHON
       This  page  is part of release 4.04 of the Linux man-pages project.  A description of the project, information about reporting bugs, and the latest version
       of this page, can be found at http://www.kernel.org/doc/man-pages/.

Linux                                                                       2015-07-23                                                                    WRITE(2)
 Manual page write(2) line 65/106 (END) (press h for help or q to quit)


20230816

Linux中的C语言write()函数

在Linux系统编程中,write()函数是一个重要的系统调用,用于将数据从缓冲区写入文件或套接字。本文将深入探讨write()函数的用法和实现细节。

1. 函数声明与参数

在unistd.h头文件中,我们可以找到write函数的声明:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

write()函数有三个参数:

  • fd: 文件描述符,标识待写入的文件或者套接字。
  • buf: 指向要写入的数据的缓冲区。
  • count: 要写入的字节数。

返回值为实际写入的字节数,错误时返回-1,并设置errno。

2. 使用示例

以下是使用write()函数将字符串写入文件的示例:

#include <fcntl.h>
#include <unistd.h>

int main() {
    int filedesc = open("testfile.txt", O_WRONLY | O_APPEND);

    if (filedesc < 0) {
        return -1;
    }

    if (write(filedesc, "This will be output to testfile.txt\n", 36) != 36) {
        write(2, "There was an error writing to testfile.txt\n", 43);
        return -1;
    }

    return 0;
}

3. 错误处理

write()函数调用失败时,它会返回-1并设置全局变量errno来指示错误类型。以下是一些可能的错误代码:

  • EBADF: 参数fd不是有效的文件描述符或者不支持写操作。
  • EIO: I/O错误发生。
  • ENOSPC: 设备上没有剩余空间。

4. 注意事项

  • 写操作始终从文件的当前位置开始,如果需要在特定位置写入数据,需要先使用lseek()函数更改位置。
  • 如果write()在写入前被信号中断,则它可以重新启动,并写入尽可能多的数据,但这不是必须的。所以,write()可能只写入部分数据。

5. 性能考虑

write()函数每次调用都涉及内核态和用户态之间的切换,频繁调用可能导致性能下降。为了提高效率,可以考虑使用如writev()等聚集I/O函数,或者使用缓冲区批量写入数据。

参考文章

  1. Linux manual page - write(2)
  2. The GNU C Library: I/O Primitives

ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ        ‌‍ᅟᅠ

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐