开头复习一下一个标志:
O_APPEND
关于 O_APPEND
当多核、众核CPU成为线上服务器的主流时,为了充分利用系统的多核处理能力,最常见的思路是将原先单进程单线程的程序改造成多进程或多线程的程序。
对于多进程或多线程的程序,一种常见的需求就是并发写日志的需求。为了解决这个需求,最容易想到的思路是利用linux进程或线程同步机制保证并发写的时候日志不会相互交错。对于多进程,常见的解决思路是利用文件锁机制。
既然有了日志文件资源竞争,就不可避免的需要考虑两个方面的内容:
(1)保证每个进程或线程在写入一条日志的过程中对日志文件是独享的
(2)避免资源竞争者之间产生死锁、忙等等各种问题
按常规的思路来解决这个需求会导致程序的复杂性和执行效率都会受到影响,幸亏linux的open系统调用为我们提供了一个O_APPEND模式,完美地解决了这个问题。
首先看看O_APPEND参数的描述:
O_APPEND
The file is opened in append mode. Before each write(), the file
offset is positioned at the end of the file, as if with lseek().
O_APPEND may lead to corrupted files on NFS file systems if more
than one process appends data to a file at once. This is because
NFS does not support appending to a file, so the client kernel has
to simulate it, which can’t be done without a race condition.
也就是说,当我们以O_APPEND参数单开一个文件时,每次调用write操作都会写到文件的末尾。通过这种机制保证了每个进程或线程写日志的原子性。
bind函数
若指定端口号为0,那么内核就在bind被调用时选择一个临时端口; 如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址
对于IPv4,通配地址常由 INADDR_ANY 来指定,值一般为0,其告诉内核去选择IP地址。(无论是网络字节序还是主机字节序都是0,用不用htonl无所谓)
若让bind选择一个端口号,bind并不会返回所选择的端口号,必须调用 getsockname 来返回协议地址。
进程捆绑非通配IP地址到套接字上的常见例子是在为多个组织提供web服务器的主机上。首先每个组织都得有自己到域名,其次每个域名都映射到不同到IP地址,不过通常仍在同一个子网。然后,把所有这些IP地址都定义成单个网络接口到别名。这么一来,IP层将接受所有目的地址为任一个别名地址的外来数据。最后,为每个组织启动一个htpp服务器到副本,每个服务器仅捆绑相应组织到IP地址。
bind函数常见返回错误为 EADDRINUSE(地址已使用)
listen函数
为了理解backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列
1、未完成队列 : 每个这样的SYN分节对应其中一项: 已知某个客户发出并达到服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于 SYN_RCVD状态
2、已完成连接队列,每个已完成TCP三路握手过程的客户对应其中一项,这些套接字处于ESTABLISHED状态
当来自客户的SYN到达时,TCP在未完成队列中创建一个新项,然后响应以三路握手的第二个分节: 服务器的SYN响应,其中捎带对客户SYN的ACK。这一项一直保留在未完成连接队列中,知道三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止。
如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。 当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。
关于这两个队列:
1、backlog参数曾被规定为这两个队列总和的最大值
2、backlog可能被增设模糊因子(比如在给定基础上乘1.5 或 加一)
3、不用将backlog定义为0,不同实现对此有不同解释
4、当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送RST。这样做是因为: 这种情况是暂时的,客户TCP将重发SYN,期望不久就能在队列中找到空余空间。 要是服务器立即响应一个RST,客户connect就会立即返回一个错误,而不是让TCP的正常重传机制来处理。 另外,客户端无法区分响应SYN的RST究竟是 “该端口没有服务在监听”还是“该端口有服务在监听,但队列满了”
当三路握手完成时,客户端的执行要先于服务器端。因为:
客户接收到三路握手的第二个分节时,connect返回,而服务器要直接收到三路握手的第三个分节才返回,即在connect返回后再过一般RTT才返回
针对UNP文件夹中的tcpserv01 和 tcpcli01 这对服务器和客户端,当我们在客户端键入EOF时:
1、fgets返回空指针,于是str_cli返回
2、当str_cli返回到客户端main函数,main通过exit终止
3、进程终止的处理工作是关闭所有打开的文件描述符,因此客户端打开的文件描述符由内核关闭。这导致客户TCP发送一个FIN给服务器,服务器TCP则以ACK响应,这就是TCP连接终止序列的前半部分。至此,服务器套接字处于CLOSE_WAIT状态,客户套接字则处于FIN_WAIT_2状态
4、当服务器TCP接收FIN时,服务器子进程阻塞于readline函数,于是readline返回0。这导致str_echo函数返回服务器子进程main函数
5、服务器子进程通过exit终止
6、服务器子进程中打开的所有文件描述符随之关闭。由子进程来关闭已连接套接字会引发TCP连接终止序列的最后两个分节:一个服务器到客户的FIN和一个从客户到服务器的ACK。至此,连接完全终止,客户套接字进入TIME_WAIT状态。
7、进程终止处理的另一部分内容是:在服务器子进程终止时,给父进程发送一个SIGCHLD信号
tcpdump监测运行如下:
不过我有个疑惑,为什么客户端进程会进入TIME_WAIT状态呢?这又是怎么产生的有什么作用呢?
http://elf8848.iteye.com/blog/1739571
这里讲解了一些这个状态的相关信息 以及 客户端/服务器主动关闭后发生的情况。以下搬运一些:
TIME_WAIT状态存在的理由
----------------------------
TCP/IP协议就是这样设计的,是不可避免的。主要有两个原因:
1)可靠地实现TCP全双工连接的终止
TCP协议在关闭连接的四次握手过程中,最终的ACK是由主动关闭连接的一端(后面统称A端)发出的,如果这个ACK丢失,对方(后面统称B端)将重发出最终的FIN,因此A端必须维护状态信息(TIME_WAIT)允许它重发最终的ACK。如果A端不维持TIME_WAIT状态,而是处于CLOSED 状态,那么A端将响应RST分节,B端收到后将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。
因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个分节任何一个分节的丢失情况,主动关闭连接的A端必须维持TIME_WAIT状态 。
2)允许老的重复分节在网络中消逝
TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个迟到的迷途分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又重新建立起一个相同的IP和端口之间的“新连接”,“前一个连接”的迷途重复分组在“前一个连接”终止后到达,而被“新连接”收到了。为了避免这个情况,TCP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消逝。
如下:
Unix网络编程的 2.6 小节比较详细的说明了TCP状态转换
accept返回前连接终止
类似于 EINTR 错误,还有一种情况也能导致accept返回一个非致命错误,这种情况只需要再次调用accept
这里,三路握手完成从而连接建立后,客户TCP却发送了一个RST。从服务器端看,就在该连接已由TCP排队,等着服务器进程调用accept的时候RST到达
POSIX规定返回的错误为 ECONNABORTED,服务器可以忽略它再次调用accept即可。但因为不同实现可能处理方式会不同
服务器进程终止(并非服务器主机)
步骤如下:
1、我们在同一主机上启动服务器和客户端,验证一切正常
2、找到服务器子进程的进程ID,kill掉。作为进程终止处理的部分工作,子进程中所有打开着的描述符都被关闭。这就导致向客户发送一个FIN,而客户TCP则响应一个ACK。即TCP终止连接的前半部分
3、SIGCHLD被发送至父进程,得到正确处理
4、客户上没有发生任何特殊的事。客户TCP收到来自服务器TCP的FINE并响应以ACK,然而问题是客户进程阻塞在fgets上,等待从终端中接受一段文本
5、这时候,我们发现:
父服务器进程: LISTEN
子服务进程: FIN_WAIT_2
客户端进程: CLOSE_WAIT
可以看出TCP终止序列的前半部分已经结束
6、我们在kill掉子服务进程后,客户端上再键入一段文本,在readline处返回了错误。
我们键入文本后,str_cli调用writen,客户TCP接着把数据发送诶服务器。(TCP允许这么做,因为客户TCP接收到FIN只是表示服务器进程已经关闭了连接的服务器端,从而不再往其中发送数据而已。FIN的接收并没有告诉客户端TCP 服务器进程已经终止)
当服务器TCP接收到来自客户的数据时,既然先前打开套接字的进程已经终止了,于是响应一个RST
7、然而客户进程看不到这个RST,因为它在调用writen后立即调用readline,并且由于第二步接收的FIN,所调用的readline立即返回0(EOF)。我们的客户并未预期收到EOF,于是标记错误并退出
8、客户终止,所打开的描述父都被关闭
本例子的问题在于,客户端实际在应对两个描述符,套接字和用户输入。它不能阻塞在某个特定源的输入上,而是应该阻塞在其中任何一个源的输入上,这正式select和poll的目的之一。
SIGPIPE信号
(接上服务器终止发出RST),要是客户不理会readline函数返回的错误,反而写入更多数据到服务器端,会如何?
当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号(默认终止进程)
不论进程如何处理该信号,写操作都将返回EPIPE错误
那如何在第一次写操作而不是第二次写操作时捕获该信号量呢?
这是不可能的
按照上述讨论,第一次写操作引发RST,第二次写操作引发SIGPIPE信号量。写一个已接收了FIN的套接字不成问题,但写一个已接收了RST的套接字则是一个错误
服务器主机崩溃
服务器主机崩溃后重启
这里我们假设在服务器主机崩溃时客户不主动给服务器发送数据,那么客户将不会知道服务器主机已崩溃(没有SO_KEEPALIVE套接字选项)
所发生的步骤如下:
1、我们启动服务器与客户端,并确认连接
2、服务器主机崩溃并重启
3、在客户上键入文本,将作为TCP数据分节发送至服务器
4、当服务器主机重启后,它将丢失崩溃前的所有信息,因此TCP对于所收到的来自客户的数据分节响应RST
5、客户TCP收到该RST,客户正阻塞与readline调用,导致该调用返回ECONNRESET错误
此时,若检测服务器主机是否崩溃很重要,那么需要采取其他技术(套接字选项等)
服务器主机关机
Unix系统关机时候,init进程先给所有进程发送SIGTERM信号(默认终止进程),等待一段时间,给所有仍在运行的进程发送SIGKILL信号
关于 O_APPEND
当多核、众核CPU成为线上服务器的主流时,为了充分利用系统的多核处理能力,最常见的思路是将原先单进程单线程的程序改造成多进程或多线程的程序。
对于多进程或多线程的程序,一种常见的需求就是并发写日志的需求。为了解决这个需求,最容易想到的思路是利用linux进程或线程同步机制保证并发写的时候日志不会相互交错。对于多进程,常见的解决思路是利用文件锁机制。
既然有了日志文件资源竞争,就不可避免的需要考虑两个方面的内容:
(1)保证每个进程或线程在写入一条日志的过程中对日志文件是独享的
(2)避免资源竞争者之间产生死锁、忙等等各种问题
按常规的思路来解决这个需求会导致程序的复杂性和执行效率都会受到影响,幸亏linux的open系统调用为我们提供了一个O_APPEND模式,完美地解决了这个问题。
首先看看O_APPEND参数的描述:
O_APPEND
The file is opened in append mode. Before each write(), the file
offset is positioned at the end of the file, as if with lseek().
O_APPEND may lead to corrupted files on NFS file systems if more
than one process appends data to a file at once. This is because
NFS does not support appending to a file, so the client kernel has
to simulate it, which can’t be done without a race condition.
也就是说,当我们以O_APPEND参数单开一个文件时,每次调用write操作都会写到文件的末尾。通过这种机制保证了每个进程或线程写日志的原子性。
bind函数
int bind(int scokfd, const struct sockaddr *myaddr,socklen_t len);
调用bind可以指定一个端口号,或指定一个IP地址,也可以两者都指定,还可以都不指定
进程指定 结果
IP地址 端口
通配地址 0 内核选择IP地址和端口
通配地址 非0 内核选择IP地址,进程指定端口
本地IP地址 0 进程指定IP地址,内核选择端口
本地IP地址 非0 进程指定IP地址和端口
若指定端口号为0,那么内核就在bind被调用时选择一个临时端口; 如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址
对于IPv4,通配地址常由 INADDR_ANY 来指定,值一般为0,其告诉内核去选择IP地址。(无论是网络字节序还是主机字节序都是0,用不用htonl无所谓)
若让bind选择一个端口号,bind并不会返回所选择的端口号,必须调用 getsockname 来返回协议地址。
进程捆绑非通配IP地址到套接字上的常见例子是在为多个组织提供web服务器的主机上。首先每个组织都得有自己到域名,其次每个域名都映射到不同到IP地址,不过通常仍在同一个子网。然后,把所有这些IP地址都定义成单个网络接口到别名。这么一来,IP层将接受所有目的地址为任一个别名地址的外来数据。最后,为每个组织启动一个htpp服务器到副本,每个服务器仅捆绑相应组织到IP地址。
bind函数常见返回错误为 EADDRINUSE(地址已使用)
listen函数
int listen(int sockfd, int backlog);
当socket函数创建一个套接字时,它被假设为一个主动套接字(即它是一个将调用connect发起连接到客户套接字)。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。将套接字从 CLOSED 状态转换为 LISTEN状态。
函数第二个参数规定了内核应该为相应套接字排队的最大连接个数。
为了理解backlog参数,我们必须认识到内核为任何一个给定的监听套接字维护两个队列
1、未完成队列 : 每个这样的SYN分节对应其中一项: 已知某个客户发出并达到服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接字处于 SYN_RCVD状态
2、已完成连接队列,每个已完成TCP三路握手过程的客户对应其中一项,这些套接字处于ESTABLISHED状态
当来自客户的SYN到达时,TCP在未完成队列中创建一个新项,然后响应以三路握手的第二个分节: 服务器的SYN响应,其中捎带对客户SYN的ACK。这一项一直保留在未完成连接队列中,知道三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止。
如果三路握手正常完成,该项就从未完成连接队列移到已完成连接队列的队尾。 当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。
关于这两个队列:
1、backlog参数曾被规定为这两个队列总和的最大值
2、backlog可能被增设模糊因子(比如在给定基础上乘1.5 或 加一)
3、不用将backlog定义为0,不同实现对此有不同解释
4、当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送RST。这样做是因为: 这种情况是暂时的,客户TCP将重发SYN,期望不久就能在队列中找到空余空间。 要是服务器立即响应一个RST,客户connect就会立即返回一个错误,而不是让TCP的正常重传机制来处理。 另外,客户端无法区分响应SYN的RST究竟是 “该端口没有服务在监听”还是“该端口有服务在监听,但队列满了”
当三路握手完成时,客户端的执行要先于服务器端。因为:
客户接收到三路握手的第二个分节时,connect返回,而服务器要直接收到三路握手的第三个分节才返回,即在connect返回后再过一般RTT才返回
针对UNP文件夹中的tcpserv01 和 tcpcli01 这对服务器和客户端,当我们在客户端键入EOF时:
1、fgets返回空指针,于是str_cli返回
2、当str_cli返回到客户端main函数,main通过exit终止
3、进程终止的处理工作是关闭所有打开的文件描述符,因此客户端打开的文件描述符由内核关闭。这导致客户TCP发送一个FIN给服务器,服务器TCP则以ACK响应,这就是TCP连接终止序列的前半部分。至此,服务器套接字处于CLOSE_WAIT状态,客户套接字则处于FIN_WAIT_2状态
4、当服务器TCP接收FIN时,服务器子进程阻塞于readline函数,于是readline返回0。这导致str_echo函数返回服务器子进程main函数
5、服务器子进程通过exit终止
6、服务器子进程中打开的所有文件描述符随之关闭。由子进程来关闭已连接套接字会引发TCP连接终止序列的最后两个分节:一个服务器到客户的FIN和一个从客户到服务器的ACK。至此,连接完全终止,客户套接字进入TIME_WAIT状态。
7、进程终止处理的另一部分内容是:在服务器子进程终止时,给父进程发送一个SIGCHLD信号
tcpdump监测运行如下:
建立连接:
16:31:16.162803 IP localhost.36649 > localhost.ruptime: Flags [S], seq 1521612678, win 65495, options [mss 65495,sackOK,TS val 10355037 ecr 0,nop,wscale 7], length 0
16:31:16.162889 IP localhost.ruptime > localhost.36649: Flags [S.], seq 4184666562, ack 1521612679, win 65483, options [mss 65495,sackOK,TS val 10355037 ecr 10355037,nop,wscale 7], length 0
16:31:16.162944 IP localhost.36649 > localhost.ruptime: Flags [.], ack 1, win 512, options [nop,nop,TS val 10355038 ecr 10355037], length 0
按下ctrl+D:
16:32:35.831165 IP localhost.36649 > localhost.ruptime: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 10434706 ecr 10355037], length 0
16:32:35.831440 IP localhost.ruptime > localhost.36649: Flags [F.], seq 1, ack 2, win 512, options [nop,nop,TS val 10434706 ecr 10434706], length 0
16:32:35.831499 IP localhost.36649 > localhost.ruptime: Flags [.], ack 2, win 512, options [nop,nop,TS val 10434706 ecr 10434706], length 0
不过我有个疑惑,为什么客户端进程会进入TIME_WAIT状态呢?这又是怎么产生的有什么作用呢?
http://elf8848.iteye.com/blog/1739571
这里讲解了一些这个状态的相关信息 以及 客户端/服务器主动关闭后发生的情况。以下搬运一些:
TIME_WAIT状态存在的理由
----------------------------
TCP/IP协议就是这样设计的,是不可避免的。主要有两个原因:
1)可靠地实现TCP全双工连接的终止
TCP协议在关闭连接的四次握手过程中,最终的ACK是由主动关闭连接的一端(后面统称A端)发出的,如果这个ACK丢失,对方(后面统称B端)将重发出最终的FIN,因此A端必须维护状态信息(TIME_WAIT)允许它重发最终的ACK。如果A端不维持TIME_WAIT状态,而是处于CLOSED 状态,那么A端将响应RST分节,B端收到后将此分节解释成一个错误(在java中会抛出connection reset的SocketException)。
因而,要实现TCP全双工连接的正常终止,必须处理终止过程中四个分节任何一个分节的丢失情况,主动关闭连接的A端必须维持TIME_WAIT状态 。
2)允许老的重复分节在网络中消逝
TCP分节可能由于路由器异常而“迷途”,在迷途期间,TCP发送端可能因确认超时而重发这个分节,迷途的分节在路由器修复后也会被送到最终目的地,这个迟到的迷途分节到达时可能会引起问题。在关闭“前一个连接”之后,马上又重新建立起一个相同的IP和端口之间的“新连接”,“前一个连接”的迷途重复分组在“前一个连接”终止后到达,而被“新连接”收到了。为了避免这个情况,TCP协议不允许处于TIME_WAIT状态的连接启动一个新的可用连接,因为TIME_WAIT状态持续2MSL,就可以保证当成功建立一个新TCP连接的时候,来自旧连接重复分组已经在网络中消逝。
如下:
tcp 0 0 localhost:36650 localhost:13000 TIME_WAIT
Unix网络编程的 2.6 小节比较详细的说明了TCP状态转换
accept返回前连接终止
类似于 EINTR 错误,还有一种情况也能导致accept返回一个非致命错误,这种情况只需要再次调用accept
这里,三路握手完成从而连接建立后,客户TCP却发送了一个RST。从服务器端看,就在该连接已由TCP排队,等着服务器进程调用accept的时候RST到达
POSIX规定返回的错误为 ECONNABORTED,服务器可以忽略它再次调用accept即可。但因为不同实现可能处理方式会不同
服务器进程终止(并非服务器主机)
步骤如下:
1、我们在同一主机上启动服务器和客户端,验证一切正常
2、找到服务器子进程的进程ID,kill掉。作为进程终止处理的部分工作,子进程中所有打开着的描述符都被关闭。这就导致向客户发送一个FIN,而客户TCP则响应一个ACK。即TCP终止连接的前半部分
3、SIGCHLD被发送至父进程,得到正确处理
4、客户上没有发生任何特殊的事。客户TCP收到来自服务器TCP的FINE并响应以ACK,然而问题是客户进程阻塞在fgets上,等待从终端中接受一段文本
5、这时候,我们发现:
父服务器进程: LISTEN
子服务进程: FIN_WAIT_2
客户端进程: CLOSE_WAIT
可以看出TCP终止序列的前半部分已经结束
6、我们在kill掉子服务进程后,客户端上再键入一段文本,在readline处返回了错误。
我们键入文本后,str_cli调用writen,客户TCP接着把数据发送诶服务器。(TCP允许这么做,因为客户TCP接收到FIN只是表示服务器进程已经关闭了连接的服务器端,从而不再往其中发送数据而已。FIN的接收并没有告诉客户端TCP 服务器进程已经终止)
当服务器TCP接收到来自客户的数据时,既然先前打开套接字的进程已经终止了,于是响应一个RST
7、然而客户进程看不到这个RST,因为它在调用writen后立即调用readline,并且由于第二步接收的FIN,所调用的readline立即返回0(EOF)。我们的客户并未预期收到EOF,于是标记错误并退出
8、客户终止,所打开的描述父都被关闭
本例子的问题在于,客户端实际在应对两个描述符,套接字和用户输入。它不能阻塞在某个特定源的输入上,而是应该阻塞在其中任何一个源的输入上,这正式select和poll的目的之一。
SIGPIPE信号
(接上服务器终止发出RST),要是客户不理会readline函数返回的错误,反而写入更多数据到服务器端,会如何?
当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号(默认终止进程)
不论进程如何处理该信号,写操作都将返回EPIPE错误
那如何在第一次写操作而不是第二次写操作时捕获该信号量呢?
这是不可能的
按照上述讨论,第一次写操作引发RST,第二次写操作引发SIGPIPE信号量。写一个已接收了FIN的套接字不成问题,但写一个已接收了RST的套接字则是一个错误
服务器主机崩溃
服务器主机崩溃后重启
这里我们假设在服务器主机崩溃时客户不主动给服务器发送数据,那么客户将不会知道服务器主机已崩溃(没有SO_KEEPALIVE套接字选项)
所发生的步骤如下:
1、我们启动服务器与客户端,并确认连接
2、服务器主机崩溃并重启
3、在客户上键入文本,将作为TCP数据分节发送至服务器
4、当服务器主机重启后,它将丢失崩溃前的所有信息,因此TCP对于所收到的来自客户的数据分节响应RST
5、客户TCP收到该RST,客户正阻塞与readline调用,导致该调用返回ECONNRESET错误
此时,若检测服务器主机是否崩溃很重要,那么需要采取其他技术(套接字选项等)
服务器主机关机
Unix系统关机时候,init进程先给所有进程发送SIGTERM信号(默认终止进程),等待一段时间,给所有仍在运行的进程发送SIGKILL信号