【unix高级编程系列】进程控制

在这里插入图片描述

引言

本文主要介绍unix系统的进程控制,其中包括创建进程、执行程序和进程终止。

进程标识

每一个进程都有一个非负整型表示的唯一进程ID,这也就是我们常说的进程ID。

注:虽然进程ID是唯一的,但是它也是可复用的。当一个进程终止后,其进程ID就成为复用的候选者。但是大多数unix系统使用延迟复用算法

系统中也会有一些专用进程,比如:

  • 进程ID 0 通常是调度进程
  • 进程ID 1 通常是init 进程。它负责在自举内核后启动一个UNIX系统,init进程绝不会终止。它是一个普通进程,但是以超级用户特权运行,还是所有孤儿进程的父进程
  • 进程ID 2 通常是页守护进程,此进程负责支持虚拟存储器系统的分页操作。

可通过下面接口获取进程ID:

#include <unistd.h>
//返回调用进程的ID
pid_t getpid(void);

//返回调用进程的父进程ID
pid_t getppid(void);

创建进程

一个现有的进程可以调用fork函数创建一个新的进程。

#include<unistd.h>
pid_t fork(void);
    // 返回值:子进程返回0,父进程返回子进程ID;若出错,返回-1

分析:为什么fork对父进程和子进程分别返回子进程ID和0?

  1. 对于父进程而言,它可以由多个子进程,并且没有相关接口获取指定进程的ID,因此需要在创建时刻获取子进程ID。
  2. 在第一点的基础上,若子进程返回父进程的ID,那么对于开发者而言,就区分不了哪一个是父进程,哪一个是子进程。所以用特殊值0表示子进程。若子进程需要获取父进程ID,可通过getppid接口。

子进程是父进程的副本:其中包括父进程数据空间、堆、栈的副本,并且父进程和子进程共享正文段

示例如下:

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

int32_t g_i32Var = 6;
int main()
{
    int32_t i32Var;
    pid_t pid;

    i32Var = 88;
    write(STDOUT_FILENO,"test fork\n",strlen("test fork\n"));
    printf("before fork\n");

    if((pid = fork()) < 0)
    {
        printf("fork failed\n");
    }
    else if(pid == 0)
    {
        g_i32Var++;
        i32Var++;
    }else{
        sleep(2);
    }

    printf("pid = %d , globvar = %d, var = %d\n",getpid(),g_i32Var,i32Var);

    return 0;
}

编译输出如下:

// 标准输出到终端
xieyihua@xieyihua:~/test$ gcc fork.c 
xieyihua@xieyihua:~/test$ ./a.out 
test fork
before fork
pid = 3635 , globvar = 7, var = 89
pid = 3634 , globvar = 6, var = 88
xieyihua@xieyihua:~/test$ 

// 标准输出重定向到文件
xieyihua@xieyihua:~/test$ ./a.out > 1
xieyihua@xieyihua:~/test$ cat 1 
test fork
before fork
pid = 3749 , globvar = 7, var = 89
before fork
pid = 3748 , globvar = 6, var = 88
xieyihua@xieyihua:~/test$ 

分析:

  • 标准输出到终端
  1. globvarvar的值输出可知,子进程会复制父进程的栈和数据段,并且修改并不会影响父进程;
  2. 在前面章节中,我们知道write是不带缓冲的,直接输出到标准输出;
  3. printf是带缓冲的标准I/O,因为标准输出为终端,因此为行缓冲,直接输出;
  • 标准输出重定向到文件

由于标准输出重定向到文件,那么printf就是全缓冲;因此内部流程如下:

  1. 父进程第一次输出before fork\n时,并没输出到终端,而是缓存在数据区
  2. 父进程fork之后,子进程的数据区也copy了一份befor fork\n
  3. 当子进程结束时,开始清空缓存区,会将所有的缓存进行输出。
  4. 当父进程结束时,开始清空缓存区,会将所有的缓存进行输出。

因此,该输出中,第一个字符串before fork实际是子进程输出的。这个你get到了吗?

思考:从上述现象中,我们可知:fork函数,会将父进程的所有打开文件描述符都被复制到子进程中。在【unix高级编程系列】文件I/O了解到linux 对共享文件的处理方式。而父进程和子进程的关系如下:

这就引发了一个新的问题:父进程和子进程共享一个文件表项,若不进行同步,则会将两者的输出混合(描述符在fork之前打开)

fork函数失败的两个主要原因有:

  1. 系统中已经有太多的进程。(常见场景:系统中存在大量的僵尸进程)
  2. 该实际用户ID的进程数超过了系统限制。可通过下述命令查看:
xieyihua@xieyihua:~$ cat /proc/sys/kernel/pid_max
4194304
xieyihua@xieyihua:~$ sysctl kernel.pid_max
kernel.pid_max = 4194304
xieyihua@xieyihua:~$

拓展: 现在很多的实现并不执行一个父进程数据段,栈和堆的完全副本。作为替代,使用了写时复制技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改为只读。如果父进程和子进程中的人一个试图修改这些区域,则内核只为修改区域的那块内存只做一个副本

进程退出

在上章节【unix高级编程系列】进程环境中,介绍了进程退出的8种方式(5种正常终止、3种异常终止方式):

  1. main函数内执行return语句。
  2. 调用exit函数。
  3. 调用_exit_Exit函数。
  4. 进程的最后一个线程在其启动列成中执行return语句。
  5. 进程的最后一个线程调用pthread_exit函数。
  6. 调用abort
  7. 进程收到某些信号时。
  8. 最后一个线程对取消请求做出响应。

不管进程如何终止,最后都期望执行内核中的同一段代码。这段代码的功能为相应进程关闭所有打开描述符,释放它所使用的存储器资源(PCB):进程ID、终止状态、以及该进程使用的CPU时间总量。

正常情况下,我们是期望父进程去执行上述操作,对子进程进行善后处理。流程:父进程调用waitwaitpid等待子进程结束,会释放上述资源。否则子进程的退出状态没有被获取,相关资源会一直保存在内核中,成为僵尸进程。

思考:由上可知,进程退出,其资源的回收时依赖父进程,否则会成为僵尸进程。那么若父进程在子进程之前终止,情况又是如何呢

答:unix 系统中,当一个进程终止时,内核会逐个检查所有活动进程,以判断它是否为即将终止进程的子进程。如果是,则将该进程的父进程ID更改为1(init 进程ID)。而init的实现逻辑:只要有一个子进程终止,init就会调用一个wait函数取得其终止状态,并进行资源回收。

总结:僵尸进程的产生原因:子进程退出,且父进程依旧运行,没有调用waitwaitpid系统调用来回收子进程的终止状态。

获取子进程终止状态

在上章节我们了解到父进程可以通过waitwaitpid获取子进程的终止状态,本章介绍如何使用,以及介绍如何获取子进程的相关资源信息。

#include<sys/wait.h>

pid_t wait(int* statloc);

pid_t waitpid(pid_t pid,int* statloc,int option);
                //两个函数返回值:若成功,返回进程ID;若出错,返回0。

区别如下:

  • 没有任何一个子进程终止前,wait使其调用者阻塞,而waitpid有一选项,可使调用者不阻塞。
  • waitpid可通过入参,控制调用进程。比如:
pid == -1 : 等待任一子进程退出,此时与wait函数等效
pid > 0 等待进程ID与pid相等的子进程。即使不是父子进程关系
pid == 0 等待组ID等于调用进程组ID的任一子进程
pid < -1 等待组ID等于oid绝对值的任一子进程

获取进程终止状态的常见用法如下:

  • 场景一:
    使用wait()系统调用: wait()系统调用会使父进程阻塞,直到任一子进程终止。当子进程终止时,wait()会回收子进程,并返回终止子进程的进程ID。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>

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

    if (pid > 0) {
        // 父进程
        int status;
        pid_t child_pid = wait(&status); // 等待子进程退出
        if (child_pid > 0) {
            printf("子进程 %d 已退出,状态: %d\n", child_pid, status);
        }
    } else if (pid == 0) {
        // 子进程
        printf("子进程开始执行...\n");
        sleep(1); // 模拟子进程工作
        printf("子进程结束。\n");
        exit(0); // 子进程退出
    } else {
        // fork失败
        perror("fork");
        exit(1);
    }

    return 0;
}
  • 场景二:使用waitpid()系统调用: waitpid()系统调用类似于wait(),但它允许父进程指定等待哪个子进程,以及是否阻塞等待。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>

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

    if (pid > 0) {
        // 父进程
        int status;
        // 等待特定的子进程退出,这里使用WUNTRACED | WCONTINUED以捕获停止或继续的子进程
        pid_t child_pid = waitpid(pid, &status, 0);
        if (child_pid > 0) {
            printf("子进程 %d 已退出,状态: %d\n", child_pid, status);
        }
    } else if (pid == 0) {
        // 子进程
        printf("子进程开始执行...\n");
        sleep(1); // 模拟子进程工作
        printf("子进程结束。\n");
        exit(0); // 子进程退出
    } else {
        // fork失败
        perror("fork");
        exit(1);
    }

    return 0;
}

