本章使用一个完整的TCP客户-服务器程序示例,实现服务器回射程序,讨论了重要的边界场景。在此把重要知识点记录。
正常情形
1. 服务器启动后,依次调用socket, bind, listen, accept, 然后阻塞于accept。此时可以使用netstat -a命令来查看套接字状态——此时正处于listen状态。
2. 客户调用socket和connect, 后者引起TCP的三路握手过程,三次握手完成后,客户中的connect和服务中的accept均返回,建立连接。
3. 客户调用str_cli函数,阻塞于fgets调用,服务中的accept返回时,服务器调用fork产生子进程,子进程拥有已完成连接的套接字,调用str_echo,此函数在readline中调用read,阻塞等待客户端的数据。
4. 在客户端完成连接而未输入数据的时候,有三个进程都处在阻塞状态:客户端等待用户输入,服务端进程下一次等待accept返回,服务子进程等待客户数据到达。此时可以再次使用nestat -a来查看各个套接字状态。客户进程与服务子进程都是连接状态,而服务主进程是listen状态。
5. 当客户在正常连接情况下使用ctrl+d来终止客户端进程,此时立即使用nestat -a查看到客户端是处于TIME_WAIT状态。服务端在read阻塞等待时如果收到FIN,会返回0?后续详述。
6. 服务子进程退出时候向父进程发送SIG_CHLD信号,然后成为僵死进程。
关于信号的问题:
1. 信号早谁发出?答:可以是一个进程发给另一进程(或自身),也可以是内核发给某个进程。
2. 可以处理信号?答:可提供一个函数来处理指定信号;可设置SIG_IGN来忽略指定信号;可设置SIG_DFL来启用默认处理,很多时候默认处理是终止进程。
3. 哪两个信号不能捕捉,也不能忽略?答:SIGKILL和SIGSTOP。为什么?
4. signal 函数在旧的设计上是有丢失信号的情形的,但是由于历史原因,很多目前版本依然保留这个名称,但改由可靠的sigaction函数来实现signal;有兴趣的可以看早期signal的实现,到底哪里有缺陷导致信号丢失,sigaction是怎么解决这个问题的。加深理解。
5. 设置处理函数的信号掩码可以使指定信号在信号处理函数被调用时阻塞——此时被阻塞的信号都不能递交给进程。
6. 一旦安装了信号处理函数,它便一直安装着;
7. 在一个信号处理函数运行期间,正被递交的信号是阻塞的,并且安装处理函数时在传递给sigaction函数的sa_mask信号集中指定的任何额外信号也被阻塞。这里的阻塞是对应的信号在阻塞期间不会被处理。
8. 如果一个信号在被阻塞期间产生了一次或多次,那么该信号被解阻塞之后通常只递交一次,也就是说Unix信号默认是不排除的。——然而,POSIX1003.1b定义了一些排除可靠信号,这是上面使用了“通常”和“默认”来修饰的原因。
9. 可以使用sigprocmask函数选择性地阻塞或解阻塞一组信号。
处理僵死进程:
1. 为正确处理僵死的子进程,父进程应该安装SIGCHLD信号的处理函数。通常情况,这个处理函数应该做一些什么事情?
2. 若父进程先于子进程终止,那么子进程终止时,会有什么样的行为?
关于慢系统调用:
1. 慢系统调用是指那些可能永远阻塞的系统调用,多数网络支持函数都属于这一类。其他慢系统调用的例子是对管道和终端设备的读和写。
2. 慢系统调用的基本规则:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。
3. 程序员为了可移植性,需要对慢系统调用返回的EINTR有所准备,书上修改处理accept部分代码,以处理遇到EINTR导致慢系统调用返回时自动重启此系统调用。
4. 自动重启的处理方式适用大多数慢系统调用——如accept, read, write, select, open等。
5. 自动重启的处理方式不适用于connect函数——如果connetc函数返回EINTR,我们就不能再次调用它了。
wait和waitpid函数的区别
1. 两个函数都用来处理已终止的进程,返回已经终止的子进程的pid。
2. 当需要等待多个子进程终止时,应该使用waitpid以同时等待多个子进程的任一个发生
下面讨论非正常情况:
服务器accept返回前连接中止:
1. 这种情况一般只会在服务特别繁忙的时候出现——客户发出的tcp连接成功,服务端已经把此连接放入已完成连接队列,但是accept还没有来得及取这个连接(或许此时前面还有很多已完成连接等待处理);然而客户端此时主动取消连接——发送RST分节。可在本地测试机器通过延时的技巧来模拟这种情况。
2. 对这种情况的处理在各种系统上有不同的实现——BSD是完全在内核中处理中止连接,把未被处理的又收到了RST的已完成连接项删除,accept根本不知道该请求已经被删除。——然而大多数SVR4实现返回错误给服务器进程,作为accept的结果。
服务器进程终止:
1. 服务器子进程被kill(或者进程崩溃),在进程终止处理过程中,会把所有打开着的描述符关闭。关闭对应套接字的时候会向客户发送一个FIN,而客户TCP则响应以一个ACK。
2. 客户端使用readline的阻塞方式等待服务端的回射消息,而此时FIN到达套接字。客户实际上在应对两个描述符——套接字和用户输入,它不能单纯在这两个源中某个特定源输入上,而是应该阻塞在其中任何一个源输入上。
3. 为解决应该阻塞在多个源输入的任何一个上的问题,可以使用select或poll函数。
4. 如果在服务器进程终止后,客户端继续发送数据,则因为服务端先前打开的那个连接已经关闭并且进程终止,服务器会发送RST分节。要是客户不理会readline返回的错误,继续向一个已经收到了RST的套接字写数据,内核会向该进程发送SIGPIPE信号,并在写操作函数返回EPIPE错误。
5. SIGPIPE信号的默认处理动作是终止进程,所以如果不想被默认处理,进程必须捕获该信号。
6. 注意,客户端对于已经接收了FIN的套接字写操作不成问题;对一个已经终止服务端进程的套接字写操作引发服务器发送RST,而客户端对已经收到RST的套接字写操作将引发SIGPIPE信号,是一个错误。
服务器主机崩溃:(注意区别上面的服务器进程终止)
1. 假设服务器主机崩溃——非正常关机,那么服务器端不发送任何东西。TCP客户端发送数据以后收不到ACK,就会按一定算法重传数据,直到超时后放弃传送。阻塞在readline上的客户将在超时的时候返回一个TIMEOUT错误;但若在传送的某个路由上判定服务器已经不可达,则返回的是EHOSTUNREACH或ENETUNREACH。
2. 如果想不主动向服务端发送数据也能检测出服务器主机崩溃,则需要使用SO_KEEPALIVE套接字选项。
3. 如果不使用SO_KEEPALIVE选项的服务器主机崩溃后重启,服务端收到客户端正常送来的数据,会响应一个RST分节。
服务器主机关机:
1. Init进程通常先给所有进程发送SIGTERM信号,等待固定时间(往往5-20秒),然后给所有仍在运行的进程发送SIGKILL信号。
2. 如果我们不捕捉SIGTERM信号并终止,我们的服务器将由SIGKILL信号终止,终止过程和上述的进程终止过程相同。