unix网络编程笔记(三)

第五章笔记

1. 僵死进程问题:

按照第四章9.5典型的并发服务器程序轮廓编写代码,如果父进程fork子进程,但没有wait,当子进程退出后会出现僵死进程。

1.1 定义:

一个进程使用fork创建子进程,如果子进程先于父进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程

1.2 出现原因:

unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息,就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括进程号,退出状态,运行时间等)。直到父进程通过wait、waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait 、waitpid的话, 那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

1.3 查看僵死进程的方法:

ps -A -o stat,ppid,pid,cmd | grep -e ‘^[Zz]’ ;其中-e表示用正则表达式匹配,^表示文本开始位置。 对于cmd显示,有的系统对僵死进程用表示

1.4 解决办法:

(1)子进程退出时会向父进程(getppid的那个进程)发出SICHLD信号。父进程需要捕获SIGCHLD信号,并调用waitpid
(2)注意wait和waitpid都可以获取并释放子进程的pid和终止状态,但必须调用waitpid。
原因:当一个进程有多个子进程时,使用wait会有问题。比如:一个进程fork了多个子进程,当多个子进程同时退出时(比如一个客户端向服务器建立了多个连接,这样服务器就fork了多个进程来处理连接。当客户端exit时,多个连接同时关闭,那么服务器的多个子进程就几乎同时退出),这时会同时有多个SIGCHLD信号产生。但因为Unix信号一般是不排队的,这时虽然产生了多个SIGCHLD但只会调用一次信号处理函数,而wait的特点是即使一个进程有多个子进程,但只要有一个子进程终止,wait就返回,这样会导致只能释放掉一个子进程的退出状态等信息。
(3)而waitpid能解决该问题的原因是:waitpid有一个标记位WNOHANG,它表示即使内核没有已终止的子进程也不要阻塞,这样我们可以在一个循环内调用waitpid,直到返回错误,这样我们可以获取所有已终止子进程的状态。而由于wait没有提供非阻塞标记,我们不能循环调用wait,这样如果对正在运行的子进程调用wait就会发生阻塞。

1. 5 fork并发服务器代码:

#include "unp.h"
void str_echo(int connfd);
void sig_child(int signo)
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        printf("CMD PORT bind_backlog\n");
        exit(0);
    }
    int listenfd = Socket(AF_INET,SOCK_STREAM,0);
    struct sockaddr_in servaddr;
    memset(&servaddr,0,sizeof(struct sockaddr_in)); //套接字地址使用前最好先清零
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  //指定本地地址直接使用INADDR_ANY,不要直接写自己的IP地址,否则不灵活
    servaddr.sin_port = htons(atoi(argv[1]));
    Bind(listenfd,(const struct sockaddr*)&servaddr,sizeof(struct sockaddr_in));
    Listen(listenfd,atoi(argv[2]));
    Signal(SIGCHLD,sig_chld);  //捕获SIGCHLD信号处理僵死进程问题
    for(;;)
    {
        int connfd = Accept(listenfd,NULL,NULL);   //包装函数考虑被信号中断EINTR和ECONNABORTED的情况
        pid_t pid;
        if( (pid = fork()) == 0)
        {
            close(listenfd);         //因为exit(0)使进程退出,进程退出前会自动关闭所有文件描述符,这里可close也可不close 
            str_echo(connfd);
            close(connfd);
            exit(0);                //子进程不要忘记调用exit(0)
        }
        else if(pid > 0)
        {
            close(connfd);          //父进程不要忘记close connfd
        }
        else
        {
            err_sys("fork");
        }
    }
}
void str_echo(int connfd)
{
    char buf[MAXLINE] = {0};
    ssize_t n;
    while(1)
    {
        n = read(connfd,buf,MAXLINE);  //read的四种处理情况:被信号中断 正常读取数据 对方close 其他错误
        if(n < 0 && errno == EINTR)
            continue;
        else if(n > 0)
        {
            writen(connfd,buf,n);
        }
        else if(n == 0)
        {
            break;
        }
        else
        {
            err_sys("read");
        }
    }
}
 void sig_child(int signo)
{
     pid_t        pid;
     int        stat;
     //处理僵尸进程
     while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
            printf("child %d terminated.\n", pid);
}

