【进程 】

进程

目录

1. 进程的创建

c 复制代码
#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);
主要功能

将当前的进程复制一份,然后这两个进程同时从本函数的下一语句开始执行。

接口解析

所有的代码、变量都会复制成两份。该函数会返回两次,一次返回父进程,值是子进程的PID,一次返回子进程,值固定为0。父子进程是并发执行的,没有先后次序,若要控制次序,要依赖于信号量、互斥锁、条件量等其他条件。

示例代码
c 复制代码
#include <stdio.h> 
#include <unistd.h> 

int main()
{
    printf("[]fork之前\n");

    pid_t pid = fork();
    // 以上函数执行成功后
    // 父子进程都将从下面的语句开始执行,不分先后

    // 以下语句会被执行两遍
    // 在父进程中,pid将是子进程的PID
    // 在子进程中,pid将是0
    printf("[%d]: pid=%d\n", getpid(), pid);
    // 让父进程等待子进程输出
    if (pid > 0) {
        wait(NULL);
    }
}
程序执行结果
plaintext 复制代码
shaseng@ubuntu:$ ./a.out
[5140]: fork之前
[5141]: pid=0
[5140]: pid=5141
shaseng@ubuntu:$ 
结果说明

在执行fork()函数之前,printf("[]fork之前\n");代码只执行一遍,并且是父进程[5140]在执行它。在执行fork()函数之后,进程分裂成两个,因此printf("[%d]: pid=%d\n", getpid(), pid);代码被执行了两遍。函数getpid()的功能是获取自身进程的PID,在程序该行,父进程和子进程分别输出了自己的PID,一个是5140,一个是5141。在5140那边,输出的pid5141,于是我们得知5140必然是父进程,因为只有父进程才能获取一个大于零的子进程的PID。在5141那边,输出的pid0,于是我们得知5141必然是子进程,因为只有子进程才会从fork()的返回值中获取一个0。通过在父进程中调用wait(NULL);,让父进程等待子进程执行完毕,避免了bash的命令行信息穿插到父子进程中间。

2. 进程的回收

c 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);
主要功能

阻塞当前进程,等待其子进程退出并回收其系统资源。

接口解析

如果当前进程没有子进程,则该函数立即返回。如果当前进程有不止1个子进程,则该函数会回收第一个变成僵尸态的子进程的系统资源。子进程的退出状态(包括退出值、终止信号等)将被放入wstatus所指示的内存中,若wstatus指针为NULL,则代表当前进程放弃其子进程的退出状态。

示例代码
c 复制代码
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <sys/wait.h> 

int main()
{
    if(fork() == 0)
    {
        printf("[%d]: 我将在3秒后正常退出,退出值是88\n", getpid());

        for(int i=3; i>=0; i--)
        {
            fprintf(stderr, " ======= %d =======%c", i, i==0?'\n':'\r');
            sleep(1);
        }

        exit(88);
    }

    else
    {
        printf("[%d]: 我正在试图回收子进程的资源...\n", getpid());

        int status;
        wait(&status);

        if(WIFEXITED(status))
        {
            printf("[%d]: 子进程正常退出了,其退出值是:%d\n", getpid(), WEXITSTATUS(status));
        }
    }
}
执行结果
plaintext 复制代码
shaseng@ubuntu:$ ./a.out
[3611]: 我正在试图回收子进程的资源...
[3612]: 我将在3秒后正常退出,退出值是88
 ======= 0 =======
[3611]: 子进程正常退出了,其退出值是:88
shaseng@ubuntu:$ 
代码解析

status用来存放子进程的退出状态,status包含了子进程退出的诸多信息,而不仅仅是退出值,因此父进程如果要获取这些信息,需要用以下宏对status进行解析:

功能
WIFEXITED(status) 判断子进程是否正常退出
WEXITSTATUS(status) 获取正常退出的子进程的退出值
WIFSIGNALED(status) 判断子进程是否被信号杀死
WTERMSIG(status) 获取杀死子进程的信号的值
waitpid函数
c 复制代码
#include <sys/types.h>
#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *wstatus, int options);
wait()的区别

可以通过参数pid用来指定想要回收的子进程,可以通过options来指定非阻塞等待。pidoptions这两个参数的取值和作用详见下表:

pid 作用 options 作用
< -1 等待组ID等于pid绝对值的进程组中的任意一个子进程 0 阻塞等待子进程的退出
-1 等待任意一个子进程 WNOHANG 若没有僵尸子进程,则函数立即返回
0 等待本进程所在的进程组中的任意一个子进程 WUNTRACED 当子进程暂停时函数返回
> 0 等待指定pid的子进程 WCONTINUED 当子进程收到信号SIGCONT继续运行时函数返回

注意:options的取值,可以是0,也可以是上表中各个不同的宏的位或运算取值。

3. 加载并执行指定程序

c 复制代码
#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...);

int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
主要功能

给进程加载指定的程序,如果成功,进程的整个内存空间都被覆盖。

接口解析

执行指定程序之后,会自动获取原来的进程的环境变量。各个后缀字母的含义:

  • l : list 以列表的方式来组织指定程序的参数
  • v : vector 矢量、数组,以数组的方式来组织指定程序的参数
  • e : environment 环境变量,执行指定程序前顺便设置环境变量
  • p : 专指PATH环境变量,这意味着执行程序时可自动搜索环境变量PATH的路径

