[TCP/IP网络编程]fork函数、僵尸进程以及信号处理

为了实现Linux下多进程服务端,我们有必要掌握三个基础知识:fork函数僵尸进程以及信号处理

fork函数

说明:多进程编程,说白了就是多个进程并发处理事件,通常我们运行一个程序的时候只有一个进程在执行任务,那么怎么才能创建多个进程来工作呢?答案就是使用fork函数,fork函数主要就是用来创建一个新的进程,被创建出来的新的进程就叫做子进程,而创建子进程的进程相对就叫做父进程。例如,进程A调用fork函数创建了进程B,那么A就是B的父进程,B就是A的子进程。调用fork函数后,父子进程拥有完全独立的内存结构。

使用:调用fork函数会返回一个pid_t类型的值,若成功创建子进程则会返回一个子进程的ID给父进程,返回一个0给子进程,创建失败返回-1。

父进程:fork函数返回子进程ID

子进程:fork函数返回0

例如有如下代码

#include<stdio.h>
#include<unistd.h>
int main(int argc,char* argv[])
{
    pid_t id = fork();
    if(id == 0)               
    {
        printf("我是子进程.....");  //子进程执行的区域
    }
    else                       
    {
        printf("我是父进程.....");  //父进程执行的区域
    }
    printf("这是大家都能执行的区域......")
}

从以上代码可以看出,我们可以通过id的值来区分哪部分是子进程执行的代码,哪部分是父进程执行的代码(因为父进程的id的值是子进程的ID,而子进程的id的值为0)。

最后一行打印,因为没有条件限制,所以父进程和子进程都能执行。

也就是,执行完fork()代码后就已经创建了一个子进程,但是id == 0 条件里面的代码只有子进程能执行,而 id != 0 条件里面的代码只有父进程能执行,若是两个条件都没的代码则父进程和子进程都可以执行。

为了加深对fork的理解,我们来做一道题:请问以下代码输出几个‘-’?(题目来源:牛客网)

int main(void) {
int i;
for (i = 0; i < 2; i++) {
fork();
printf("-\n");
}
return 0;
}

好吧,答案是6。

解析:

1、首先刚开始进入循环,i = 0,执行了一个fork(),于是后面的打印由一个父进程和子进程执行,所以打印两个‘-’。目前‘-’为2个。

2、接下来父进程i++,子进程也i++,那么父进程和子进程的i都变成了1,都分别进入第二次循环。

3、在第二次循环中,父进程又执行了fork,于是跟步骤1一样,所以这时候又打印出两个‘-’,目前‘-’总共打印了四个(包括第一步打印的)。

4、还是在第二次循环中,因为在第一步中fork创建出了一个子进程,该子进程进到第二次循环的时候也执行fork操作,于是该子进程又创建了一个新的子进程,该子进程为新子进程的父进程,新子进程为该子进程的子进程。之后你们懂的,执行跟步骤1一样的动作,子进程和新的子进程分别打印‘-’,至此总共打印了6个‘-’。

5、i再++,i = 2,退出循环。

僵尸进程

说明:父进程通过fork创建出一个子进程后,子进程占用着系统的资源,所以当子进程执行完任务后应被销毁。但如果在子进程执行完任务后,父进程没对它进行销毁,则该状态下的子进程我们称为僵尸进程。

这里啰嗦一下说明僵尸进程孤儿进程的区别:

僵尸进程:父进程还在,但子进程执行完任务了,父进程没有销毁子进程,子进程成为僵尸进程。

孤儿进程:父进程已经退出,但子进程还在执行人物,子进程成为孤儿进程。

后果:因为僵尸进程也占据着系统的资源,若是僵尸进程数量变多,则给系统带来严重的负担。

销毁:既然知道了什么是僵尸进程,那么我们怎么销毁它呢?因为僵尸进程本来就是由某个父进程创建出来的,所以要销毁的工作也得交给父进程来做。

销毁方法一:wait函数

pid_t wait(int* statloc); //该函数放在父进程里调用

销毁过程:子进程完成任务后需要执行return或exit()语句,执行这两个语句相当于在跟父进程说:我已经执行完任务了,你把我销毁吧。

当子进程return或者exit()完成后,父进程就执行wait函数销毁子进程。

同时介绍两个宏:

WIFEXITED:子进程正常终止时返回“真”(true)

WEXITSTATUS:返回子进程的返回值

执行完wait函数后可以这样使用来判断是否销毁了子进程

if(WIFEXITED(status))    //如果正常销毁了子进程
{
    printf("child pass num:%d",WEXITSTATUS(status));  //返回子进程的返回值
}

演示一遍利用wait函数销毁子进程

