文件描述符

  对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读或写一个文件时,使用open或create返回的文件描述符表示该文件,将其作为参数传给read或write函数。

write函数

  write函数定义如下:

#include <unistd>
ssize_t write(int filedes, void *buf, size_t nbytes);
// 返回:若成功则返回写入的字节数,若出错则返回-1
// filedes:文件描述符
// buf:待写入数据缓存区
// nbytes:要写入的字节数

  同样,为了保证写入数据的完整性,在《UNIX网络编程 卷1》中,作者将该函数进行了封装,具体程序如下:

 ssize_t                        /* Write "n" bytes to a descriptor. */
writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr; ptr = vptr;
nleft = n;
while (nleft > ) {
if ( (nwritten = write(fd, ptr, nleft)) <= ) {
if (nwritten < && errno == EINTR)
nwritten = ; /* and call write() again */
else
return(-); /* error */
} nleft -= nwritten;
ptr += nwritten;
}
return(n);
}
/* end writen */ void
Writen(int fd, void *ptr, size_t nbytes)
{
if (writen(fd, ptr, nbytes) != nbytes)
err_sys("writen error");
}

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》中,作者将该函数进行了封装,以确保数据读取的完整,具体程序如下:

 ssize_t                        /* Read "n" bytes from a descriptor. */
readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char *ptr; ptr = vptr;
nleft = n;
while (nleft > ) {
if ( (nread = read(fd, ptr, nleft)) < ) {
if (errno == EINTR)
nread = ; /* and call read() again */
else
return(-);
} else if (nread == )
break; /* EOF */ nleft -= nread;
ptr += nread;
}
return(n - nleft); /* return >= 0 */
}
/* end readn */ ssize_t
Readn(int fd, void *ptr, size_t nbytes)
{
ssize_t n; if ( (n = readn(fd, ptr, nbytes)) < )
err_sys("readn error");
return(n);
}

  

  本文下半部分摘自博文浅谈TCP/IP网络编程中socket的行为

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上测试的结果)
sysctl net.core.wmem_default       #126976
sysctl net.core.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)。

 // 设置一个文件描述符为nonblock