这组函数只是改变了进程的内存空间里面的代码和数据,但并未改变本进程的其他属性。

注意事项

execl(const char *path, const char *arg, ...)为例,参数path是需要加载的指定程序,而arg则是该程序运行时的命令行参数,值得注意的是,命令行参数包括程序名本身,并且全部是字符串。例如:

plaintext 复制代码
shaseng@ubuntu:$ ./a.out 123 abc

上述命令用execl来指定则是:

c 复制代码
execl("./a.out", "./a.out", "123", "abc", NULL);

这其中:第一个./a.out是程序本身,第二个./a.out是第一个参数。参数列表以NULL结尾。

示例代码
c 复制代码
// child.c
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <sys/wait.h> 

int main(int argc, char **argv)
{
    // 倒数 n 秒
    for(int i=atoi(argv[1]); i>0; i--)
    {
        printf("%d\n", i);
        sleep(1);
    }

    // 程序退出,返回 n
    exit(atoi(argv[1]));
}

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

int main()
{
    // 子进程
    if(fork() == 0)
    {
        printf("加载新程序之前的代码\n");

        // 加载新程序,并传递参数3
        if (execl("./child", "./child", "3", NULL) == -1) {
            perror("execl failed");
            exit(EXIT_FAILURE);
        }

        printf("加载新程序之后的代码\n");
    }

    // 父进程
    else
    {
        // 等待子进程的退出
        int status;
        int ret = waitpid(-1, &status, 0);

        if(ret > 0)
        {
            if(WIFEXITED(status))
                printf("[%d]: 子进程[%d]的退出值是:%d\n",
                        getpid(), ret, WEXITSTATUS(status));
        }
        else
        {
            printf("暂无僵尸子进程\n");
        }
    }
}
程序运行结果
plaintext 复制代码
shaseng@ubuntu:$ gcc child.c -o child
shaseng@ubuntu:$ gcc main.c -o main
shaseng@ubuntu:$ ./main
加载新程序之前的代码
3
2
1
[5634]: 子进程[5635]的退出值是:3
shaseng@ubuntu:$ 
程序解析

子进程中加载新程序之后的代码无法运行,因为已经被覆盖了。waitpid()中指定了options的值为0,意味着阻塞等待子进程,效果跟直接调用wait()相当。

「课堂练习」

练习 2
c 复制代码
#include <stdio.h> 
#include <unistd.h> 
#include <sys/wait.h> 

int main()
{
    printf("[]fork之前\n");

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("[%d]: pid=%d\n", getpid(), pid);
    } else {
        // 父进程
        int status;
        wait(&status);
        printf("[%d]: pid=%d\n", getpid(), pid);
    }
    return 0;
}
练习 3
c 复制代码
// child.c
#include <stdio.h> 
#include <stdlib.h> 
#include <unistd.h> 
#include <sys/wait.h> 

int main(int argc, char **argv)
{
    // 倒数 n 秒
    for(int i=atoi(argv[1]); i>0; i--)
    {
        printf("%d\n", i);
        sleep(1);
    }

    // 程序退出,返回 n
    exit(atoi(argv[1]));
}

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

int main()
{
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("加载新程序之前的代码\n");
        if (execl("./nonexistent_child", "./nonexistent_child", "3", NULL) == -1) {
            perror("execl failed");
            exit(EXIT_FAILURE);
        }
        printf("加载新程序之后的代码\n");
    } else {
        // 父进程
        // 模拟父进程先退出
        printf("[%d]: 父进程即将退出\n", getpid());
        return 0;
    }
    return 0;
}

在这个练习中,子进程尝试加载一个不存在的程序,会导致execl失败并输出错误信息。父进程先于子进程退出,子进程会成为孤儿进程,被init进程(systemd)收养。

相关推荐
bugtraq202110 分钟前
XiaoMi Mi5(gemini) 刷入Ubuntu Touch 16.04——安卓手机刷入Linux
linux·运维·ubuntu
CodeWithMe1 小时前
[ Vim ] 常用命令 and 配置
linux·编辑器·vim
DC_BLOG1 小时前
Linux-GlusterFS进阶分布式卷
linux·运维·服务器·分布式
cookies_s_s2 小时前
Linux--进程(进程虚拟地址空间、页表、进程控制、实现简易shell)
linux·运维·服务器·数据结构·c++·算法·哈希算法
丁劲犇2 小时前
碳基生物的悲歌-DeepSeek思考实现Linux动态库递归收集工具
linux·递归·deepseek·ldd
zhouwu_linux2 小时前
MT7628基于原厂的SDK包, 修改ra1网卡的MAC方法。
linux·运维·macos
2401_897930062 小时前
linux系统如何配置host.docker.internal
linux·docker·eureka
诶尔法Alpha3 小时前
Linux上使用dify构建RAG
linux·运维·服务器
熬夜苦读学习3 小时前
Linux文件系统
linux·运维·服务器·开发语言·后端
沐千熏4 小时前
Liunx(CentOS-6-x86_64)系统安装MySql(5.6.50)
linux·mysql·centos