分类: 系统运维
2008-12-04 12:02:34
网络编程学习笔记 2
--异常情况
一. 处理SIGCHLD信号:
设置僵尸状态进程的目的就是维护子进程的状态信息,以便父进程在以后某个时刻取回。这些进程信息包括:子进程id,终止状态,以及子进程的资源利用信息(cpu利用时间,内存等等)。如果一个进程终止,且该进程具有僵尸状态的子进程,则所有僵尸状态的子进程的ID设为1,init进程负责清除他们(init进程将wait它们)。
处理僵尸进程的一个办法就是:在程序中捕SIGCLHLD信号,在信号处理程序中调用wait。
有一种情况:当父进程阻塞于慢系统调用accept时,如果此时子进程退出,引发SIGCHLD信号,那么,父进程会调用信号处理函数,等待子进程退出,信号处理函数返回之后,此时内核将使accept调用返回EINTR的错误,如果父进程不处理此错误,那么,有可能父进程退出。所以在编写带有信号处理的网络程序时,我们应该分清被中断的系统调用并且处理他们。
系统对于被中断的系统调用是否执行重启是跟实现相关的,也就是对Posix的SA_RESTART标志的支持。诸如下面的代码在实际的编码中经常出现:
上面这段代码中,我们要做的就是重启被中断的accept调用,这对于accept,read,write,open,select这样的调用是可以的,但是有一个函数例外,就是connect调用。如果connect调用返回EINTR,再调用connect的话,会立即返回错误。当调用的connect调用被一个捕获的信号中断而且不自动重启(tcpv2第466页)时,我们必须调用select来等待连接完成。
二. wait和waitpid函数:
我们使用wait系列函数来处理终止的子进程。函数原型如下:
#include
pid_t wait (int *statloc);
pid_t waitpid (pid_t pid, int *statloc, int options);
Both return: process ID if OK, 0 or–1 on error
这两个函数均涉及两个值,函数的返回值时结束进程的ID,结束状态通过statloc指针返回。
wait函数调用时,wait会阻塞到第一个进程结束。而waitpid函数对等待哪个进程以及是否阻塞给了我们更多的控制。参数pid表示让我们指定想等待的子进程,值-1表示等待第一个结束的子进程(也有其他选项,他们涉及进程组ID),参数options让我们指定附加选项,最常用的选项就是WNOHANG,用来通知内核在没有子进程终止时不要阻塞。
函数wait和waitpid的区别:
在信号处理程序中调用wait函数,并不能杜绝zombie进程的出现,问题在于所有的信号在调用处理程序之前完成,而处理程序又只执行了一次,且UNIX对信号一般是不排队的。更加严重的是,是否排队是不确定的。
正确的办法是调用waitpid而不是wait,如下:
2 void
3 sig_chld(int signo)
4 {
5 pid_t pid;
6 int stat;
7 while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
8 printf("child %d terminated\n", pid);
9 return;
10 }
上面的代码有几个地方需要说明的就是:在循环中调用waitpid,options参数使用WNOHANG。
在进行网络编程时,我们可能会遇到三种情况,如下:
三. accept返回前,连接夭折
四. 服务器进程终止
启动客户-服务器,然后杀死服务器的子进程,模拟服务器进程的崩溃,来观察一下客户端的反映。
Kill服务器子进程的ID,此时,引发服务端发送一个FIN分节,客户的tcp以ACK响应,信号SIGCHLD被发往服务器进程,并且被正确处理,客户端此时阻塞于fgets调用。使用netstat命令查看,获得客户端的status为:CLOSE_WAIT,而服务端的status为:FIN_WAIT_2。此时客户端再次调用write给该connfd发送数据时(这种情况时允许的,因为刚才收到的FIN分节代表服务端关闭连接且不再发送数据过来。),接收到FIN分节的客户tcp并没有通知客户进程,此时当服务端收到数据时会响应RST。客户端再次调用readline函数时,由于上面收到的FIN分节会导致readline返回0,所以客户进程退出。客户进程将不会看到RST。
因为FIN分节到达的较早,所以readline返回0,如果在发送之后停顿一段时间,等待tcp收到RST分节,那么再次调用readline的时候,RST分节就会先于FIN分节,导致readline返回另外一个错误:ECONNRESET(连接被复位)。
上面例子的问题是:客户端有两个描述字,套接口和标准输入,它不应该只阻塞于某个特定输入,而是应该阻塞于任意一个源。这就是select和poll的目的。
五. SIGPIPE信号
如果客户进程不理会readline返回的错误,而继续更多的数据的话,会引发内核对进程发送一个SIGPIPE的信号,此信号的缺省行为就是进程退出,如果不想进程退出,那么必须捕获此信号。进程不论是捕获该信号并从其信号处理程序返回,还是不理会该信号,写操作都会返回错误EPIPE。
处理SIGPIPE信号的建议方法是:先确定该信号发生时,进程想做什么。如果没有特殊的进程需要做的话可以将该信号设置为SIG_IGN,并假设后续的输出操作将捕捉EPIPE错误并终止。如果信号出现时需要采取特殊措施(比如在日志文件中记录),那么就必须捕获该信号,并在信号处理程序中执行所期望的动作。如果有多个套接字,那么不能确定到底时哪个套接字发生该错误,如果需要知道哪个write出错,那么要么必须要不理会该信号,要么信号处理程序返回之后再处理来自write的EPIPE。
六. 服务器主机崩溃
启动客户端之后,先确认一下已经连上,之后断开服务主机,在客户上再键入一行。客户端调用write,客户端tcp会作为一个分节发送给服务器,然后客户端会阻塞在readline调用,等待echo。使用tcpdump会发现,客户端的tcp会重发该分节,以期待对方的ACK,当客户端最终放弃时,会返回给用户一个错误。该错误有几种情况:假设服务端已经崩溃,则不会对客户分节作出响应,这个错误就是ETIMEOUT;但是如果中间服务器判定目的主机不可达,且返回给客户端一个目的主机不可达的icmp错误,那么该错误就是ENETUNSEARCH或者EHOSTUNSEARCH。
有时候我们需要等待的时间短一些,一个方法就是给readline设置一个超时。如果想在给服务端发送数据之前就检测到服务端已崩溃,那么可以使用套接口选项SO_KEEPALIVE。
七. 服务器主机崩溃后重启
假设不使用SO_KEEPALIVE选项,连接建立之后,服务器断网重启,客户端键入一行,此时服务端收到客户端发来的分节,因为服务器已经丢失了之前的连接信息,所以会返回给客户端RST分节。返回RST分节的时候,如果客户端正处于readline调用,那么会导致返回ECONNRESET错误。
八. 服务器主机关机
要区分崩溃和关机的区别,关键就在于是否给进程关闭描述符的时间。
当Unix系统关机时,一般时由init进程给所有进程发信号SIGTERM(此信号能够捕捉),5~20s之后,给还在运行的进程发送SIGKILL信号(此信号不可捕捉)。这就给进程一段时间进行清除和终止。如果我们不捕获SIGTERM,那么进程就由SIGKILL信号终止。当进程终止时,所有打开的描述符关闭,引发FIN分节。之后就如同服务器进程终止。
九. 例子程序小结
双方通讯之前,指定四元组:本地ip,本地端口,远端ip,远端端口。
从客户端的角度来看,远端ip和远端端口必须在客户端connect时指定,而两个本地值则由内核作为connect的一部分来选定。客户也可在调用connect之前通过调用bind来指定其中一个或两个本地值,但这不常用。客户端可以在连接建立之后使用getsockname来获取两个本地值。
服务器的角度来看,本地端口由bind指定,尽管也可以使用bind来指定本地地址,但是一般不会那么做,一般会用全零 (wildcard)的通配地址来指定服务器的ip地址。如果服务器端在一个多网卡(multihomed)的主机上绑定通配地址,那么可以使用getsockname来获得本地ip,两个远端值由accept返回给服务器。服务器可以使用getpeername来获取两个客户端值。
十. 传送的数据格式
改动例子程序,编程在服务器和客户端之间传送数据结构而不是文本串,那么会遇到一下问题:
1. 不同的实现以不同的格式存储二进制数据,最常见的实现是大端和小端。
2. 不同的实现在存储相同的c数据类型时可能不同。典型的32机存储long类型时,使用33bit,而64位机存储long时为64位。
3. 不同的实现给结构打包时的方式也是不同的,有时候取决于机器的对齐方式。
有两个常用办法解决数据格式的问题:
1. 把所有的数值数据作为文本串来传送,当然这要求两台通讯主机以相同的字符集作为基础。
2. 显式定义所支持数据类型的二进制格式(位数,大端或小端),在服务器和客户机之间以此格式传递所有数据。
十一. 小结
我们遇到的第一个问题就是僵尸进程,通过捕捉SIGCHLD信号来处理,接着,信号处理程序调用waitpid,必须调用此函数,而不是原来的wait函数,是因为Unix信号是不排队的,这些引导我们进入了Posix信号处理的一些细节,可以参照APUE的第10章。
遇到的下一个问题是服务器进程终止,客户端不知道。客户的tcp被通知,但是由于客户端进程阻塞于某个调用而为接到通知。以后会使用select和poll调用来解决这个问题,客户端不应该只阻塞于某一个,而是等待所有描述符中准备好的那个。
如果服务器进程崩溃,那么客户端需要向服务器再发送一次数据才能知道,一些应用程序应该尽早检测到,这就引入了套接口选项中的SO_KEEPALIVE。