linux应用—进程的一些小结

本文详细介绍了Linux系统中创建新进程的fork()函数,包括其返回值的含义、进程副本的概念以及父子进程如何交互。同时,讨论了wait()和waitpid()函数在监视和回收子进程中的作用,以及如何通过SIGCHLD信号实现异步监控。文章还提到了僵尸进程和孤儿进程的问题,强调了父进程回收子进程的重要性。
摘要由CSDN通过智能技术生成

fork()

一个现有的进程可以调用fork()函数创建一个新的进程,调用fork()函数的进程成为父进程,由fork()函数创建出来的进程被称为子进程


一次fork调用会产生两次返回值

怎么理解这句话?

  • fork调用会创建一个新的进程,这个新的进程就是子进程。也就是说,fork调用之后会存在两个进程:一个子进程,一个父进程
  • 所以会有两个返回值,子进程返回一次,父进程返回一次。
  • 父进程返回的是子进程的pid,子进程返回的是0。

用代码测验 一下,加深理解。

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

int main(void){

    int ret;
    ret = fork();
    if(-1 == ret){
        perror("fork error");
        return 1;

    }
    else if(0 == ret){
        printf("I am child, PID = %d, PPID = %d\n",getpid(),getppid());
    }
    else {
        printf("I am parent, PID = %d, PPID = %d, CPID = %d\n", getpid(), getppid(), ret);
    }
    return 0;
}

然后编译运行。

可以看到,返回了一个大于0的数和0。

大于0时,返回的是创建的子进程的pid,也就是4697,此时的父进程是bash,pid是4640,bash的子进程为4696。

等于0时,此时父进程pid为4696,子进程为4697,也就是fork创建的子进程。

OK,这样应该就理解了fork的两次返回了。


fork创建了一个与原来进程几乎完全相同的进程

  • 子进程看作是父进程的一个副本,fork是以复制的形式创建子进程,子进程几乎完全复制了父进程
  • 父进程与子进程不共享这些存储空间,比如拷贝父进程的数据段、堆、栈、文件描述符等。
  • 子进程与父进程各自在自己的进程空间运行,相互独立

可以用代码来测试一下。

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

int main(void){

    int ret;
    int a = 10;
    ret = fork();
    if(-1 == ret){
        perror("fork error");
        return 1;

    }
    else if(0 == ret)//子进程
    {
        printf("I am child\n");
        printf("a = %d\n",a);
        a = 100;
        printf("a= %d\n",a);
    }
    else//父进程
    {
        sleep(1);//先让子进程去修改a的值
        printf("I am parent\n");
        printf("a = %d\n",a);
    }
    return 0;
}

编译运行。

可以看到,即便是子进程里修改了a的值,但是父进程打印出来的还是原来的值。


子进程从fork调用返回后开始运行

  • 虽然子进程和父进程运行在不同的进程空间中,但是它们执行的却是同一个程序。
  • 子进程执行的是fork之后的代码,不会从头开始运行

代码测试一下。

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

int main(void){

    int ret;
    int a = 10;

    printf("hello world!\n");//写在fork之前
/**************************************************/
    ret = fork();
    if(-1 == ret){
        perror("fork error");
        return 1;

    }
    else if(0 == ret)//子进程
    {
        printf("I am child\n");
        printf("a = %d\n",a);
        a = 100;
        printf("a= %d\n",a);
    }
    else//父进程
    {
        sleep(1);//先让子进程去修改a的值
        printf("I am parent\n");
        printf("a = %d\n",a);
    }
    printf("end\n");
    return 0;
}

编译运行一下。

 可以发现,确实是如此。

父、子进程间的文件共享

子进程复制了父进程的文件描述符,那如果父、子进程都对同一个文件进行读写,那是会覆盖还是接续写呢?

理论上是接续写。

但我们可以用一个程序来进行验证一下。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(void){

    int fd;

    fd = open("./test.txt",O_WRONLY | O_TRUNC);
    if(-1 == fd){
        perror("open error");
        return 1;
    }
    switch (fork())
    {
    case -1:
        perror("fork error");
        close(fd);
        return 1;
    //子进程
    case 0:
        printf("I am child process\n");
        write(fd,"hello world",11);
        close(fd);
        return 0;
    
    default:
        printf("I am parent process\n");
        write(fd,"123456", 6);
        close(fd);
        return 0;
    }
    return 0;

}

编译运行一下。

可以看出,的确是接续写的。

父、子进程间的竞争关系

fork之后父进程、子进程谁先运行?

不确定,绝大多数情况下是父进程先运行。

监视子进程

父进程监视子进程,父进程需要知道子进程的状态改变,子进程什么时候发生状态改变

那么子进程状态改变包括哪些呢?

  1. 子进程终止
  2. 子进程因为收到停止信号而停止运行,SIGSTOP、SIGTSTP
  3. 子进程在停止状态下因为收到恢复信号而恢复运行,SIGCONT

wait()函数

  1. 只能监视子进程什么时候终止,获取子进程终止时的状态信息
  2. 回收子进程的资源

编写程序验证一下。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>