#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;         //子进程通过return终止
    }
    else                  //else里面是父进程执行的内容
    {
        wait(&status);    //终止的子进程的相关信息将保存在status变量中,同时相关子进程被销毁
        if(WIFEXITED(status))    //如果正常销毁了子进程
        {
            printf("child pass num:%d",WEXITSTATUS(status));  //返回子进程的返回值
        }
    }
    return 0;
}

缺陷:该方法有一个缺陷,就是父进程执行到wait函数那条语句的时候将会发生阻塞,如果没有子进程需要终止则父进程一直阻塞在wait函数那行语句,直到有子进程终止,因此需谨慎调用该函数。

销毁方法二:waitpid函数

pid_t waitpid(pid_t pid,int* statloc,int options);
//成功时返回终止的子进程ID或0,失败时返回-1

参数

pid:等待终止的目标子进程id,若传递-1,则与wait函数一样,可以等待任意子进程终止。

statloc:与wait函数的statioc参数具有相同的含义。

options:传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数。

除了参数需要理解以下,其它的跟wait函数差不多,演示代码:

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>

int main()
{
    int status;
    pid_t pid = fork();  //创建子进程
    
    if(pid == 0)        //子进程执行的语句
    {
        sleep(15);      //推迟15秒执行
        return 24;
    }
    else                //else 里面是父进程执行的内容
    {
        //利用while语句一直在等待看有没有终止的子进程,若没有则返回0,之后每个三秒又检查一遍
        while(!waitpid(-1,&status,WNOHANG)) 
        {
            sleep(3);
            puts("sleep 3sec....");
        }
        if(WIFEXITED(status))    //如果正常销毁了子进程
        {
            printf("child pass num:%d",WEXITSTATUS(status));  //返回子进程的返回值
        }
    }
    return 0;
}

为什么要用while循环一直检查有没有子进程终止?

因为如果父进程执行到waitpid函数时还没子进程终止的话,父进程会继续执行下面的代码而不发生阻塞,这里用while模拟阻塞的情况,大家也可以不循环,那么结果显而易见:子进程得不到销毁。因为子进程的代码里有sleep(15)语句,所以当父进程执行到waitpid函数时,子进程可能还没return,但这样也证明了waitpid函数并没有阻塞。

信号处理

前面销毁子进程遗留的问题:目前我们已经知道了进程创建以及销毁的方法,但是还有一个问题还没解决:子进程究竟何时终止?难道我们调用waitpid函数后要利用while函数无休止地等待吗?

通常父进程也很繁忙,不可能一直在原地等待子进程的终止,于是为了既能销毁子进程,又不妨碍父进程执行后面的任务,可以向操作系统求助。

利用信号处理就可以达到我们的目的。

什么是信号处理

说白了就是进程在开始执行下面任务之前,会跟操作系统说:嘿,操作系统,如果我之前创建的子进程终止了,就帮我调用某个函数(该函数用来销毁子进程)。

操作系统:好的,如果你的子进程终止了,我会帮你调用那个函数(用来销毁子进程的那个函数),你忙去吧!

通过以上对话我们可以得出两个结论:1、这个销毁子进程的函数我们要提前写好。2、调用函数把子进程销毁的是操作系统而不是父进程,所以在销毁子进程的时候父进程不必停下手头的工作。

signal函数

#include<signal.h>

void (*signal(int signo,void(*func)(int)))(int);

这个函数不好理解,如果对函数指针不熟悉的大概率是看不懂,所以建议那些还不会函数指针的同学先把函数指针的知识先补补。

函数名:signal

参数:int signo,void(*func)(int)

返回类型:参数类型为int型,返回void型函数指针

调用上述函数时,第一个参数是信号信息,第二个参数是发出该信号信息时需要调用的函数的地址值(指针)。

那么有什么信号信息呢?下面列举三个:

SIGALRM:已经通过调用alarm函数的注册的时间。

SIGINT:输入CTRL+C。

SIGCHLD:子进程终止。

可能大家看到这里还有点懵,但是没关系,现在不需要完全知道什么意思,接下来我们通过例子来一一攻破。

首先认识前两个信号信息:SIGALRM 和 SIGINT

这个情况我们还需要认识一个函数

#include<unistd.h>

unsigned int alarm(unsigned int seconds);

我们向该函数传进去一个正整型的数字,该数字代表时间(以秒为单位),相应时间后将产生SIGALRM信号,signal函数收到SIGALRM信号后会调用相应的函数。

而发送SIGINT信号的方式就是在键盘上按下“CTRL+C”。

代码演示:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void timeout(int sig)      //收到SIGALRM信号后执行的函数
{
    if(sig == SIGALRM)
        puts("Time out!");
    alarm(2);              //再次设置alarm函数,过两秒后继续发出SIGALRM信号
}

viud keycontrol(int sig)    //收到SIGINT信号后执行的函数
{
    if(sig == SIGINT)
        puts("CTRL+C pressed");
}

