UnixLinux中的read和write函数
⽂件描述符
  对于内核⽽⾔,所有打开的⽂件都通过⽂件描述符引⽤。⽂件描述符是⼀个⾮负整数。当打开⼀个现有⽂件或创建⼀个新⽂件时,内核向进程返回⼀个⽂件描述符。当读或写⼀个⽂件时,使⽤open或create返回的⽂件描述符表⽰该⽂件,将其作为参数传给read或write函数。
write函数
  write函数定义如下:
#include <unistd>
ssize_t write(int filedes, void *buf, size_t nbytes);
// 返回:若成功则返回写⼊的字节数,若出错则返回-1
// filedes:⽂件描述符
// buf:待写⼊数据缓存区
// nbytes:要写⼊的字节数
  同样,为了保证写⼊数据的完整性,在《UNIX⽹络编程 卷1》中,作者将该函数进⾏了封装,具体程序如下:
1 ssize_t                        /* Write "n" bytes to a descriptor. */
2 writen(int fd, const void *vptr, size_t n)
3 {
4    size_t nleft;
5    ssize_t nwritten;
6const char *ptr;
7
8    ptr = vptr;
9    nleft = n;
10while (nleft > 0) {
11if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
12if (nwritten < 0 && errno == EINTR)
13                nwritten = 0;        /* and call write() again */
14else
15return(-1);            /* error */
16        }
17
18        nleft -= nwritten;
19        ptr  += nwritten;
20    }
21return(n);
22 }
23/* end writen */
24
25void
26 Writen(int fd, void *ptr, size_t nbytes)
27 {
28if (writen(fd, ptr, nbytes) != nbytes)
29        err_sys("writen error");
30 }
read函数
  read函数定义如下:
#include <unistd>
ssize_t read(int filedes, void *buf, size_t nbytes);
// 返回:若成功则返回读到的字节数,若已到⽂件末尾则返回0,若出错则返回-1
// filedes:⽂件描述符
// buf:读取数据缓存区
// nbytes:要读取的字节数
  有⼏种情况可使实际读到的字节数少于要求读的字节数:
  1)读普通⽂件时,在读到要求字节数之前就已经达到了⽂件末端。例如,若在到达⽂件末端之前还有30个字节,⽽要求读100个字节,则read返回30,下⼀次再调⽤read时,它将返回0(⽂件末端)。
  2)当从终端设备读时,通常⼀次最多读⼀⾏。
  3)当从⽹络读时,⽹络中的缓存机构可能造成返回值⼩于所要求读的字结束。
  4)当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可⽤的字节数。
  5)当从某些⾯向记录的设备(例如磁带)读时,⼀次最多返回⼀个记录。
  6)当某⼀个信号造成中断,⽽已经读取了部分数据。
  在《UNIX⽹络编程 卷1》中,作者将该函数进⾏了封装,以确保数据读取的完整,具体程序如下:
1 ssize_t                        /* Read "n" bytes from a descriptor. */
2 readn(int fd, void *vptr, size_t n)
3 {
4    size_t nleft;
5    ssize_t nread;
6char *ptr;
7
8    ptr = vptr;
9    nleft = n;
10while (nleft > 0) {
11if ( (nread = read(fd, ptr, nleft)) < 0) {
12if (errno == EINTR)
13                nread = 0;        /* and call read() again */
14else
15return(-1);
16        } else if (nread == 0)
17break;                /* EOF */
18
19        nleft -= nread;
20        ptr  += nread;
21    }
22return(n - nleft);        /* return >= 0 */
23 }
24/* end readn */
25
26 ssize_t
27 Readn(int fd, void *ptr, size_t nbytes)
28 {
29    ssize_t        n;
30
31if ( (n = readn(fd, ptr, nbytes)) < 0)
32        err_sys("readn error");
33return(n);
34 }
  本⽂下半部分摘⾃博⽂。
read/write的语义:为什么会阻塞?
  先从write说起:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
  ⾸先,write成功返回,只是buf中的数据被复制到了kernel中的TCP发送缓冲区。⾄于数据什么时候被发往⽹络,什么时候被对⽅主机接收,什么时候被对⽅进程读取,系统调⽤层⾯不会给予任何保证和通知。
  write在什么情况下会阻塞?当kernel的该socket的发送缓冲区已满时。对于每个socket,拥有⾃⼰的send buffer和receive
