Chinaunix首页 | 论坛 | 博客
  • 博客访问: 148313
  • 博文数量: 32
  • 博客积分: 2050
  • 博客等级: 大尉
  • 技术积分: 335
  • 用 户 组: 普通用户
  • 注册时间: 2006-08-10 09:43
文章分类

全部博文(32)

文章存档

2012年(1)

2011年(1)

2009年(1)

2008年(5)

2007年(22)

2006年(2)

我的朋友

分类: 系统运维

2008-12-04 12:02:34

网络编程学习笔记 2

--异常情况

一.    处理SIGCHLD信号:

设置僵尸状态进程的目的就是维护子进程的状态信息,以便父进程在以后某个时刻取回。这些进程信息包括:子进程id,终止状态,以及子进程的资源利用信息(cpu利用时间,内存等等)。如果一个进程终止,且该进程具有僵尸状态的子进程,则所有僵尸状态的子进程的ID设为1init进程负责清除他们(init进程将wait它们)。

处理僵尸进程的一个办法就是:在程序中捕SIGCLHLD信号,在信号处理程序中调用wait

 

 

有一种情况:当父进程阻塞于慢系统调用accept时,如果此时子进程退出,引发SIGCHLD信号,那么,父进程会调用信号处理函数,等待子进程退出,信号处理函数返回之后,此时内核将使accept调用返回EINTR的错误,如果父进程不处理此错误,那么,有可能父进程退出。所以在编写带有信号处理的网络程序时,我们应该分清被中断的系统调用并且处理他们。

系统对于被中断的系统调用是否执行重启是跟实现相关的,也就是对PosixSA_RESTART标志的支持。诸如下面的代码在实际的编码中经常出现:

上面这段代码中,我们要做的就是重启被中断的accept调用,这对于acceptreadwriteopenselect这样的调用是可以的,但是有一个函数例外,就是connect调用。如果connect调用返回EINTR,再调用connect的话,会立即返回错误。当调用的connect调用被一个捕获的信号中断而且不自动重启(tcpv2466页)时,我们必须调用select来等待连接完成。

 

 

 

 

 

 

二.    waitwaitpid函数:

我们使用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,用来通知内核在没有子进程终止时不要阻塞。

 

函数waitwaitpid的区别:

在信号处理程序中调用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 }
            上面的代码有几个地方需要说明的就是:在循环中调用waitpidoptions参数使用WNOHANG

在进行网络编程时,我们可能会遇到三种情况,如下:

三.    accept返回前,连接夭折

四.    服务器进程终止

启动客户-服务器,然后杀死服务器的子进程,模拟服务器进程的崩溃,来观察一下客户端的反映。

Kill服务器子进程的ID,此时,引发服务端发送一个FIN分节,客户的tcpACK响应,信号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(连接被复位)。

上面例子的问题是:客户端有两个描述字,套接口和标准输入,它不应该只阻塞于某个特定输入,而是应该阻塞于任意一个源。这就是selectpoll的目的。

五.    SIGPIPE信号

如果客户进程不理会readline返回的错误,而继续更多的数据的话,会引发内核对进程发送一个SIGPIPE的信号,此信号的缺省行为就是进程退出,如果不想进程退出,那么必须捕获此信号。进程不论是捕获该信号并从其信号处理程序返回,还是不理会该信号,写操作都会返回错误EPIPE

处理SIGPIPE信号的建议方法是:先确定该信号发生时,进程想做什么。如果没有特殊的进程需要做的话可以将该信号设置为SIG_IGN,并假设后续的输出操作将捕捉EPIPE错误并终止。如果信号出现时需要采取特殊措施(比如在日志文件中记录),那么就必须捕获该信号,并在信号处理程序中执行所期望的动作。如果有多个套接字,那么不能确定到底时哪个套接字发生该错误,如果需要知道哪个write出错,那么要么必须要不理会该信号,要么信号处理程序返回之后再处理来自writeEPIPE

六.    服务器主机崩溃

启动客户端之后,先确认一下已经连上,之后断开服务主机,在客户上再键入一行。客户端调用write,客户端tcp会作为一个分节发送给服务器,然后客户端会阻塞在readline调用,等待echo。使用tcpdump会发现,客户端的tcp会重发该分节,以期待对方的ACK,当客户端最终放弃时,会返回给用户一个错误。该错误有几种情况:假设服务端已经崩溃,则不会对客户分节作出响应,这个错误就是ETIMEOUT;但是如果中间服务器判定目的主机不可达,且返回给客户端一个目的主机不可达的icmp错误,那么该错误就是ENETUNSEARCH或者EHOSTUNSEARCH

有时候我们需要等待的时间短一些,一个方法就是给readline设置一个超时。如果想在给服务端发送数据之前就检测到服务端已崩溃,那么可以使用套接口选项SO_KEEPALIVE

七.    服务器主机崩溃后重启

假设不使用SO_KEEPALIVE选项,连接建立之后,服务器断网重启,客户端键入一行,此时服务端收到客户端发来的分节,因为服务器已经丢失了之前的连接信息,所以会返回给客户端RST分节。返回RST分节的时候,如果客户端正处于readline调用,那么会导致返回ECONNRESET错误。

八.    服务器主机关机

要区分崩溃和关机的区别,关键就在于是否给进程关闭描述符的时间。

Unix系统关机时,一般时由init进程给所有进程发信号SIGTERM(此信号能够捕捉),520s之后,给还在运行的进程发送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被通知,但是由于客户端进程阻塞于某个调用而为接到通知。以后会使用selectpoll调用来解决这个问题,客户端不应该只阻塞于某一个,而是等待所有描述符中准备好的那个。

如果服务器进程崩溃,那么客户端需要向服务器再发送一次数据才能知道,一些应用程序应该尽早检测到,这就引入了套接口选项中的SO_KEEPALIVE

阅读(1208) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

chinaunix网友2009-03-16 13:39:26

"RST分节就会先于FIN分节",应该说是"RST分节"覆盖"了FIN分节吧。