在实际业务中,需要编写并发服务器来同时向所有发起请求的客户端进行服务,下面列出三个比较有代表性的并发服务器实现模型和方法:
1.多进程服务器: 通过创建多个进程提供服务
2.多路复用服务器: 通过捆绑并统一管理I/O对象提供服务
3.多线程服务器: 通过生成与客户端等量的线程提供服务
下面先讲解多进程服务器,该方法Windows平台不支持,因此主要是在Linux平台下进行编程。
1.进程
1.1 概念
进程是操作系统进行资源分配和调度的一个独立单位,它是程序的运行实例。
1.1.1 进程ID
通常缩写为PID,是操作系统分配给每个进程的非负整数的唯一标识符,通常是大于2的整数,在Linux系统中可以使用ps au
命令进行查看
1.1.2 通过fork()
函数创建进程
基本语法
pid_t fork(void);//创建成功返回PID,失败返回-1;pid_t是一个数据类型,用于表示进程ID(PID)
通过fork()
函数创建的进程,是调用该函数的进程的副本,也就是父进程创建副本子进程。
fork()
函数在父进程中返回新创建的子进程的PID,在子进程中返回0。以此来区分父进程和子进程。
调用fork()
函数后,子进程和父进程有着独立的内存结构,可以单独运行程序,下面是验证代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork(); // 创建新进程
int a=10;
if (pid < 0) {
// fork失败
perror("fork failed");
exit(EXIT_FAILURE);
}
if (pid > 0) {//设置pid>0,则可以在该部分编写父进程代码
// 父进程
printf("I am the parent process (PID: %ld)\n", (long)getpid());
a=a+10;
printf("parent a is %d\n",a);
} else {//设置pid==0,则可以在该部分编写子进程代码
// 子进程
printf("I am the child process (PID: %ld)\n", (long)getpid());
a=a+5;
printf("child a is %d\n",a);
}
// 父进程和子进程都可以执行到这里
printf("Continuing execution...\n");
return 0;
}
2.僵尸进程
僵尸进程通常指的是已经终止但尚未被其父进程回收状态信息的子进程,进程在完成工作后(执行完mian函数中的程序)应该被销毁,但有时并没有,于是变成了僵尸进程,继续占用系统资源。
2.1 产生僵尸进程的原因
父进程没有主动要求获得子进程的结束状态,导致操作系统一直保持该子进程,即进入僵尸状态。下面将主动创建一个僵尸进程,然后查看验证
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork(); // 创建新进程
if(pid==0){
puts("I am child\n");
}
else{
printf("child PID is %d\n",getpid());
sleep(30);// 父进程休眠60秒,方便查看子进程状态
}
if (pid==0)
{
printf("end child process\n");
}else{
printf("end parent process\n");
}
return 0;
}
左边输出子进程的PID 11957,并且显示子进程已经end,但实际上在右边输入ps aux
时,依然发现了子进程对应的PID 11957,说明已经成为了僵尸进程。
2.2 销毁僵尸进程方法1: 利用wait()
函数
为了销毁子进程,父进程应该主动请求子进程的返回值
2.2.1基本语法
pid_t wait(int *status);
//int *status 参数是一个指向 int 类型的指针,用于接收关于终止子进程的状态信息
status 参数包含了多种信息,可以通过以下宏来解释:
- WIFEXITED(status): 检查子进程是否正常退出。如果返回非零值,表示子进程是因调用 exit() 或返回语句结束的。
- WEXITSTATUS(status): 如果子进程是正常退出的(即 WIFEXITED(status)返回非零值),这个宏会返回子进程的退出代码(exit code),这是由 exit() 函数的参数或 return 语句的值决定的。
- WIFSIGNALED(status): 检查子进程是否是因接收到信号而终止的。如果返回非零值,表示子进程是被信号终止的。
- WTERMSIG(status): 如果子进程是因为信号而终止的(即 WIFSIGNALED(status)
返回非零值),这个宏会返回导致子进程终止的信号。
接下来会用一个例子来展示用法
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int status;
pid_t pid = fork(); // 创建新进程
if (pid==0)
{
return 3;//WEXITSTATUS(status)会返回3
}else{
printf("child PID is %d\n",pid);
pid=fork();
if (pid==0)
{
exit(7);//WEXITSTATUS(status)会返回7
}else{
printf("child PID is %d\n",pid);
wait(&status);//销毁子进程
if (WIFEXITED(status))
{
printf("child exit code is %d\n",WEXITSTATUS(status));
}
wait(&status);//销毁子进程
if (WIFEXITED(status))
{
printf("child exit code is %d\n",WEXITSTATUS(status));
}
sleep(30);
}
}
return 0;
}
查看右边,发现两个子进程已经被销毁了,同时也将3和7返回给了父进程。
调用wait()函数要谨慎,如果没有已经终止的子进程,则程序会发生阻塞(Blocking),直达有子进程终止。
2.3 销毁僵尸进程方法2:利用waitpid()
函数
阻塞: 程序发生阻塞,通常指的是程序在执行过程中遇到了某个条件尚未满足,因此无法继续执行后续操作,必须停下来等待这个条件变为可继续执行的状态。
wait()
函数会引起堵塞,可以考虑使用waitpid()
函数,与wait()
函数相比,waitpid()
提供了更多的灵活性,因为它允许父进程指定要等待的子进程的 PID 或一组 PID。
2.3.1 基本语法
pid_t waitpid(pid_t pid, int *status, int options);
//pid:指定要等待的子进程的 PID。
//status:指向一个整数变量的指针,用于接收子进程的退出状态。
//options:指定等待操作的选项。常用的选项有 WNOHANG(使 waitpid() 调用非阻塞)、WUNTRACED(使 waitpid() 能够返回停止的子进程)。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int status;
pid_t pid = fork(); // 创建新进程
if (pid==0)
{
sleep(1);//让子进程挂起1秒
return 24;
}else{
while(!waitpid(-1,&status,WNOHANG)){
//使用WNOHANG,即使子进程没有结束,依然能够向下运行,如果使用wait,则不会向下运行
printf("parent");
}
if (WIFEXITED(status))
{
printf("child send %d \n",WEXITSTATUS(status));
}
}
return 0;
}
3.信号处理
如果子进程一直不终止,父进程也不能一直调用waitpid()
函数等待子进程终止,因为父进程还有自己的事要做,因此引入信号处理机制,当子进程终止时,让操作系统向父进程发送信号,让父进程立即来处理子进程终止任务。
3.1 信号注册函数signal()
函数和 sigaction()
函数
3.1.1 signal()
函数基本语法:
是一种在 Unix 和类 Unix 系统中用于设置信号处理方式的古老函数,它允许程序指定当接收到特定信号时应该调用的函数。
void (*signal(int signum, void (*handler)(int)))(int);
//signum:指定要处理的信号的整数常量,例如:
// SIGINT:中断信号,通常由 Ctrl+C 产生。
// SIGALRM:由 alarm 函数生成,用于定时器到期。
// SIGCHLD:当子进程停止或终止时发送给父进程。
//handler:当信号发生时应该调用的函数。这个函数应该符合 sighandler_t 类型,它是一个接受一个 int 参数的函数指针。
//alarm()函数,设置一个定时器,经过指定的秒数后,定时器将向调用进程发送 SIGALRM 信号
unsigned int alarm(unsigned int seconds);//seconds:指定定时器到期前的秒数。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void timeout(int sig){
if(sig==SIGALRM){
puts("TIME OUT");
}
alarm(2);
}
void keycontrol(int sig){
if(sig==SIGINT){
puts("CTRL+C PRESSED");
}
}
int main(int argc,char* argv[]){
signal(SIGALRM,timeout);
signal(SIGINT,keycontrol);
alarm(2);
for(int i=0;i<3;i++){
puts("wait...");
sleep(100);
}
return 0;
}
会输出TIME OUT三次后结束,因为运行了timeout()
函数;如果按下CTRL+C,则会输出CTRL+C PRESSED,因为运行了keycontrol()
函数
3.1.2 sigaction()
函数基本语法:
现代 POSIX 标准推荐使用 sigaction()
函数作为替代,下面只讲解其中替代signal()
函数的功能
int sigaction(
int signum, //指定要操作的信号的整数常量,如 SIGINT、SIGTERM 等
const struct sigaction *restrict action, //指向 sigaction 结构的指针,该结构定义了信号的新的处理行为。
struct sigaction *restrict oaction//指向 sigaction 结构的指针,用于存储信号的旧处理行为。如果不需要旧的处理行为,这个参数可以是 NULL。
);
//sigaction 结构
struct sigaction {
void (*sa_handler)(int); // 指向信号处理函数的指针
void (*sa_sigaction)(int, siginfo_t *, void *); // 另一种信号处理函数,可以接收额外信息
sigset_t sa_mask; // 在信号处理函数执行期间需要屏蔽的信号集
int sa_flags; // 信号处理选项
void (*sa_restorer)(void); // 已废弃,不应使用
};
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
void timeout(int sig){
if(sig==SIGALRM){
puts("TIME OUT");
}
alarm(2);
}
int main(int argc,char* argv[]){
struct sigaction act;// 初始化sigaction结构,用于设置信号处理器
act.sa_handler=timeout;// 设置信号处理器为timeout函数
sigemptyset(&act.sa_mask);// 初始化信号掩码,为空表示不屏蔽任何信号
act.sa_flags=0;// 设置信号处理选项,0表示默认行为
// 使用sigaction设置SIGALRM信号的处理函数
sigaction(SIGALRM,&act,0);
// 设置一个2秒的定时器,当定时器到期时,将发送SIGALRM信号给本进程
alarm(2);
for(int i=0;i<3;i++){
puts("wait...");
sleep(100);
}
return 0;
}
3.2 利用信号处理技术消灭僵尸进程
上面介绍了信号注册方法,下面利用 sigaction()
函数编写消灭僵尸进程,子进程终止时将产生SIGCHLD信号。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.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);
printf("exit code: %d \n", WEXITSTATUS(status));
}
}
int main(int argc, char* argv[]){
pid_t pid;
// 初始化sigaction结构
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
// 使用sigaction设置SIGCHLD信号的处理函数
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;
}
可以看出两个子进程正常终止了,没有成为僵尸进程,因为printf("removed proc id: %d \n", id);
正确运行。