2. 由于信号导致的EINTR错误

2.1 信号基本知识:

(1)信号可以由一个进程发给另一个进程(或自身),或由内核发给某个进程。
(2)对于信号有三种处理方法:设置信号捕获函数(捕获)、忽略、 默认行为。其中信号SIGKILL SIGSTOP不能捕获,不能够忽略,所以不能作为sigaction或signal的参数。一般信号的默认处理是终止进程,个别信号的默认处理是忽略,比如SIGCHLD
(3)信号捕获函数:sigactionsignal
之所以有两个信号处理函数是因为signal出现的时间早于POSIX,没有形成标准,不用的实现有不同的信号语义。而POSIX明确规定了调用sigaction时的信号语义。
而因为signal参数简单调用比较方便,所以为了便于调用,实现一个自己的signal函数,内部通过调用sigaction来实现,代码见lib/signal.c

sigaction:int sigaction(int signum,const struct sigaction* act,struct sigaction* oldact);
 struct sigaction {
               void     (*sa_handler)(int);  //信号处理函数
               void     (*sa_sigaction)(int, siginfo_t *, void *);  //可以设置信号处理函数,与sa_handler同时只能使用一个,一般用上面那个
               sigset_t   sa_mask;   //设置屏蔽信号,注意不要设置自己,自己肯定是阻塞的
               int        sa_flags;  //设置一些标记,比如SA_RESTART用于设置系统调用自动重启
               void     (*sa_restorer)(void);  //不用
           };

(4)一旦使用sigaction安装了信号处理函数,它便一直安装着。
(5)在一个信号处理函数运行期间,正被递交(它本身)的信号是阻塞的。而且信号处理函数运行时,之前传递给给sigaction的sa_mask信号也被阻塞。如果在执行信号处理函数期间,被阻塞的信号和自身信号又产生了一次或多次,那么该信号被解阻塞之后通常只递交一次,也就是说Unix信号默认是不排队的。

2.2 EINTR错误:

(1)慢系统调用:是指那些可能永远无法返回的系统调用,比如accept如果没有客户端连接会永远阻塞。其他例子比如:对网络的读。但对于磁盘IO的读写不是慢系统调用,因为除非出现系统硬件故障,磁盘IO总会返回。
(2)适用于慢系统调用的基本规则是:对阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回EINTR错误
(3)注意:由于系统对POSIX的SA_RESTART标志的支持是可选的,所以某个系统即使支持SA_RESTART标记,也并非所有被中断的系统调用都可以自动重启,所以大多数源于Berkeley的实现从不自动重启select,其中有些实现从不重启accept和recvfrom。所以我们必须对慢系统调用返回EINTR有所准备。
(4)注意:connect这个函数即使返回EINTR,我们也不能再次调用它,否则会立即返回一个错误。所以出现EINTR可以采取和其他错误同样的处理方式,比如输出错误退出进程。

3. 服务器和客户端正常连接或发生异常情况的通信过程

3.1 查看套接字和进程状态的命令:

(1)netstat -tapn:可以帮我们查看tcp套接字的状态。其中t表示tcp端口;a表示tcp所有端口包括listen 已连接 cose状态;n表示显示IP和端口号的数字形式;p表示显示进程pid和进程名
(2)netstat -tlpn:查看正常监听的tcp套接字状态
(3)ps -o pid,ppid,tty,stat,args,wchan:查看正在当前终端运行的进程的pid ppid等信息。其中如果进程处于睡眠状态WCHAN会列出进程阻塞在的内核哪个函数上。stat表示显示进程的状态。具体进程处于睡眠状态,Stat为S,僵死状态为Z。使用-t 可以查看正在其它终端运行的进程的信息。比如:ps -t pst/6 -o pid,ppid,tty,stat,args,wchan表示查看在pts/6终端打开的进程的状态。想知道当前终端的终端号可以运行ps -o tty查看。

3.2 服务器和客户端建立连接:

