简单回射程序概述
- 客户从标准输入读入一行文本,并写给服务器
- 服务器从网络输入读入这行文本,并回射给客户
- 客户从网络输入读入这行回射文本,并显示在标准输出上
TCP回射服务程序
tcpserv01.c
1 |
|
- 50 ~ 55: 这里用到的是系统的read函数,没有对信号中断错误处理,需要自己处理。(收到客户的FIN或EOF将导致read返回)
TCP回射客户端程序
1 |
|
正常启动
先启动服务器
1
2
3
4
5
6
7
8
9//后台启动服务器
./tcpserv01 &
[1] 10186
//我们查看一下端口的状态,用管道过滤,只显示9748端口的信息
netstat -a | grep 9748
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:9748 0.0.0.0:* LISTEN接着直接在本机连接服务器
1
2
3
4
5
6
7
8
9//连接到本机的回环地址
./tcpcli01 127.0.0.1
//再开一个shell,查看当前的端口状态状态
netstat -a | grep 9748
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:9748 0.0.0.0:* LISTEN
tcp 0 0 localhost:9748 localhost:32890 ESTABLISHED
tcp 0 0 localhost:32890 localhost:9748 ESTABLISHED0.0.0.0 ==> 代表通配地址
- * ==> 代表通配端口
- 客户端在收到三路握手的第二个分节的时候,connect函数就返回了。而服务器在收到三次握手的第三个分节的时候accept才返回(之前分析过,服务器在收到第二个分节的时候还处于SYN_RECV状态,只有在收到第三个分节的时候才进入ESTABLISHED状态,此时相应的socket才被扔进监听套接字的已完成队列。而accept只在已完成队列中取socket,所以accept必定是在收到第三个分节之后才返回)
正常终止
- 状态转换图:
- 进程终止:
- 关闭本进程打开的所有的描述符
- 向父进程发送一个SIGCHLD信号
1 | ./tcpcli01 127.0.0.1 |
- 键入EOF后,客户端的fgets返回NULL,导致str_cli结束,最终导致main函数执行到exit而终止
- 进程终止的部分任务是关闭进程打开的所有的描述符,因此客户端打开的套接字由内核关闭,这导致客户TCP向服务器发送一个FIN,服务器回一个ACK。至此,四路挥手的前两步完成。服务器处于CLOSE_WAIT状态,而客户端处于FIN_WAIT_2状态
- 服务器TCP收到FIN后,readline函数返回0,导致str_echo退出,接着main函数执行到exit,进而导致服务端子进程退出。
- 同样的,服务端子进程所打开的所有描述符随之关闭。这导致服务器向客户发送一个FIN,客户回一个ACK,至此,四路挥手结束,连接完全终止。客户套接字进入TIME_WAIT状态。
- 进程终止的另一部分内容是:在服务器子进程终止时,给父进程发送一个SIGCHLD信号。由于我们在代码中没有捕获该信号,而该信号的默认处理为忽略,所以就导致子进程进入僵死状态
1
2
3
4
5
6
7
8
9//我们调用ps命令来验证一下
//因为笔者在测试的时候打开了客户端两次,所以有两个僵死的子进程
ps -a
PID TTY TIME CMD
2720 pts/0 00:00:21 hexo
10186 pts/1 00:00:00 tcpserv01
11146 pts/1 00:00:00 tcpserv01 <defunct> //僵死进程
13389 pts/1 00:00:00 tcpserv01 <defunct> //僵死进程
18996 pts/1 00:00:00 ps
POSIX信号处理
信号(signal)就是告知某个进程发生了某个事件的通知,有时也称为软件中断(software interrupt)。通常是异步的
- 每个信号关联一个处置(deposition),或称行为(action)。在信号发生时执行
类型
- 一个进程发给另一个进程(可以是自身)
- 由内核发给进程
三种处置
自定义信号处理函数,然后用sigaction设置给信号
- SIGKILL和SIGSTOP不能被捕获
1
2//信号处理函数原型
void handler(int signo);
- SIGKILL和SIGSTOP不能被捕获
SIG_IGN ==> 忽略信号
- SIG_DEF ==> 默认处理
signal函数
原型:
1
2
3
4
5
6
7
8
9
10
11
12void (*signal(int signo, void (*func)(int)))(int);
//定义新类型,来化简上面的原型
typedef void Sigfunc(int)
/**
* 为一个信号设置处理函数
* @param signo 信号
* @param func 信号处理函数
* @return 指向信号处理函数
*/
Sigfunc *signal(int signo, Sigfunc *func);POSIX规定设置信号的处置必须调用sigaction,上面的signal是对signation的封装,更容易使用
处理SIGCHLD信号
僵死状态
- 僵死(zombie)状态的目的是维护子进程的信息,以便父进程在以后某个时候获取。这些信息包括子进程的进程ID、终止状态以及资源利用信息。如果一个进程终止,而该进程有子进程处于僵死状态,那么它的所有僵死子进程的父进程ID将被重置为1(init进程)。继承这些子进程的init进程将清理它们
处理僵死进程
1
2
3
4
5
6
7
8
9
10
11//首先定义如下信号处理函数
void sig_chld(int signo){
pid_t pid;
int stat;
pid = wait(&stat);
printf("child %d terminated\n", pid);
return;
}
//在上面的server端程序的Listen之后添加下面这行
Signal(SIGCHLD, sig_child);- 点我查看源码
- 在执行了上面的处理之后,再测试,就观测不到僵死进程了
处理被中断的慢系统调用
- 适用于慢系统调用的基本规则:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误
- 有些系统上会发生,有些系统做了处理,不会发生。但是为了便于移植,还是建议用类似于下面的方法处理这种错误
1
2
3
4
5
6
7
8
9for( ; ; ){
clilen = sizeof(cliaddr);
if( (connfd = accept(listenfd, (SA *) &cliaddr, &clilen)) < 0){
if(errno == EINTR)
continue;
else
err_sys("accept error");
}
}
wait和waitpid函数
1 |
|
- 调用wait函数的时候如果没有已经终止的子进程,不过仍然有一个或多个子进程在执行,那么wait函数将阻塞到其中任意一个子进程终止为止
- waitpid的options可选项如果制定为WNOHANG,则告知内核在没有已终止子进程时不要阻塞
wait函数和waitpid的区别
- waitpid可以指定终止哪个子进程,而wait不能
- waitpid可以实现在没有已终止子进程时不要阻塞,而wait不能
根据上述区别的第二点我们改进之前的信号处理函数
1
2
3
4
5
6
7
8
9
10//由于wait不能实现在没有已终止子进程时不要阻塞,所以在下面的循环中不能调用wait,否则可能会阻塞主线程
//经过下面的修改之后,就可以支持一次调用清理多个进程的要求
void sig_chld(int signo){
pid_t pid;
int stat;
while( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
printf("chihld %d terminated\n", pid);
return;
}具体可以参考课本5.10介绍的同时开五个连接请求的情况