本文是小编巩固自身而作,如有错误,欢迎指出!

目录

一、进程创建

fork函数

返回值规则

​编辑循环fork创建子进程

写时拷贝

​编辑二、进程终止

正常退出(自愿退出)

异常退出(出错退出)

被信号杀死(最常见)

三、进程等待

进程等待的重要性

wait(阻塞等待任意子进程)

waitpid(更强大:指定 PID、非阻塞)


一、进程创建

fork函数

在前文我们已经讲述过fork的大体功能:

fork() 是 Linux/Unix 中创建新进程最核心的系统调用,作用是复制当前进程,产生一个几乎一模一样的子进程。

返回值规则

  • 父进程:返回子进程的 PID(>0)
  • 子进程:返回 0
  • 创建失败:返回 -1
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("before fork\n");

    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        return 1;
    }

    if (pid == 0) {
        // 子进程
        printf("I am child, pid=%d, ppid=%d\n", getpid(), getppid());
    } else {
        // 父进程
        printf("I am parent, pid=%d, child pid=%d\n", getpid(), pid);
    }

    return 0;
}



循环fork创建子进程

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

// 创建 5 个子进程
#define PROC_NUM 5

int main() {
    int i;
    pid_t pid;

    for (i = 0; i < PROC_NUM; i++) {
        pid = fork(); // 创建子进程

        if (pid < 0) {
            perror("fork failed");
            return 1;
        }

        // ==================================
        // 子进程:执行任务,然后退出!
        // ==================================
        if (pid == 0) {
            printf("【子进程】 pid=%d, 编号=%d, 父pid=%d\n",
                   getpid(), i, getppid());

            // 子进程执行完任务就退出,不回到循环!
            return 0;
        }

        // 父进程:什么都不做,继续下一次循环
    }

    // ==================================
    // 父进程:等待所有子进程退出
    // ==================================
    for (i = 0; i < PROC_NUM; i++) {
        wait(NULL);
    }

    printf("【父进程】所有子进程已回收\n");
    return 0;
}

细心的同学会发现一个问题,我们在for循环每个子进程创建完毕后,也有一个return,这是什么原因呢?

因为子进程不能回到 for 循环,否则子进程也会执行 fork(),导致进程爆炸

写时拷贝

通常父子进程代码共享,当父子进程不写入数据的时候,数据是共享的,当任意一方试图写入的时候,便另外开一份空间,以拷贝的方式拷贝一份数据进行修改,原有的空间的数据属于未进行修改的那一方,这样两方便各自有一份数据了

写时拷贝,就是:先共享,要改再复制。

更标准一点的定义:

多个调用者一开始共享同一份数据 / 内存,当任何一方要修改(写)数据时,系统才真正为它复制一份独立副本,让它在副本上修改,不影响其他人。

二、进程终止

正常退出(自愿退出)

(1)main 函数 return

  • C/C++ 程序 main() 返回 0 或其他值
  • 等价于调用 exit(返回值)

(2)调用 exit () /_exit () /_Exit ()

  • exit():标准库函数,会做清理(刷新缓冲区、调用 atexit 函数)
  • _exit() / _Exit():系统调用,直接退出,不清理

exit 和 _exit 的区别

1. exit ()(温柔退出)

执行流程:

  1. 调用 atexit() 注册的所有清理函数
  2. 刷新所有标准 IO 缓冲区(printf/fwrite 等数据会真正写入)
  3. 关闭所有打开的文件流
  4. 最后调用 _exit() 让内核终止进程

2. _exit ()(暴力退出)

执行流程:直接进入内核,立即终止进程

  • 不刷新缓冲区
  • 不执行清理函数
  • 不关闭用户态文件流
  • 内核直接回收进程资源
  • #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    void clean() {
        printf("清理函数执行\n");
    }
    
    int main() {
        atexit(clean);  // 注册退出时执行的函数
        printf("Hello World");  // 没有换行,数据在缓冲区
    
        // 测试1:用 exit() —— 会输出、会执行清理函数
        exit(0);
    
        // 测试2:用 _exit() —— 什么都不输出,直接退出
        // _exit(0);
    }

    exit()输出:

     _exit()输出:

  • exit(0):输出 Hello World + 清理函数执行
  • _exit(0)无任何输出,缓冲区数据丢失

  

(3)最后一个线程退出

多线程程序中,主线程退出不代表进程退出;最后一个线程退出时进程才终止。

异常退出(出错退出)

  • 除 0
  • 非法内存访问(段错误 Segmentation fault)
  • 总线错误
  • 栈溢出
  • 未捕获的异常(C++、Java、Python)

内核会向进程发送致命信号,进程无法忽略。

