10.4 基于多任务的并发服务器 P176

This commit is contained in:
riba2534
2019-01-21 12:01:03 +08:00
parent 9d447f635e
commit 34f485a63a
4 changed files with 389 additions and 2 deletions

218
README.md
View File

@@ -1001,7 +1001,7 @@ IP 层只关注一个数据包(数据传输基本单位)的传输过程。
#### 4.2.2 进入等待连接请求状态 #### 4.2.2 进入等待连接请求状态
已经调用了 bind 函数给他要借资分配地址,接下来就是要通过调用 listen 函数进入等待接请求状态。只有调用了 listen 函数,客户端才能进入可发出连接请求的状态。换言之,这时客户端才能调用 connect 函数 已经调用了 bind 函数给套接字分配地址,接下来就是要通过调用 listen 函数进入等待接请求状态。只有调用了 listen 函数,客户端才能进入可发出连接请求的状态。换言之,这时客户端才能调用 connect 函数
```c ```c
#include <sys/socket.h> #include <sys/socket.h>
@@ -2287,7 +2287,7 @@ gcc fork.c -o fork
- 传递参数并调用 exit() 函数 - 传递参数并调用 exit() 函数
- main 函数中执行 return 语句并返回值 - main 函数中执行 return 语句并返回值
**向 exit 函数传递的参数值和 main 函数的 return 语句返回的值都回传递给操作系统。而操作系统不会销毁子进程,知道把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。**也就是说将子进程变成僵尸进程的正是操作系统。既然如此,僵尸进程何时被销毁呢? **向 exit 函数传递的参数值和 main 函数的 return 语句返回的值都回传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。**也就是说将子进程变成僵尸进程的正是操作系统。既然如此,僵尸进程何时被销毁呢?
> 应该向创建子进程册父进程传递子进程的 exit 参数值或 return 语句的返回值。 > 应该向创建子进程册父进程传递子进程的 exit 参数值或 return 语句的返回值。
@@ -2612,6 +2612,220 @@ gcc signal.c -o signal
#### 10.3.3 利用 sigaction 函数进行信号处理 #### 10.3.3 利用 sigaction 函数进行信号处理
前面所学的内容可以防止僵尸进程,还有一个函数,叫做 sigaction 函数,他类似于 signal 函数,而且可以完全代替后者,也更稳定。之所以稳定,是因为:
> signal 函数在 Unix 系列的不同操作系统可能存在区别,但 sigaction 函数完全相同
实际上现在很少用 signal 函数编写程序,他只是为了保持对旧程序的兼容,下面介绍 sigaction 函数,只讲解可以替换 signal 函数的功能。
```c
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
/*
成功时返回 0 ,失败时返回 -1
act: 对于第一个参数的信号处理函数(信号处理器)信息。
oldact: 通过此参数获取之前注册的信号处理函数指针,若不需要则传递 0
*/
```
声明并初始化 sigaction 结构体变量以调用上述函数,该结构体定义如下:
```c
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
};
```
此结构体的成员 sa_handler 保存信号处理的函数指针值地址值。sa_mask 和 sa_flags 的所有位初始化 0 即可。这 2 个成员用于指定信号相关的选项和特性,而我们的目的主要是防止产生僵尸进程,故省略。
下面的示例是关于 sigaction 函数的使用方法。
- [sigaction.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/sigaction.c)
```c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if (sig == SIGALRM)
puts("Time out!");
alarm(2);
}
int main(int argc, char *argv[])
{
int i;
struct sigaction act;
act.sa_handler = timeout; //保存函数指针
sigemptyset(&act.sa_mask); //将 sa_mask 函数的所有位初始化成0
act.sa_flags = 0; //sa_flags 同样初始化成 0
sigaction(SIGALRM, &act, 0); //注册 SIGALRM 信号的处理器。
alarm(2); //2 秒后发生 SIGALRM 信号
for (int i = 0; i < 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
```
编译运行:
```shell
gcc sigaction.c -o sigaction
./sigaction
```
结果:
```
wait...
Time out!
wait...
Time out!
wait...
Time out!
```
可以发现,结果和之前用 signal 函数的结果没有什么区别。以上就是信号处理的相关理论。
#### 10.3.4 利用信号处理技术消灭僵尸进程
下面利用子进程终止时产生 SIGCHLD 信号这一点,来用信号处理来消灭僵尸进程。看以下代码:
- [remove_zomebie.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/remove_zomebie.c)
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void read_childproc(int sig)
{
int status;
pid_t id = waitpid(-1, &status, WNOHANG);
if (WIFEXITED(status))
{
printf("Removed proc id: %d \n", id); //子进程的 pid
printf("Child send: %d \n", WEXITSTATUS(status)); //子进程的返回值
}
}
int main(int argc, char *argv[])
{
pid_t pid;
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, 0);
pid = fork();
if (pid == 0) //子进程执行阶段
{
puts("Hi I'm child process");
sleep(10);
return 12;
}
else //父进程执行阶段
{
printf("Child proc id: %d\n", pid);
pid = fork();
if (pid == 0)
{
puts("Hi! I'm child process");
sleep(10);
exit(24);
}
else
{
int i;
printf("Child proc id: %d \n", pid);
for (i = 0; i < 5; i++)
{
puts("wait");
sleep(5);
}
}
}
return 0;
}
```
编译运行:
```shell
gcc remove_zomebie.c -o zombie
./zombie
```
结果:
```
Child proc id: 11211
Hi I'm child process
Child proc id: 11212
wait
Hi! I'm child process
wait
wait
Removed proc id: 11211
Child send: 12
wait
Removed proc id: 11212
Child send: 24
wait
```
请自习观察结果结果中的每一个空行代表间隔了5 秒,程序是先创建了两个子进程,然后子进程 10 秒之后会返回值,第一个 wait 由于子进程在执行,所以直接被唤醒,然后这两个子进程正在睡 10 秒,所以 5 秒之后第二个 wait 开始执行,又过了 5 秒,两个子进程同时被唤醒。所以剩下的 wait 也被唤醒。
所以在本程序的过程中,当子进程终止时候,会向系统发送一个信号,然后调用我们提前写好的处理函数,在处理函数中使用 waitpid 来处理僵尸进程,获取子进程返回值。
### 10.4 基于多任务的并发服务器
#### 10.4.1 基于进程的并发服务器模型
之前的回声服务器每次只能同事向 1 个客户端提供服务。因此,需要扩展回声服务器,使其可以同时向多个客户端提供服务。下图是基于多进程的回声服务器的模型。
![](https://i.loli.net/2019/01/21/5c453664cde26.png)
从图中可以看出,每当有客户端请求时(连接请求),回声服务器都创建子进程以提供服务。如果请求的客户端有 5 个,则将创建 5 个子进程来提供服务,为了完成这些任务,需要经过如下过程:
- 第一阶段:回声服务器端(父进程)通过调用 accept 函数受理连接请求
- 第二阶段:此时获取的套接字文件描述符创建并传递给子进程
- 第三阶段:进程利用传递来的文件描述符提供服务
#### 10.4.2 实现并发服务器
下面是基于多进程实现的并发的回声服务器的服务端,可以结合第四章的 [echo_client.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch04/echo_client.c) 回声客户端来运行。
- [echo_mpserv.c](https://github.com/riba2534/TCP-IP-NetworkNote/blob/master/ch10/echo_mpserv.c)
编译运行:
```shell
gcc echo_mpserv.c -o eserver
./eserver
```
结果:
## License ## License

88
ch10/echo_mpserv.c Normal file
View File

@@ -0,0 +1,88 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char *message);
void read_childproc(int sig);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
pid_t pid;
struct sigaction act;
socklen_t adr_sz;
int str_len, state;
char buf[BUF_SIZE];
if (argc != 2)
{
printf("Usgae : %s <port>\n", argv[0]);
exit(1);
}
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0); //注册信号处理器,把成功的返回值给 state
serv_sock = socket(PF_INET, SOCK_STREAM, 0); //创建服务端套接字
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) //分配IP地址和端口号
error_handling("bind() error");
if (listen(serv_sock, 5) == -1) //进入等待连接请求状态
error_handling("listen() error");
while (1)
{
adr_sz = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
if (clnt_sock == -1)
continue;
else
puts("new client connected...");
pid = fork();
if (pid == -1)
{
close(clnt_sock);
continue;
}
if (pid == 0) //子进程运行区域
{
close(serv_sock);
while ((str_len = read(clnt_sock, buf, BUFSIZ)) != 0)
write(clnt_sock, buf, str_len);
close(clnt_sock);
puts("client disconnected...");
return 0;
}
else
close(clnt_sock);
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}

56
ch10/remove_zomebie.c Normal file
View File

@@ -0,0 +1,56 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
void read_childproc(int sig)
{
int status;
pid_t id = waitpid(-1, &status, WNOHANG);
if (WIFEXITED(status))
{
printf("Removed proc id: %d \n", id); //子进程的 pid
printf("Child send: %d \n", WEXITSTATUS(status)); //子进程的返回值
}
}
int main(int argc, char *argv[])
{
pid_t pid;
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, 0);
pid = fork();
if (pid == 0) //子进程执行阶段
{
puts("Hi I'm child process");
sleep(10);
return 12;
}
else //父进程执行阶段
{
printf("Child proc id: %d\n", pid);
pid = fork();
if (pid == 0)
{
puts("Hi! I'm child process");
sleep(10);
exit(24);
}
else
{
int i;
printf("Child proc id: %d \n", pid);
for (i = 0; i < 5; i++)
{
puts("wait");
sleep(5);
}
}
}
return 0;
}

29
ch10/sigaction.c Normal file
View File

@@ -0,0 +1,29 @@
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if (sig == SIGALRM)
puts("Time out!");
alarm(2);
}
int main(int argc, char *argv[])
{
int i;
struct sigaction act;
act.sa_handler = timeout; //保存函数指针
sigemptyset(&act.sa_mask); //将 sa_mask 函数的所有位初始化成0
act.sa_flags = 0; //sa_flags 同样初始化成 0
sigaction(SIGALRM, &act, 0); //注册 SIGALRM 信号的处理器。
alarm(2); //2 秒后发生 SIGALRM 信号
for (int i = 0; i < 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}