buffer。从Linux 2.6开始,两个缓冲区⼤⼩都由系统来⾃动调节(autotuning),但⼀般在default和max之间浮动。
# 获取socket的发送/接受缓冲区的⼤⼩:(后⾯的值是在Linux 2.6.38 x86_64上测试的结果)
wmem_default      #126976
wmem_max        #131071
  已经发送到⽹络的数据依然需要暂存在send buffer中,只有收到对⽅的ack后,kernel才从buffer中清除这⼀部分数据,为后续发送数据腾出空间。接收端将收到的数据暂存在receive buffer中,⾃动进⾏确认。
但如果socket所在的进程不及时将数据从receive buffer中取出,最终导致receive buffer填满,由于TCP的滑动窗⼝和拥塞控制,接收端会阻⽌发送端向其发送数据。这些控制皆发⽣在TCP/IP栈中,对应⽤程序是透明的,应⽤程序继续发送数据,最终导致send buffer填满,write调⽤阻塞。
  ⼀般来说,由于接收端进程从socket读数据的速度跟不上发送端进程向socket写数据的速度,最终导致发送端write调⽤阻塞。
  ⽽read调⽤的⾏为相对容易理解,从socket的receive buffer中拷贝数据到应⽤程序的buffer中。read调⽤阻塞,通常是发送端的数据没有到达。
blocking(默认)和nonblock模式下read/write⾏为的区别
  将socket fd设置为nonblock(⾮阻塞)是在服务器编程中常见的做法,采⽤blocking IO并为每⼀个client创建⼀个线程的模式开销巨⼤且可扩展性不佳(带来⼤量的切换开销),更为通⽤的做法是采⽤线程池+Nonblock I/O+Multiplexing(select/poll,以及Linux上特有的epoll)。
1// 设置⼀个⽂件描述符为nonblock
2int set_nonblocking(int fd)
3 {
4int flags;
5if ((flags = fcntl(fd, F_GETFL, 0)) == -1)
6        flags = 0;
7return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
8 }
  ⼏个重要的结论:
  1. read总是在接收缓冲区有数据时⽴即返回,⽽不是等到给定的read buffer填满时返回。
  只有当receive buffer为空时,blocking模式才会等待,⽽nonblock模式下会⽴即返回-1(errno = EAGAIN或EWOULDBLOCK)
  注:阻塞模式下,当对⽅socket关闭时,read会返回0。
  2. blocking的write只有在缓冲区⾜以放下整个buffer时才返回(与blocking read并不相同)
  nonblock write则是返回能够放下的字节数,之后调⽤则返回-1(errno = EAGAIN或EWOULDBLOCK)
  对于blocking的write有个特例:当write正阻塞等待时对⾯关闭了socket,则write则会⽴即将剩余缓冲区填满并返回所写的字节数,再次调⽤则write失败(connection reset by peer),这正是下个⼩节要提到的:
read/write对连接异常的反馈⾏为
  对应⽤程序来说,与另⼀进程的TCP通信其实是完全异步的过程:
  1. 我并不知道对⾯什么时候、能否收到我的数据
  2. 我不知道什么时候能够收到对⾯的数据
  3. 我不知道什么时候通信结束(主动退出或是异常退出、机器故障、⽹络故障等等)
linux怎么读文件内容
  对于1和2,采⽤write() -> read() -> write() -> read() ->...的序列,通过blocking read或者nonblock read+轮询的⽅式,应⽤程序基于可以保证正确的处理流程。
  对于3,kernel将这些事件的“通知”通过read/write的结果返回给应⽤层。
  假设A机器上的⼀个进程a正在和B机器上的进程b通信:某⼀时刻a正阻塞在socket的read调⽤上(或者在nonblock下轮询socket)
  当b进程终⽌时,⽆论应⽤程序是否显式关闭了socket(OS会负责在进程结束时关闭所有的⽂件描述符,对于socket,则会发送⼀个FIN包到对⾯)。
  ”同步通知“:进程a对已经收到FIN的socket调⽤read,如果已经读完了receive buffer的剩余字节,则会返回EOF:0
  ”异步通知“:如果进程a正阻塞在read调⽤上(前⾯已经提到,此时receive buffer⼀定为空,因为read在receive buffer有内容时就会返回),则read调⽤⽴即返回EOF,进程a被唤醒。
  socket在收到FIN后,虽然调⽤read会返回EOF,但进程a依然可以其调⽤write,因为根据TCP协议,收到对⽅的FIN包只意味着对⽅不会再发送任何消息。 在⼀个双⽅正常关闭的流程中,收到FIN包的⼀端将剩余数据发送给对⾯(通过⼀次或多次write),然后关闭socket。
  但是事情远远没有想象中简单。优雅地(gracefully)关闭⼀个TCP连接,不仅仅需要双⽅的应⽤程序遵守约定,中间还不能出任何差错。
  假如b进程是异常终⽌的,发送FIN包是OS代劳的,b进程已经不复存在,当机器再次收到该socket的