被信号杀死(最常见)

常见终止信号

  • SIGINT (2):Ctrl + C
  • SIGQUIT (3):Ctrl + \
  • SIGKILL (9):强制杀死,不可捕获、不可忽略
  • SIGTERM (15):默认 kill 命令,可捕获
  • SIGSEGV (11):段错误
  • SIGABRT (6):调用 abort () 或 assert 失败

信号导致退出时,进程会被内核强制终止。

父进程退出,被 init/systemd 收养后清理

  • 父进程先死,子进程变成孤儿进程
  • 被 PID 1(init/systemd)收养
  • 子进程退出后由 PID 1 自动回收

三、进程等待

进程等待的重要性

子进程退出,如果父进程对此漠不关心,那么就会造成僵尸进程,进而造成内存泄露的问题。

所以,进程等待就是:

父进程调用 wait /waitpid,等子进程退出,然后回收它的尸体(PCB),防止僵尸进程。

  • 回收子进程资源,防止僵尸进程(Zombie)
  • 获取子进程退出状态(正常退出?被杀死?退出码多少?)
  • 父子进程同步(父进程等子进程干完再继续)

wait(阻塞等待任意子进程)

  1. wait的作用是等待当前进程的任意一个子进程的状态改变,即检测并等待进程状态变成Z僵尸状态,释放进程的资源,获取进程退出信息
  2. 返回值: 成功返回被等待进程pid,失败返回-1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

void RunChild()
{
  int cnt = 2;
  while(cnt)
  {
    printf("i am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
    cnt--;
    sleep(1);
  }
}

int main()
{
  pid_t id = fork();
  if(id < 0)
  {
    perror("fork");
    return 1;
  }
  else if(id == 0)
  {
    RunChild();

    exit(0);
  }
  else 
  {
    //id > 0
    sleep(4)
    int ret = waitpid(-1, NULL, 0);
    if(ret == id)
    {
      printf("我是父进程,我等待子进程成功了, 子进程的pid: %d\n", ret);
    }
    else 
    {
      printf("等待子进程失败\n");
    }

    sleep(2);
  }

  return 0;
}

完整执行时间线:

  1. 0 秒:fork 创建父子进程
  2. 0~2 秒:子进程跑,打印 2 次
  3. 第 2 秒:子进程执行完毕,调用 exit(0)
    • 子进程变成 僵尸进程(Z)
    • 子进程资源释放,但 PCB 还在
  4. 2~4 秒:父进程还在 sleep,没回收子进程这段时间子进程是僵尸进程!
  5. 第 4 秒:父进程 sleep 结束
  6. 第 4 秒:父进程调用 wait(NULL)
    • 回收僵尸子进程
    • 打印等待成功
  7. 4~6 秒:父进程再 sleep (2),结束

waitpid(更强大:指定 PID、非阻塞)

让父进程等待指定的子进程,回收它,防止僵尸进程。

函数原型如下

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *status, int options);

pid:等待哪个子进程

  • pid == -1:等待任意子进程(等价 wait
  • pid > 0:等待指定 PID的子进程
  • pid == 0:等待同组的任意子进程
  • pid < -1:等待指定进程组的任意子进程

最常用:pid = -1 或 子进程 PID

status:输出子进程退出状态

wait 一样,用宏解析:

  • WIFEXITED(status):是否正常退出
  • WEXITSTATUS(status):获取退出码
  • WIFSIGNALED(status):是否被信号杀死
  • WTERMSIG(status):获取杀死信号

options:最关键!

  • 0阻塞等待(和 wait 一样)
  • WNOHANG非阻塞等待(没有子进程退出就立刻返回,不卡住)

返回值

  • > 0:成功回收的子进程 PID
  • = 0子进程还在运行(只有用 WNOHANG 才会返回)
  • -1:出错(没有子进程了)

现在我们就看看和普通wait最主要的区别,waitpid可以非阻塞等待

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

int main()
{
    pid_t id = fork();

    if (id == 0)
    {
        // 子进程
        printf("子进程运行,3秒后退出\n");
        sleep(3);
        exit(0);
    }

    // 父进程:非阻塞轮询
    while (1)
    {
        int status;
        // WNOHANG = 非阻塞
        pid_t ret = waitpid(id, &status, WNOHANG);

        if (ret == 0)
        {
            // 子进程还在跑
            printf("父进程:子进程还没退出,我继续干别的...\n");
            sleep(1);
        }
        else if (ret == id)
        {
            // 子进程退出了
            printf("父进程:检测到子进程退出,回收成功\n");
            break;
        }
    }

    return 0;
}

本次分享就到这里结束了,后续会继续更新,感谢阅读!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