代码分析
代码如下:
回显服务端
1 |
|
回显客户端
1 |
|
首先运行service, 此时服务端会出去监听状态。
然后运行client,此时客户端调用 connect 与服务端进行三次握手,建立连接之后客户端的状态会变为ESTABLISHED态。服务端accept某来自某个客户端的连接之后,会fork一个子进程,在这个子进程中去处理客户端发送过来的数据,而本身则继续去监听有没有其他连接到来。
所以此时客户端和服务器的状态分别是
- 客户端 ESTABLISHED
- 服务端 LISTEN
- 服务端fork的子进程 ESTABLISHED
用ps命令查看一下进程的关系。
图上可以看到 105137 的进程为服务端进程,此时正处于accept状态。
105140为105137的子进程,也就是服务端fork出来的子进程,此时处于wait状态,它此时正阻塞在read函数上
105139为客户端进程,此时阻塞在fgets上等待用户输入
现在来讨论几种情况
正常终止
在客户端随便输入几行数据,服务端都成功的将数据回传给客户端,客户端将服务端传回来的数据显示出来。然后,在末尾键入EOF结束符(CTRL+D),结束客户端。
此时手速一定要快,在终端输入命令 netstat -a | grep 45678 观察服务端和客户端的状态。
客户端在收到EOF之后, my_cli函数中的fgets返回NULL,循环终止。因为我们没有主动调用close关闭socket,在进程结束之后内核帮我们主动回收打开的文件描述符(也就是将内核中维护的文件描述符的饮用计数减1),当这个文件描述符的引用计数为0时,内核将其close。调用close时,会向服务端发送一份FIN包,此时客户端的连接状态也就变成了TIME_WAIT
说完了客户端之后再来说服务端,服务端进程一直处于listen状态毋庸置疑,重点要说的是服务端fork出的子进程。
这个子进程一直执行的代码如下1
2
3
4
5
6
7
8
9
10
11
12
13close(listen_sock);
my_echo(clientfd);
exit(0);
void my_echo(int connectfd){
ssize_t n;
char buf[MAXLINE] = {0};
while ((n = read(connectfd, buf, MAXLINE)) > 0) {
write(connectfd, buf, n);
memset(buf, 0, MAXLINE);
if (n < 0 && errno == EINTR)
continue;
}
子进程在收到客户端发送过来的FIN包后,进入四次挥手状态,当他们断开连接之后,read函数返回-1,循环结束,此时子进程调用exit结束进程。这个子进程虽然结束了,但其并没有彻底的消亡。在它的pcb中还存在着一份数据要交给他的父进程。这就是Linux中著名的僵尸进程。关于僵尸进程更深入的内容,可以参考我的博客。此时我们观察一下进程的状态来验证之前所说
很明显可以看到,105140号进程变为了 Z状态
当不断的有客户端连接和退出的时候,僵尸进程就会慢慢的占满内核空间,导致服务端再也无法创建进程,这个问题是必须要被解决的。
信号
信号的概念就是告知某个进程发生了某件事情,它的本质是软件中断。比如在子进程终止的时候,子进程会给父进程发送SIGCHLD信号,父进程只需要在收到该信号的时候wait子进程即可解决僵尸进程的问题。
信号通常是异步发生的。它可以由一个进程发给另一个进程,也可以由内核发给进程。接下来讲讲使用信号的基本函数。
sigaction
1 | int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact); |
先看一下结构体 sigaction
sa_handler 为信号处理函数
sa_mask 表示在当前信号处理函数被调用时,需要被阻塞的一组信号。
sa_flag 一个选项,有两个值比较常用
- SA_NODEFER:信号处理函数正在进行时,不堵塞对于信号处理函数自身信号功能
- SA_RESETHAND:当用户注册的信号处理函数被执行过一次后,该信号的处理函数被设为系统默认的处理函数。
sigaction 函数简单的理解就是在某个信号上绑定一个函数,当该信号到来时,就会执行我们自定义的处理函数中去。
处理僵尸进程
接下来就使用sigaction来捕获子进程的SIGCHLD信号,防止子进程变成僵尸进程。
信号处理函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23typedef void signfun(int);
void sig_child(int signo);
signfun* sigaction_child(int signo, signfun *func);
void sig_child(int signo)
{
int stat, pid;
pid = wait(&stat);
}
signfun* sigaction_child(int signo, signfun *func)
{
struct sigaction act, oldact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (sigaction(signo, &act, &oldact) < 0) {
return SIG_ERR;
}
return oldact.sa_handler;
}
完整代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
static const int MAXLINE = 4096;
static const int MAXLISTEN = 1024;
static const int PORT = 45678;
typedef void signfun(int);
int e_socket(int domain, int type, int protocol);
int e_bind(int sockfd, sockaddr_in* myaddr, socklen_t addrlen);
int e_listen(int sockfd, int backlog);
int e_accept(int sockfd, sockaddr_in* myaddr, socklen_t *addrlen);
void my_echo(int connectfd);
void sig_child(int signo);
signfun* sigaction_child(int signo, signfun *func);
int e_socket(int domain, int type, int protocol){
int sockfd = socket(domain, type, protocol);
if (sockfd < 0)
{
exit(0);
}
return sockfd;
}
int e_bind(int sockfd, sockaddr_in* myaddr, socklen_t addrlen){
int ret = bind(sockfd, (sockaddr*)myaddr, addrlen);
if (ret != 0){
close(sockfd);
exit(0);
}
return ret;
}
int e_listen(int sockfd, int backlog){
int ret = listen(sockfd, backlog);
if (ret != 0) {
close(sockfd);
exit(0);
}
return ret;
}
int e_accept(int sockfd, sockaddr_in* myaddr, socklen_t *addrlen){
int ret = accept(sockfd, (sockaddr*)myaddr, addrlen);
if (ret == -1)
{
printf("%d", errno);
}
return ret;
}
void my_echo(int connectfd){
ssize_t n;
char buf[MAXLINE] = {0};
while ((n = read(connectfd, buf, MAXLINE)) > 0) {
write(connectfd, buf, n);
memset(buf, 0, MAXLINE);
if (n < 0 && errno == EINTR)
continue;
}
}
void sig_child(int signo)
{
int stat, pid;
pid = wait(&stat);
}
signfun* sigaction_child(int signo, signfun *func)
{
struct sigaction act, oldact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (sigaction(signo, &act, &oldact) < 0) {
return SIG_ERR;
}
return oldact.sa_handler;
}
int main(int argc, const char * argv[]) {
int listen_sock = e_socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in servaddr, clientaddr;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
e_bind(listen_sock, &servaddr, sizeof(servaddr));
e_listen(listen_sock, MAXLISTEN);
sigaction_child(SIGCHLD, sig_child);
socklen_t clientlen = 0;
int clientfd = -1;
while (true) {
clientlen = sizeof(clientaddr);
clientfd = accept(listen_sock, (sockaddr*)&clientaddr, &clientlen);
if (clientfd < 0) {
if (errno == EINTR) {
continue;
}
exit(0);
}
if (fork() == 0) {
close(listen_sock);
my_echo(clientfd);
exit(0);
}
close(clientfd);
}
return 0;
}
重新编译后执行结果如下,已经没有了Z进程
被中断的系统调用
前面我们解决了fork子进程引发的僵尸进程问题。但是也引发了一些新的问题。我们再来理一下执行流程
服务端
- 服务端首先打开soccet,然后accept阻塞等待客户端的连接
- 当客户端的连接到来之后,服务端fork子进程,用这个子进程与客户端进行通信,而本身任然阻塞在accept上继续等待其他客户端的连接到来
- 当某个客户端退出之后,服务端收到了SIGCLID 信号,调用该信号对应的处理程序,也就是调用wait。
客户端
- 打开socket
- connect
- 通信
- 断开连接
问题非常明显,就发生在服务端的第三步上。服务端进程本身是阻塞在accept上的,而当它接收到子进程退出的SIGCLID信号之后,触发了一个新的系统调用,那么服务端此时就会从原本的accept系统调用中返回,去执行新触发的系统调用。
此时accept函数会返回-1,并将errno置为4(EINTR, 被中断的系统调用)。如果我们的父进程没有处理这个错误,就会终止执行,在这种情况下,我们的服务端进程要忽略这个错误
1 | while (true) { |