对于场景一,在子进程未终止前,父进程会一直阻塞,导致无法执行后续业务。若应用场景,父进程并不关注子进程的终止状态,仅为了避免产生僵尸进程,可通过信号处理:父进程可以设置一个信号处理函数来处理SIGCHLD信号,该信号在子进程退出时由内核发送给父进程。

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

void sigchld_handler(int sig) {
    int status;
    pid_t child_pid = wait(&status);
    if (child_pid > 0) {
        printf("子进程 %d 已退出,状态: %d\n", child_pid, status);
    }
}

int main() {
    signal(SIGCHLD, sigchld_handler); // 设置SIGCHLD的处理函数

    pid_t pid = fork();

    if (pid > 0) {
        // 父进程
        // ... 父进程可以继续其他工作
    } else if (pid == 0) {
        // 子进程
        printf("子进程开始执行...\n");
        sleep(1); // 模拟子进程工作
        printf("子进程结束。\n");
        exit(0); // 子进程退出
    } else {
        // fork失败
        perror("fork");
        exit(1);
    }

    return 0;
}

无论是wait还是waitpid只能获取进程的终止状态。实际上内核为终止进程还保存了其它信息,比如:系统资源信息,用户CPU时间总量、系统CPU时间总量、缺页次数、收到信号的次数等。我们可以通过wait4函数获取。

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait4(pid_t pid, int* statloc,int options,struct rusage* rusage);

使用示例:

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

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

    if (pid == -1) {
        // fork失败
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid > 0) {
        // 父进程
        int status;
        struct rusage usage;
        pid_t child_pid;

        // 等待子进程退出,并获取资源使用情况
        child_pid = wait4(pid, &status, 0, &usage);
        if (child_pid == -1) {
            perror("wait4");
            exit(EXIT_FAILURE);
        }

        // 检查子进程是否正常退出
        if (WIFEXITED(status)) {
            printf("子进程 %d 已退出,退出状态: %d\n", child_pid, WEXITSTATUS(status));
        }

        // 打印资源消耗
        printf("用户态CPU时间: %ld.%06ld秒\n", usage.ru_utime.tv_sec, usage.ru_utime.tv_usec);
        printf("核心态CPU时间: %ld.%06ld秒\n", usage.ru_stime.tv_sec, usage.ru_stime.tv_usec);
        printf("最大驻留集大小: %ld 千字节\n", usage.ru_maxrss);
        printf("页面错误次数: %ld\n", usage.ru_majflt);
        printf("自愿上下文切换次数: %ld\n", usage.ru_nvcsw);
        printf("非自愿上下文切换次数: %ld\n", usage.ru_nivcsw);
    } else {
        // 子进程
        printf("子进程开始执行...\n");
        // 子进程做一些工作,这里只是简单地睡眠一段时间
        sleep(2);
        printf("子进程结束。\n");
        exit(EXIT_SUCCESS); // 子进程正常退出
    }

    return 0;
}

输出如下:

xieyihua@xieyihua:~/test$ gcc 3.c -o 3
xieyihua@xieyihua:~/test$ ./3 
子进程开始执行...
子进程结束。
子进程 5472 已退出,退出状态: 0
用户态CPU时间: 0.001042秒
核心态CPU时间: 0.000000秒
最大驻留集大小: 1028 千字节
页面错误次数: 0
自愿上下文切换次数: 2
非自愿上下文切换次数: 0
xieyihua@xieyihua:~/test$ 

总结

文章主要介绍了Unix系统中进程控制的相关知识,包括进程标识、创建进程、进程退出以及获取子进程终止状态的方法。

  1. 进程标识。
    每个进程都有一个唯一的进程ID(PID),用于标识系统中的进程。PID是可复用的,当一个进程终止后,其PID就成为复用的候选者。
  2. 创建进程。fork函数是用来创建新进程的。父进程通过fork创建子进程,子进程是父进程的副本,包括数据空间、堆、栈的副本,并且父进程和子进程共享正文段。
  3. 进程退出。进程有多种方式退出,包括正常退出(如return语句、exit函数)和异常退出(如信号处理)。正常情况下,父进程应该调用wait或waitpid来回收子进程的终止状态,否则子进程的退出状态没有被获取,相关资源会一直保存在内核中,成为僵尸进程。
  4. 获取子进程终止状态。父进程可以通过wait、waitpid和wait4系统调用获取子进程的终止状态。wait和waitpid可以获取进程的终止状态,而wait4还可以获取进程的资源使用情况。

若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途
在这里插入图片描述

在这里插入图片描述

  • 11
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢艺华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值