在我们的fork服务器中,如果有一个客户端与之建立联系,那么使用netstat -tapn会看到三个套接字的状态,一个是客户端,为ESTABLISHED状态,两个是服务器,一个是LISTEN一个是ESTABLISHED。对于LISTEN套接字IP地址显示0.0.0.0:端口

3.3 服务器和客户端正常结束过程:

当客户端调用close时,导致客户端发送FIN,然后服务器发送ACK回复。当服务器收到FIN时就向进程发送一个EOF(文件结束符),这样服务器进程read会返回0,在程序中这导致服务器调用close,而服务器的close导致服务器发送FIN,之后客户端回复ACK。至此连接完全终止。
四个终止分节发送完后,客户端最后的状态是TIME_WAIT,一般这个状态会保持一定时间才结束。而服务器则彻底关闭,使用netstat看不到连接描述符的终止状态,只剩下哪个监听套接字的状态
注意:不管程序还是服务器,只有程序主动调close或shutdown才会发送FIN

3.4 当服务器进程终止(网络连接正常),而本端不知仍然向对端发送数据:(虽然本端Tcp被告知了,但客户进程由于阻塞在用户输入不能立即得到通知)

3.4.1 通信过程

(1)进程终止处理的部分工作是关闭所有打开的描述符,当服务器进程终止导致发送FIN处于CLOSE_WAIT状态,而本端发送ACK处于FIN_WAIT状态,并read收到一个eof。如果本端这时正阻塞在某处比如终端,不知道对端连接已经终止,之后阻塞解除后向对端发送数据,那对端会发送一个RST
(2)当对端发送了RST后,本端继续向对端进行套接字写操作,那内核会向该进程发送一个SIGPIPE信号。不管该进程是捕获了该信号(安装了信号处理函数)还是简单的忽略,写操作都将返回EPIPE错误

3.4.2 总结:

1)当收到FIN,第一次写操作引发对端发送RST,第二次写引发内核发送SIGPIPE信号,即写一个已接收了FIN的套接字不成问题,但是写一个已接收了RST的套接字则是一个错误;
2)当服务器主机终止,客户端由于阻塞在终端不能立即检测到,需要使用select poll解决这样问题,使得服务器进程终止一旦发生立即能检测到。

3.4.3 注意:

当一个进程向某个已收到RST的套接字执行写操作时,内核向该进程发送一个SIGPIPE信号。EPIPE的默认行为是终止进程。因此进程需要捕获它(编写信号处理函数)以免被终止

3.5 当服务器主机崩溃,对客户端在已有的网络连接上发送不了任何数据,客户端不知仍然向对端发送数据(本端Tcp无法得到通知):