int set_nonblocking(int fd)
{
int flags;
if ((flags = fcntl(fd, F_GETFL, )) == -)
flags = ;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

  几个重要的结论:

  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. 我不知道什么时候通信结束(主动退出或是异常退出、机器故障、网络故障等等)

  对于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,关闭一段时间没有通信的”空闲“连接。这里可以参考一篇文章:

  Muduo 网络编程示例之八:Timing wheel 踢掉空闲连接 by 陈硕

参考资料

  《UNIX环境高级编程》

  《UNIX网络编程 卷1》

  浅谈TCP/IP网络编程中socket的行为

Unix/Linux中的read和write函数的更多相关文章

  1. [转]unix/linux中的dup()系统调用

    [转]unix/linux中的dup()系统调用    在linux纷繁复杂的内核代码中,sys_dup()的代码也许称得上是最简单的之一了,但是就是这么一个简单的系统调用,却成就了unix/linu ...

  2. 如何设置UNIX/Linux中新创建目录或文件的默认权限

    在unix或者linux中,每创建一个文件或者目录时,这个文件或者目录都具有一个默认的权限,比如目录755,文件644,那么这些默认权限是怎么控制的呢? 答案是"umask"权限掩 ...

  3. unix/linux中图形界面那些事

    我们知道unix/linux刚开始的时候是没有图形界面的,随着时代的发展,排版.制图.多媒体应用越来越普遍了,这些需求都需要用到图形界面(Graphical User Interface).为此,MI ...

  4. Unix/Linux中的grep命令(转)

    本文转载自:如何使用Unix/Linux grep命令——磨刀不误砍柴工系列.该博文条理很清晰. grep简介 grep在一个或多个文件中查找与模式字符串(pattern)匹配的行,并将搜索的结果打印 ...

  5. Unix/Linux中/usr目录的由来

    在Linux系统中,有一个很重要的目录——/usr目录.关于这个目录名称的由来,网上主要有下面几种说法: user的缩写 User Shareable Read-only的缩写 Unix/User S ...

  6. Unix/Linux中shell调用sqlplus的方式

    Unix/Linux下,shell脚本调用sqlplus的几种方式介绍: 一.最简单的shell调用sqlplus #!/bin/bash sqlplus -S /nolog > sqlplus ...

  7. Unix/Linux中的fork函数

    fork函数介绍 一个现有进程可以调用fork函数创建一个新进程.该函数定义如下: #include <unistd.h> pid_t fork(void); // 返回:若成功则在子进程 ...

  8. #include<unistd.h>存在linux中,含有系统服务的函数

    #include<unistd.h> linux标准库#include <unistd.h>与windows的#include <windows.h>(C语言开发) ...

  9. Linux中exec()执行文件系列函数的使用说明

    函数原型: 描述:    exec()系列函数使用新的进程映像替换当前进程映像.    工作方式没有什么差别, 只是参数传递的方式不同罢了. 说明:    1. 这6个函数可分为两大类: execl( ...

随机推荐

  1. Docker学习笔记4: Docker-Compose—简化复杂容器应用的利器

    本文转载自http://www.tuicool.com/articles/AnIVJn. 因Python语言,个人也没学过,不是太熟悉,这篇文章的代码格式排版不准确爆了很多错,让我走了好多坑,不过还是 ...

  2. Android5.0特性ToolBar

    >Toolbar是什么?大概说一下它的官方介绍.Toolbar是应用的内容的标准工具栏,`可以说是Actionbar的升级版`,两者不是独立关系,要使用Toolbar还是得跟ActionBar扯 ...

  3. 用scheme最基本的元素定义排序函数

    用到的元素有9个: define,if,null?,cons car,cdr,lambda,let,named let, 其实let 和 named let可以去掉.但那样会带来性能和可读性下降的问题 ...

  4. NSDictionary writeToFile:atomically: 时失败

    一.现象:如下图,当NSDictionary调用 writeToFile:atomically: 时,如果容器里面包含的对象(即通过 objectForKey: 拿到的对象),为非property l ...

  5. 开源框架Volley的使用《二》[NetWorkImageView&&LruCache&ImageLoader]

    转载本专栏每一篇博客请注明转载出处地址,尊重原创.此博客转载链接地址:小杨的博客    http://blog.csdn.net/qq_32059827/article/details/5278849 ...

  6. 64位Linux下安装mysql-5.7.13-linux-glibc2.5-x86_64 || 转载:http://www.cnblogs.com/gaojupeng/p/5727069.html

    由于公司临时让将Oracle的数据移植到mysql上面,所以让我在公司服务器上面安装一下mysql.下面就是我的安装过程以及一些错误解决思路.其实对于不同版本安装大体都有差不多. 1. 从官网下载 m ...

  7. Android Multimedia框架总结(九)Stagefright框架之数据处理及到OMXCodec过程

    转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼:http://blog.csdn.net/hejjunlin/article/details/52532085 不知不觉到第九篇了,感觉还有 ...

  8. ListView嵌套GridView

    首先,我们通过两个实例来了解下本篇文章所讲的重点,看下图: 微博: 陌陌: 大家应该对这两款软件并不陌生,接下来,我将列举下本文将要实现的几个点: 1.ListView嵌套GridView,互不冲突, ...

  9. J2EE进阶(四)Spring配置文件详解

    J2EE进阶(四)Spring配置文件详解 前言 Spring配置文件是用于指导Spring工厂进行Bean生产.依赖关系注入(装配)及Bean实例分发的"图纸".Java EE程 ...

  10. UNIX网络编程——套接字选项(SO_REUSEADDR)

    1.一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以被再次使用. SO_REUSEADDR用于对TCP套接字处于TIME_WAIT状态下的sock ...