消息时,会回应RST(因为拥有该socket的进程已经终⽌)。a进程对收到RST的socket调⽤write时,操作系统会给a进程发送SIGPIPE,默认处理动作是终⽌进程,知道你的进程为什么毫⽆征兆地死亡了吧:)
  from 《Unix Network programming, vol1》 3rd Edition:
"It is okay to write to a socket that has received a FIN, but it is an error to write to a socket that has received an RST."
  通过以上的叙述,内核通过socket的read/write将双⽅的连接异常通知到应⽤层,虽然很不直观,似乎也够⽤。
  这⾥说⼀句题外话:
  不知道有没有同学会和我有⼀样的感慨:在写TCP/IP通信时,似乎没怎么考虑连接的终⽌或错误,只是在read/write错误返回时关闭socket,程序似乎也能正常运⾏,但某些情况下总是会出奇怪的问题。想完美处理各种错误,却发现怎么也做不对。
  原因之⼀是:socket(或者说TCP/IP栈本⾝)对错误的反馈能⼒是有限的。
  考虑这样的错误情况:
  不同于b进程退出(此时OS会负责为所有打开的socket发送FIN包),当B机器的OS崩溃(注意不同于⼈为关机,因为关机时所有进程的退出动作依然能够得到保证)/主机断电/⽹络不可达时,a进程根本不会收到FIN包作为连接终⽌的提⽰。
  如果a进程阻塞在read上,那么结果只能是永远的等待。
  如果a进程先write然后阻塞在read,由于收不到B机器TCP/IP栈的ack,TCP会持续重传12次(时间跨度⼤约为9分钟),然后在阻塞的read调⽤上返回错误:ETIMEDOUT/EHOSTUNREACH/ENETUNREACH
  假如B机器恰好在某个时候恢复和A机器的通路,并收到a某个重传的pack,因为不能识别所以会返回⼀个RST,此时a进程上阻塞的read调⽤会返回错误ECONNREST
  恩,socket对这些错误还是有⼀定的反馈能⼒的,前提是在对⾯不可达时你依然做了⼀次write调⽤,⽽不是轮询或是阻塞在read上,那么总是会在重传的周期内检测出错误。如果没有那次write调⽤,应⽤层永远不会收到连接错误的通知。
  write的错误最终通过read来通知应⽤层,有点阴差阳错?
还需要做什么?
  ⾄此,我们知道了仅仅通过read/write来检测异常情况是不靠谱的,还需要⼀些额外的⼯作:
  1. 使⽤TCP的KEEPALIVE功能?
cat /proc/sys/net/ipv4/tcp_keepalive_time
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
cat /proc/sys/net/ipv4/tcp_keepalive_probes
  以上参数的⼤致意思是:keepalive routine每2⼩时(7200秒)启动⼀次,发送第⼀个probe(探测包),如果在75秒内没有收到对⽅应答则重发probe,当连续9个probe没有被应答时,认为连接已断。(此时read调⽤应该能够返回错误,待测试)
  但在我印象中keepalive不太好⽤,默认的时间间隔太长,⼜是整个TCP/IP栈的全局参数:修改会影响其他进程,Linux的下似乎可以修改per socket的keepalive参数?(希望有使⽤经验的⼈能够指点⼀下),但是这些⽅法不是portable的。
  2. 进⾏应⽤层的⼼跳
  严格的⽹络程序中,应⽤层的⼼跳协议是必不可少的。虽然⽐TCP⾃带的keep alive要⿇烦不少,但有其最⼤的优点:可控。
  当然,也可以简单⼀点,针对连接做timeout,关闭⼀段时间没有通信的”空闲“连接。这⾥可以参考⼀篇⽂章:
  by 陈硕
参考资料
  《UNIX环境⾼级编程》
  《UNIX⽹络编程 卷1》

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。