第五章上篇主要讲述信号处理,其他异常情况,以及传输的数据格式。
1. 信号处理
本篇还会继续使用上篇说到的例子,上篇说到子进程结束会给父进程发送信号SIGCHLD,如果父进程不处理将会导致子进程僵死,可能会耗尽系统的进程资源。使sigaction函数和struct sigaction结构封装signal函数,书中并没有详细说明sigaction结构及函数,下面代码会给出详细结构和函数声明
为什么信号处理函数用wait不好用?当我们和server建立多个连接,而只用一个连接来传输数据时,还会有终止的子进程变成僵死进程。例如client使用5个socket与server建立连接,server将会有5个子进程与client连接, 然后client只用一个socket来传输数据, 当client进程结束的时候,关闭所有描述符,client的每个连接的socket都会发送FIN分组给server的子进程socket,5个子进程将会几乎同时终止,返回5个SIGCHLD信号,而使用wait的信号处理函数只能处理一个或几个。因为5个信号都在信号处理函数之前产生,而信号处理函数只会执行一次,更严重的是,问题是不确定的。正确方法是使用waitpid而不是使用wait函数。
a. accept 返回前连接中止
连接建立,客户TCP却发送一个RST。在服务器看来,该连接已由TCP排队,等着服务器进程调用accept的时候RST到达(这里本人不懂,书中前面的内容accept调用不是在第二路握手开始的时候,返回在第三次握手结束的时候吗,如有人懂,可评论告知,谢谢)
b. 服务器进程终止
建立连接后,client输入数据,可以回显,此时kill子进程,会怎样?
服务器子进程被kill,关闭打开的连接套接字描述符,将向client发送FIN,而client响应一个ACK,这就是连接终止的前半部分。SIGCHLD信号能够被父进程正确处理,而此时client阻塞在fgets,当用户输入数据,client TCP会把输入数据发送给服务器,可以这么做,因为client接收到FIN,只是表示server已关闭连接的server端,不会再往client发送数据而已,FIN的接收并不会告知server进程终止 。本例中的server的进程确实终止了,server端接收到client的数据,将会返回一个RST。client在writen之后调用readline,readline返回0,出现我们自己打印的错误,服务器过早终止。最后client进程终止,它所有打开的描述符关闭。
c. SIGPIPE信号
当一个进程向某个已收到RST的套接字执行写操作时,内核向进程发送SIGPIPE信号,该信号默认动作是终止进程。
调用两次writen: 第一次将数据第一个字节写入套接字中,睡眠1s,然后再写入剩下的数据。第一次是为了返回RST,第二次writen返回SIGPIPE。
d. 服务器主机崩溃
服务器主机崩溃, 已有的网络连接上不发出任何东西
用户输入数据,client往套接字调用writen,并阻塞在readline,等待应答,客户TCP会持续重传数据分节,直到超时放弃重传。有时候我们希望更快的检测这种情况,所用方法就是给readline设置个超时。
e. 服务器主机崩溃后重启
当服务器主机崩溃重启时,它的TCP丢失了崩溃前的所有连接信息,因此会响应一个RST。当client接收到RST,此时它正阻塞到readline调用,导致该调用返ECONNRESET错误。我们需要采用某种技术,SO_KEEPALIVE 套接字选项,即使client不主动发送数据也能检出这种情况
f. 服务器主机关机
使用二进制格式进行传输会有潜在问题:不同实现以不同的格式存储二进制数,大小端;存储的相同的C数据类型可能有差异,32位,64位系统;结构打包方式不同,字节对齐问题。
有两个常用方法:将所有数值数据作为文本串传递;显示定义二进制格式,并以这样的格式在client和server传递所有数据。
1. 信号处理
本篇还会继续使用上篇说到的例子,上篇说到子进程结束会给父进程发送信号SIGCHLD,如果父进程不处理将会导致子进程僵死,可能会耗尽系统的进程资源。使sigaction函数和struct sigaction结构封装signal函数,书中并没有详细说明sigaction结构及函数,下面代码会给出详细结构和函数声明
struct sigaction{
void (*sa_handler)(int); //处理函数的函数指针
sigset_t sa_mask; //信号掩码
int sa_flags; //标志符
void (*sa_sigaction)(int, siginfo_t *, void*);
};
//第一个参数是信号
//第二个参数是处理该信号的sigaction结构指针
//第三个参数是原处理该信号的sigaction结构指针
//成功则返回0, 失败返回<0
int sigaction(int, struct sigaction *act, struct sigaction *oact);
sigaction是POSIX的接口, 大部分情况下我们都应该使用该接口,以下是signal函数的封装.
typedef void (*SigFunc)(int);
SigFunc signal(int signo, SigFunc func)
{
struct sigaction act, oact;
act.sa_handler = func; //设置处理函数
sigemptyset(&act.sa_mask); //设置处理函数的信号掩码
act.sa_flags = 0; //设置标志
if(signo == SIGALRM){
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; //由该信号中断的系统调用不会重启
#endif
}
else{
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; //由该信号中断的系统调用不会重启
#endif
}
if(sigaction(signo,&act, &oact) < 0) //调用信号处理sigaction
return SIG_ERR;
return oact.sa_handler; //返回原有的信号处理函数
}
再定义信号处理函数sig_chld, 我们可以在服务器代码中加入signal(SIGCHLD, sig_chld),用来捕获SIGCHLD信号清理已终止的子进程。
void sig_chld(int signo)
{
pid_t pid;
int stat;
//wait不能很好处理
//pid = wait(&stat);
while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
//在信号处理函数中调用printf等标准I/O函数是不合适的,只是为了查看子进程id
printf("child %d terminated\n", pid);
return;
}
当SIGCHLD信号递交时,通过上篇可知道父进程阻塞于accept调用,而当阻塞于慢系统调用例如accept捕获到信号,且信号函数返回时该系统调用将会返回EINTR错误。我们必须处理这个错误。
for(;;){
if((connfd = accept(listenfd, NULL, NULL)) < 0){
if(errno == EINTR) continue; //处理被中断的系统调用
else err_sys("accept error");
}
....
}
接着讲解wait函数和waitpid函数的原型, 它两的区别,以及为什么上面的代码只使用wait不能很好的处理已终止的子进程。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
//均返回:成功子进程id,失败为0或-1
wait和waitpid都可以通过statloc指针返回子进程终止状态(整数)。如果调用wait, 没有子进程终止但有一个或多个子进程在执行,那么wait会一直阻塞到现有子进程第一个终止为止;而waitpid可以就等待的进程以及是否阻塞给与我们更多的控制,pid参数允许我们指定等待的进程ID,-1表示等待第一个终止的进程(还可以指定进程组)。options参数指定附加的选项, 最常用的是WNOHANG,表示没有已终止的进程的时候不要阻塞。
为什么信号处理函数用wait不好用?当我们和server建立多个连接,而只用一个连接来传输数据时,还会有终止的子进程变成僵死进程。例如client使用5个socket与server建立连接,server将会有5个子进程与client连接, 然后client只用一个socket来传输数据, 当client进程结束的时候,关闭所有描述符,client的每个连接的socket都会发送FIN分组给server的子进程socket,5个子进程将会几乎同时终止,返回5个SIGCHLD信号,而使用wait的信号处理函数只能处理一个或几个。因为5个信号都在信号处理函数之前产生,而信号处理函数只会执行一次,更严重的是,问题是不确定的。正确方法是使用waitpid而不是使用wait函数。
目前在网络编程会遇到3个问题:1. 当fork子进程,子进程终止必须捕获SIGCHLD信号;2. 当捕获信号的时候,必须处理被中断的系统调用;3. SIGCHLD信号处理函数必须正确编写。
a. accept 返回前连接中止
连接建立,客户TCP却发送一个RST。在服务器看来,该连接已由TCP排队,等着服务器进程调用accept的时候RST到达(这里本人不懂,书中前面的内容accept调用不是在第二路握手开始的时候,返回在第三次握手结束的时候吗,如有人懂,可评论告知,谢谢)
b. 服务器进程终止
建立连接后,client输入数据,可以回显,此时kill子进程,会怎样?
服务器子进程被kill,关闭打开的连接套接字描述符,将向client发送FIN,而client响应一个ACK,这就是连接终止的前半部分。SIGCHLD信号能够被父进程正确处理,而此时client阻塞在fgets,当用户输入数据,client TCP会把输入数据发送给服务器,可以这么做,因为client接收到FIN,只是表示server已关闭连接的server端,不会再往client发送数据而已,FIN的接收并不会告知server进程终止 。本例中的server的进程确实终止了,server端接收到client的数据,将会返回一个RST。client在writen之后调用readline,readline返回0,出现我们自己打印的错误,服务器过早终止。最后client进程终止,它所有打开的描述符关闭。
c. SIGPIPE信号
当一个进程向某个已收到RST的套接字执行写操作时,内核向进程发送SIGPIPE信号,该信号默认动作是终止进程。
调用两次writen: 第一次将数据第一个字节写入套接字中,睡眠1s,然后再写入剩下的数据。第一次是为了返回RST,第二次writen返回SIGPIPE。
d. 服务器主机崩溃
服务器主机崩溃, 已有的网络连接上不发出任何东西
用户输入数据,client往套接字调用writen,并阻塞在readline,等待应答,客户TCP会持续重传数据分节,直到超时放弃重传。有时候我们希望更快的检测这种情况,所用方法就是给readline设置个超时。
e. 服务器主机崩溃后重启
当服务器主机崩溃重启时,它的TCP丢失了崩溃前的所有连接信息,因此会响应一个RST。当client接收到RST,此时它正阻塞到readline调用,导致该调用返ECONNRESET错误。我们需要采用某种技术,SO_KEEPALIVE 套接字选项,即使client不主动发送数据也能检出这种情况
f. 服务器主机关机
关机时,init进程会给所有进程发送SIGTERM信号,一段时间后,给仍在运行的进程发送SIGKILL信号。当服务器子进程终止时,它所有的打开的描述符都将关闭(和情形b一致)。使用select/poll函数,服务器进程终止这种情况一经发生就能检测。
使用二进制格式进行传输会有潜在问题:不同实现以不同的格式存储二进制数,大小端;存储的相同的C数据类型可能有差异,32位,64位系统;结构打包方式不同,字节对齐问题。
有两个常用方法:将所有数值数据作为文本串传递;显示定义二进制格式,并以这样的格式在client和server传递所有数据。