参考
- 《TCP/IP网络编程》
多进程服务器端
具有代表性的并发服务器端实现模型和方法:
- 多进程服务器:通过创建多个进程提供服务
- 多路复用服务器:通过捆绑并同一管理I/O对象提供服务
- 多线程服务器:通过生成与客户端等量的线程提供服务
多进程服务器不适合在Windows平台下
进程
进程的定义:占用内存空间的正在运行的程序。
进程ID
所有进程都会从操作系统分配到ID。此ID称为进程ID,其值为大于2的整数。1要分配给操作系统启动后的(用于协助操作系统)首个进程,因此用户进程无法得到ID值1
fork函数
fork函数将创建调用的进程副本。即并非完全不同的程序创建进程,而是复制正在运行的、调用fork函数的进程。另外,两个进程都将执行fork函数调用后的语句。但因为通过同一进程、复制相同的内存空间,之后的程序需要根据fork函数的返回值加以区分。即利用fork函数的如下特点区分程序执行流程:
- 父进程:fork函数返回子进程ID
- 子进程:fork函数返回0
#include <unistd.h>
pid_t fork(void);
成功时返回进程ID,失败时返回-1
示例
#include <stdio.h>
#include <unistd.h>
int gval = 0;
int main(int argc, char* argv[])
{
pid_t pid;
int lval = 20;
gval++;
lval++;
pid = fork();
if (pid == 0) // 子进程
{
gval += 2;
lval += 2;
}
else // 父进程
{
gval -= 2;
lval -= 2;
}
if (pid == 0)
{
printf("Child Proc: [%d, %d] \n", gval, lval);
}
else
{
printf("Parent Proc: [%d, %d] \n", gval, lval);
}
return 0;
}
僵尸进程
进程完成工作后(执行完main函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作僵尸进程
产生僵尸进程的原因
调用fork函数产生子进程的终止方式:
- 传递参数并调用exit函数
- main函数中执行return语句并返回值
向exit函数传递的参数值和main函数的return语句返回的值都会传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生子进程的父进程。处在这种状态下的进程就是僵尸进程。所以要销毁僵尸进程,应该向创建子进程的父进程传递子进程的exit参数或return语句的返回值
但操作系统不会主动把这些值传递给父进程,只有父进程主动发起请求(函数调用)时,操作系统才会传递该值。而如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态
创建僵尸进程示例
#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
pid_t pid = fork();
if (pid == 0) // 子进程
{
puts("Hi, I am a child process");
}
else // 父进程
{
printf("Child Process ID: %d \n", pid);
sleep(30);
}
if (pid == 0)
{
puts("End child process");
}
else
{
puts("End parent process");
}
return 0;
}
后台处理
后台处理是指将控制台窗口中的指令放到后台运行的方式。如果以如下方式运行上述示例,则程序将在后台运行(&将触发后台处理):
root@my_linux:/tcpip# ./zombie &
如果采用这种方式运行程序,即可在同一控制台输入其他命令,无需另外打开新控制台
销毁僵尸进程方式1:利用wait函数
#include <sys/wait.h>
pid_t wait(int* statloc);
成功时返回终止的子进程ID,失败时返回-1
调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit函数的参数值,main函数的return返回值)将保存到该函数的参数所指内存空间。但函数参数指向的单元中还包含其他信息,因此需要通过下列宏进行分离:
- WIFEXITED:子进程正常终止时返回true
- WEXITSTATUS:返回子进程的返回值
所以,向wait函数传递变量status的地址时,调用wait函数后应编写:
if (WIFEXIED(status)) // 是否正常终止
{
puts("Normal termination!");
printf("Child pass num: %d", WEXITSTATUS(status));
}
wait函数示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
int status;
pid_t pid = fork();
if (pid == 0)
{
return 3;
}
else
{
printf("Child PID: %d \n", pid);
pid = fork();
if (pid == 0)
{
exit(7);
}
else
{
printf("Child PID: %d \n", pid);
wait(&status);
if (WIFEXITED(status))
{
printf("Child send one: %d \n", WEXITSTATUS(status));
}
wait(&status);
if (WIFEXITED(status))
{
printf("Child send two: %d \n", WEXITSTATUS(status));
}
sleep(30);
}
}
return 0;
}
调用wait函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此需谨慎调用该函数
销毁僵尸进程2:使用waitpid函数
调用waitpid函数时,程序不会阻塞
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* statloc, int options);
成功时返回终止的子进程ID(或0),失败时返回-1
(1)pid
等待终止的目标子进程的ID,若传递-1,则与wait函数相同,可以等待任意子进程终止
(2)statloc
与wait函数的statloc参数具有相同含义
(3)options
传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数
waitpid函数示例
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
int status;
pid_t pid = fork();
if (pid == 0)
{
sleep(15);
return 24;
}
else
{
while (!waitpid(-1, &status, WNOHANG))
{
sleep(3);
puts("sleep 3sec.");
}
if (WIFEXITED(status))
{
printf("Child send %d \n", WEXITSTATUS(status));
}
}
return 0;
}
信号与signal函数
父进程不能只调用waitpid函数以等待子进程终止,当子进程结束时通过操作系统通知父进程将更高效。于是引入信号处理(Signal Handling)机制。此处的“信号”是在特定事件发生时由操作系统向进程发生的消息
通过注册信号,当进程发现自己的子进程结束时,请求操作系统调用特定函数。这一功能可以通过signal函数实现,其在产生信号时调用,返回之前注册的函数指针
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
第一个参数为特殊情况信息,第二个参数为特殊情况下佳宁要调用的函数的地址值。发生第一个参数代表的情况时,调用第二个参数所指的函数
在signal函数中注册的部分特殊情况和对应常数:
- SIGALRM:已到通过调用alarm函数注册的时间
- SIGINT:输入CTRL+C
- SIGCHLD: 子进程终止
例如,完成“子进程终止则调用mychild函数”的请求:
signal(SIGCHLD, mychild);
signal函数示例
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void timeout(int sig)
{
if (sig == SIGALRM)
{
puts("Time out!");
}
alarm(2); // 实现每隔2秒重复产生SIGALRM信号
}
void keycontrol(int sig)
{
if (sig == SIGINT)
{
puts("CTRL+C pressed");
}
}
int main(int argc, char* argv[])
{
int i;
signal(SIGALRM, timeout);
signal(SIGINT, keycontrol);
alarm(2); // 预约2秒后发送SIGALRM信号
for (i = 0; i < 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
其中,alarm函数的介绍如下:
#include <unistd.h>
unsigned int alarm(unsigned int senconds);
返回0或以秒为单位的距SIGALRM信号发生所剩时间。如果调用该函数的同时向它传递一个正整数参数,相应时间后将产生SIGALRM信号。若向该函数传递0,则之前对SIGALRM信号的预约将取消。如果通过该函数预约信号后未指定该信号对应的处理函数,则(通过调用signal函数)终止进程,不做任何处理
调用信号处理函数的主体的确是操作系统,但进程处于睡眠状态时无法调用函数。因此,产生信号时,为了调用信号处理器,将唤醒由于调用sleep函数而进入阻塞状态的进程,而且,进程一旦被唤醒就不会再进入睡眠状态。即使还未到sleep函数中规定的时间也是如此。所以,上述示例运行不到10秒就会结束
sigaction函数
sigaction函数类似于signal函数,而且完全可以代替后者,也更稳定,因为signal函数在UNIX系列的不同操作系统中可能存在区别,但sigaction函数完全相同
#include <signal.h>
int sigaction(int signo, const struct sigaction* act, struct sigaction* oldact);
成功时返回0,失败时返回-1
(1)signo
与signal函数相同,传递信号信息
(2)act
对应于第一个参数的信号处理函数(信号处理器)信息
(3)oldact
通过此参数获取之前注册的信号处理函数指针,若不需要则传递0
sigaction结构体定义如下:
struct sigaction
{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
};
其中,sa_handler保存信号处理函数的指针值。sa_mask和sa_flags的所有位初始化为0即可,这两个成员用于指定信号相关的选项和特性
sigaction示例
#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);
act.sa_flags = 0;
sigaction(SIGALRM, &act, 0);
alarm(2);
for (i = 0; i < 3; i++)
{
puts("wait...");
sleep(100);
}
return 0;
}
利用sigaction消灭僵尸进程
#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); // 调用waitpid函数,子进程将正常终止
if (WIFEXITED(status))
{
printf("Remove proc id: %d \n", id);
printf("Child send: %d \n", WEXITSTATUS(status));
}
}
int main(int argc, char* argv[])
{
pid_t pid;
struct sigaction act; // 注册SIGCHLD信号对应的处理器
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;
}
基于多任务的并发服务器
扩展之前的回声服务器端,使其可以同时向多个客户端提供服务。每当有客户端请求服务时,回声服务器端都创建子进程以提供服务。为了完成这些任务,需要经过如下过程:
- 第一阶段:回声服务器端(父进程)通过调用accept函数受理连接请求
- 第二阶段:此时获取的套接字文件描述符创建并传递给子进程
- 第三阶段:子进程利用传递来的文件描述符提供服务
实现并发服务器
#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("Usage : %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);
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)
{
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, BUF_SIZE)) != 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 read_childproc(int sig)
{
pid_t pid;
int status;
pid = waitpid(-1, &status, WNOHANG);
printf("removed proc id: %d \n", pid);
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
注意:调用fork函数时复制父进程的所有资源。但套接字并非进程所有,从严格意义上说,套接字属于操作系统,只是进程拥有代表相应套接字的文件描述符。调用fork函数后,2个文件描述符指向同一套接字,只有2个文件描述符都终止(销毁)后,才能销毁套接字。因此,调用fork函数后,要将无关的套接字文件描述符关掉
分割TCP的I/O程序
对于已实现的回声客户端,传输数据后需要等待服务器端返回的数据,因为程序代码中重复调用了read和write函数。现在可以创建多个进程,分割数据的收发过程
客户端的父进程负责接收数据,额外创建子进程负责发送数据。分割后,不同进程分别负责输入和输出,这样,无论客户端是否从服务器端接收完数据都可以进程传输
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
void read_routine(int sock, char* buf);
void write_routine(int sock, char* buf);
int main(int argc, char* argv[])
{
int sock;
pid_t pid;
char buf[BUF_SIZE];
struct sockaddr_in serv_adr;
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
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 = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(argv[2]));
if (connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
{
error_handling("connect() error!");
}
pid = fork();
if (pid == 0)
{
write_routine(sock, buf);
}
else
{
read_routine(sock, buf);
}
close(sock);
return 0;
}
void read_routine(int sock, char* buf)
{
while (1)
{
int str_len = read(sock, buf, BUF_SIZE);
if (str_len == 0)
{
return;
}
buf[str_len] = 0;
printf("Message from server: %s", buf);
}
}
void write_routine(int sock, char* buf)
{
while (1)
{
fgets(buf, BUF_SIZE, stdin);
if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
{
shutdown(sock, SHUT_WR); // 向服务器端传递EOF
return;
}
write(sock, buf, strlen(buf));
}
}
void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}