int main()
{
    int i;
    signal(SIGALRM,timeout);   //注册:收到SIGALRM信号后执行timeout函数
    signal(SIGINT,keycontrol); //注册:收到SIGINT信号后执行keycontrol函数
    alarm(2);                  //预约2秒后发出SIGALRM信号
    
    for(int i = 0;i<3;i++)
    {
        puts("wait...");
        sleep(100);
    }
    return 0;
}

通过代码我们可以很容易得出打印的信息为:

wiat...

Time out!

wait...

Time out!

wait...

Time out!

上面的打印结果是还没有发出SIGINT信号的结果,如果在运行过程中按下CTRL+C,可以看到输出“CTRL+C pressed”。

调用函数的主体确实是操作系统,但进程处于睡眠状态时无法调用函数。因此,产生信号时,为了调用信号对应的函数,将唤醒由于调用sleep函数而进入阻塞状态的进程。

接下来我们再认识多一个函数:sigaction函数

为什么要认识这个函数,其实这个函数跟signal函数的功能一样,但是signal函数在UNIX系列的不同操作系统中可能存在区别,而sigaction函数完全相同,也就是更稳定。所以实际上现在很少使用signal函数编写程序。

sigaction函数原型

#include<signal.h>

int sigaction(int signo,const struct sigaction* act,struct sigaction* oldact);

//成功时返回0,失败时返回-1.

signo:与signal函数相同,传递信号信息。

act:对应与第一个参数的信号处理函数的信息。

oldact:通过此参数获取之前注册的信号处理函数指针,若不需要则传递0。

第二个参数的结构体如下

struct sigaction
{
    void (*sa_handler)(int);  //需要处理的函数
    sigset_t sa_mask;      
    int sa_flags;
}

里面的sa_mask和sa_flags我还没搞懂干嘛用的,但是不妨碍目前的学习,我们把它俩初始化为0即可。

利用sigaction函数处理SIGALRM信号演示代码:

#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void timeout(int sig)             //收到SIGALRM信号需要执行的函数
{
    if(sig == SIGALRM)
        puts("Time out!");
    alarm(2);                     //再次启动计时器
}    

int main()
{
    int i;
    struct sigaction act;
    //下面三行初始化act
    act.sa_handler = timeout;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    sigaction(SIGALRM,&act,0);  //注册信号

    ararm(2);                  //2秒后发出SIGALRM信号

    for(i = 0;i<3;i++)
    {
        puts("wait...");
        sleep(100);
    }
    return 0;
}

运行结果:

wait...

Time out!

wait...

Time out!

wait..

Time out!

现在介绍最后一个信号信息,SIGCHLD:子进程终止。

顾名思义,发出这个信号的条件是某个子进程终止,而通过上面的学习我们知道子进程想要终止只需执行return或者exit()语句,然后等待被销毁就行了。

利用信号处理技术消灭僵尸进程的演示代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>

void read_childproc(int sig)   //收到SIGCHLD信号执行的函数(销毁子进程的函数)
{
    int status;
    pid_t id = waitpid(-1,&status,WNOHANG);   //销毁子进程
    if(WIFEXITED(status))                     //如果正常销毁了子进程
    {
        printf("销毁的子进程的ID:%d",id);
        printf("child pass num:%d",WEXITSTATUS(status));  //返回子进程的返回值
    }
}

int main()
{
    pid_t pid;
    struct sigaction act;
    //下面三行初始化act
    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    
    sigaction(SIGCHLD,&act,0);  //注册,等待SIGCHLD信号

    pid = fork();             //创建子进程
    if(pid == 0)              //子进程执行区域
    {
        puts("hi~i'm child process");
        sleep(10);             //先挂起10s
        return 12;             //终止
    }
    else                       //else里面为父进程的内容
    {
        printf("child proc id:%d \n",pid);
        pid = fork();          //再创建一个子进程
        if(pid == 0)
        {
            puts("hi~i'm child process");
            sleep(10);         //先挂起10s
            exit(24);          //子进程结束
        }
        else
        {
            int i;
            printf("Child proc id:%d \n",pid);
            for(i = 0;i<5;i++)
            {
                puts("wait...");
                sleep(5);
            }
        }
    }
    return 0;
}

 

以上就是通过信号处理销毁僵尸进程的代码。有了这些知识我们就可以编写多进程服务器端的代码了。

我将在后面出一篇多进程服务器端的文章。

以上的代码演示均是在博客这边手打出来,自己还没丢进编译器运行过的,可能会有语法报错,但是这篇文章主要是介绍fork函数、僵尸进程以及信号处理的理论知识,只要能看懂就好,有错的地方大家可以在下方留言。

参考书籍:《TCP/IP网络编程》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值