(1)这种情况与(3)的区别是服务器进程终止,会向客户端发送close。但服务器主机崩溃,不可能发送任何网络数据,客户不知服务器已经崩溃。
(2)如果这时客户端向服务器发送数据并read,write会把数据写到内核缓冲区后并返回,之后进程阻塞在read中。而本端Tcp协议栈会向对端发送数据分节并等待ACK,当一段时间收不到ACK后,Tcp会重传数据n次,最终放弃重传并且向客户进程返回一个错误,因为这时进程正阻塞在read上,则read返回错误。(如果是因为主机不能发回响应,返回ETIMEDOUT超时错误,如果是因为路由器判定服务器主机不可达,那么返回EHOSTUNREACH或ENETUNREACH错误。通过这几种错误都要隔几分钟才会返回。
(3)注意当服务器主机崩溃时,只有当我们主动向服务器发送数据才能知道服务器已经崩溃。所以在实际情况下要写心跳包或SO_KEEPALIVE套接字选项来获取对方服务器的状态

3.6 当服务器主机崩溃后又重启(服务端Tcp连接丢失)

当服务器主机崩溃后重启,Tcp会丢失崩溃前的所有连接信息,如果这时客户端向服务器发送数据并read,服务器会响应一个RST。当客户收到RST,客户正阻塞在read,于是read返回ECONNRESET错误

3. 7当服务器主机关机但网络连接正常

当服务器关机,init进程通常先向所有进程发送SIGTERM信号,之后给所有进程发送SIGKILL信号,这会导致进程终止。当服务器进程终止,它所有打开的文件描述符关闭,之后情况和(3)一样。

4. 服务器和客户端传递数据的格式:

(1)在套接字之间传递二进制结构(即直接传递数值、结构体)是不明智的。
(2)原因:不同主机以不同的格式存储二进制数值,比如不同主机的主机字节序不同,不同主机存储相同的C数据类型使用的位数不同,不同的主机对结构体打包的方式也不同(各个简单数据的位数不同,机器对齐也不同)
(3)解决方案:
所有的数值都使用字符串方式传递(字符串是使用char一个字节传递不会出现问题,而前面出现问题主要是因为数据类型会使用多个字节存储,而多个字节的存放方式不同的机器会不同)
显示定义所支持的数据类型的二进制格式(位数 字节序),以明确的方式在客户和服务器之间传递所有数据。远程过程调用通常使用这种技术。

5. 总结

5. 1 解决fork版本服务器的几个问题:

(1)一个进程使用fork创建子进程,如果如果子进程先于父进程退出,而父进程未加处理会导致僵死进程。解决方法fork的子进程终止时,会给父进程发送一个SIGCHLD信号。该信号的默认行为是被忽略。避免产生僵死进程,父进程需要捕获SIGCHLD信号。
(2)对阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回EINTR错误。当捕获信号时,必须处理由于EINTR被中断的系统调用。
(3)SIGCHLD的信号处理函数必须正确编写,应使用waitpid函数以免留下僵死进程
(4)对accept的特殊处理:如果出现EINTR或ECONNABORTED错误时可以忽略继续调用下一次accept(5.11)
(5)对connect的特殊处理:connect这个函数即使返回EINTR,我们也不能再次重启它,否则会立即返回一个错误(5.9)。
(6)在套接字之间传递二进制结构(即直接传递数值、结构体)是不明智的。

5. 2 客户端同时需要读终端和套接字存在的问题:

如果客户端面向两个文件描述符,控制和连接套接字,那么就不能同时收到终端和套接字的数据,如果客户端阻塞在终端,那即使服务器发来了close也不能立即处理。合理的处理方式是:我们可以使用select 或 poll 或epoll 阻塞在其中一个源的输入上,当有任意一个收到了数据的发送就立即接到通知。

5.3 信号知识:

(1)在一个信号处理函数运行期间,正被递交(它本身)的信号是阻塞的。
(2)如果在执行信号处理函数期间,被阻塞的信号和自身信号又产生了一次或多次,那么该信号被解阻塞之后通常只递交一次,也就是说Unix信号默认是不排队的
(3)信号捕获函数:建议使用sigaction
(4)某个系统即使支持SA_RESTART标记,也并非所有被中断的系统调用都可以自动重启

5.4 通信状态:

(1)当服务器收到FIN时就向进程读发送一个EOF,这时进程read就会返回0
(2)不管程序还是服务器,只有程序主动调close或shutdown才会发送FIN
(3)当四个终止分节发送完后,客户端最后的状态是TIME_WAIT,一般这个状态会保持一定时间才结束
(4)当本端收到FIN,第一次写操作引发对端发送RST,第二次写引发内核发送SIGPIPE信号,写操作也会返回EPIPE错误
(5)当服务器和客户端之间无法发送网络数据,客户端无法知道与服务器已经断连,当客户端向服务器发送数据,Tcp会重传n次后给客户端发送错误。即只有当我们主动向服务器发送数据才能与服务器断连,所以在实际情况下要写心跳包或SO_KEEPALIVE套接字选项来获取对方服务器的状态
(6)当服务器主机崩溃后重启,Tcp会丢失崩溃前的所有连接信息,如果这时客户端向服务器发送数据并read,服务器会响应一个RST。当客户收到RST,客户正阻塞在read,于是read返回ECONNRESET错误
(7)对方服务器关机最终导致进程终止,这会导致向客户端发送FIN

5.5 查看套接字和进程状态的命令

netstat -tapn、-tlpn:查看套接字状态
ps -t pst/6 -o pid,ppid,tty,stat,args,wchan:查看进程状态
sudo tcpdump -i xxxx(某个网络接口可使用ifconfig查看):查看网络包

你可能感兴趣的:(unix,网络编程)