int main(void){

    switch (fork())
    {
    case -1:
        perror("fork error");
        return 1;
    case 0:
        printf("I am child,PID = %d\n",getpid());
        return 0;
    
    default:{
        int ret;
        int status;
        printf("I am parent\n");
        ret = wait(&status);
        if(-1 == ret){
            perror("wait error");
            return 1;
        }
        printf("%d, %d\n",ret,WIFEXITED(status));
        return 0;
    }
    }

    return 0;
}

特别注意的是,一开始default:之后我是没有加大括号的,然后出现了报错。

然后我去查了一下,发现其实是因为如果不加大括号,那么这个变量的声明是在整个switch作用域的,不够严谨,所以要加大括号。

具体参考这篇文章:http://t.csdn.cn/aMH62

编译运行一下。

成功。

参数status不为NULL的情况下,则wait()会将子进程的终止时的状态信息存储在它指向的int变量中,可以通过以下的宏来进行检查。

  • WIFEXITED(status)如果子进程正常终止,则返回 true
  • WEXITSTATUS(status)返回子进程退出状态,是一个数值,其实就是子进程调用_exit()exit() 时指定的退出状态;wait()获取得到的 status 参数并不是调用_exit()exit()时指定的状态,可通过 WEXITSTATUS 宏转换
  • WIFSIGNALED(status)如果子进程被信号终止,则返回 true
  • WTERMSIG(status)返回导致子进程终止的信号编号。如果子进程是被信号所终止,则可以通过此宏获取终止子进程的信号
  • WCOREDUMP(status)如果子进程终止时产生了核心转储文件,则返回 true

waitpid()函数

使用wait()函数存在一些限制:

  1. 父进程创建多个子进程,无法等待某个特定的子进程完成,只能按照顺序
  2. 如果子进程没有终止,wait()总是保持阻塞,有时我们希望非阻塞等待
  3. 使用wait()只能发现那些被终止的子进程

waitpid()原型如下:

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options)

重点看options这个参数,它可以包括0个或多个如下标志:

  • WNOHANG如果子进程没有发生状态改变(终止、暂停),则立即返回,也就是执行非阻塞等待,可以实现轮训 poll,通过返回值可以判断是否有子进程发生状态改变,若返回值等于 0 表示没有发生改变。
  • WUNTRACED除了返回终止的子进程的状态信息外,还返回因信号而停止(暂停运行)的子进程状态信息;
  • WCONTINUED返回那些因收到 SIGCONT 信号而恢复运行的子进程的状态信息。

由此可见,它的功能要强于wait()函数,弥补了子进程状态改变的后两个情况。

异步方式监视子进程

SIGCHLD信号

我们可以为SIGCHLD信号绑定一个信号处理函数,然后在信号处理函数中调用wait/waitpid函数回收子进程。

只有子进程终止后才需要回收,如果是其他两种状态改变,则父进程可根据设计需求在信号处理函数做出相应的处理

使用SIGCHLD 信号回收子进程需要注意一个问题:

调用信号处理函数的时候,会暂时将当前正要处理的信号添加到进程的信号掩码中,这样一来,当SIGCHLD信号处理函数正在为某一个已经终止的子进程收尸时,如果此时相继有两个子进程终止了,也就是会产生两次SIGCHLD信号,但是会有一次SIGCHLD信号会被丢失,也就是说父进程最终也会只能接收一次SIGCHLD,那么就会导致漏掉一个,导致有一个子进程没有被回收

解决此问题,就是在信号处理函数中循环以非阻塞方式来调用waitpid()。

while (waitpid(-1, NULL, WNOHANG) > 0)
continue;

使用代码测试一下。

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

static void wait_child(int sig)
{
    /*替子进程收尸*/
    printf("父进程回收子进程\n");
    while (waitpid(-1,NULL,WNOHANG)>0)//循环非阻塞的方式
    {
        continue;
    }
    
}

int main(void){

    /*为信号绑定SIGCHLD函数*/
    struct sigaction sig = {0};
    sigemptyset(&sig.sa_mask);
    sig.sa_handler = wait_child;
    sig.sa_flags = 0;
    if(-1 == sigaction(SIGCHLD,&sig,NULL)){
        perror("sigaction error");
        exit(-1);
    }

    /*创建子进程*/
    switch (fork())
    {
    case -1:
        perror("fork error");
        exit(-1);

    case 0://子进程
        printf("子进程<%d>被创建\n", getpid());
        sleep(1);
        printf("子进程结束\n");
        _exit(0);

    
    default://父进程

        break;
    }

    while(1){
        sleep(1);
    }
    return 0;
}

编译运行一下。

实现了异步监视。

僵尸进程与孤儿进程

  • 父进程先于子进程结束,成为孤儿进程,父进程变为init进程
  • 如果子进程先于父进程结束,此时父进程还未来得及“收尸”,成为僵尸进程
  • 僵尸进程无法通过信号将其删除,僵尸进程本来就是已经终止的进程,只不过还未被回收,只要它的父进程一直不去回收它,这个僵尸进程就会一直存在系统中。

参考正点原子linux应用开发教程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值