什么是进程?

进程是正在运行的程序,是操作系统进行资源分配和调度的基本单位,程序是存储在硬盘或内存的一段二进制序列,是静态的,而进程是动态的,进程包括代码,数据以及分配给它的其他系统资源(如文件描述符,网络连接等)。

我们打开的VMWare、开启的浏览器都对应操作系统的一个进程。
在Linux中,进程是程序的一次执行实例,每个进程由内核通过PCB(Process Control Block) 管理,包含PID,状态,内存映射,文件描述符表等信息。

本篇我们来学习四个问题:

(1)如何启动一个新程序?

(2)如何在当前程序中并发执行多个任务?

(3)子进程结束后,父进程如何获知其退出状态?

(4)如果父进程先退出,子进程会怎样?

我们先来简单介绍几个函数的应用

——————fork函数的使用——————

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

int main(int argc, char const *argv[])
{
    //调用fork之前,代码都在父进程中运行,

    //子进程复制父进程的地址空间(包括代码、数据、堆栈、文件描述符等)

    printf("海哥教老学员%d春暖花开\n",getpid());
    //使用fork创建子进程
    /**
     * 不需要传参,调用一次,返回两次
     * return:int 进程号
     *      (1):-1出错
     *      (2):父进程中表示子进程的pid
     *      (3):子进程中显示为0
     * __pid_t fork (void)
    */
    pid_t pid = fork();

    //从fork之后  所有的代码都是在父子进程中各执行一次的
    if (pid < 0)
    {
        printf("新学员加入失败\n");
        exit(EXIT_FAILURE);
    }else if(pid == 0)
    {
        //执行单独子进程代码
        printf("新学员%d加入成功,他是老学员%d推荐的\n",getpid(),getppid());
    }else{
        //执行单独父进程代码
        printf("老学员%d继续深造,他推荐了%d\n",getpid(),pid);
    }
    return 0;
}

——————运行日志内容——————

海哥教老学员17042春暖花开
老学员17042继续深造,他推荐了17043
新学员17043加入成功,他是老学员17042推荐的

通过这个函数我们可以知道,可以使用fork函数来复制当前进程的地址空间(包括代码、数据、堆栈、文件描述符等)然后创建新的子进程,这个函数的返回值有三个,返回-1代表出错,返回零代表执行子进程的代码,返回当前进程代表执行父进程的代码

#include <sys/types.h>

#include <unistd.h>

getpid()这个用来获取当前进程PID

getppid()这个用来获取当前进程的父进程PID

————————fork函数与文件描述符fd的配合使用————————

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

int main(int argc, char const *argv[])
{
    //调用fork之前
    //打开一个文件
    int fd = open("io.txt",O_CREAT | O_WRONLY | O_APPEND,0644);

    if(fd == -1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }
    char buffer[1024];//缓冲区存放读写的数据

    pid_t pid = fork();
    if (pid < 0)
    {
        printf("新学员加入失败\n");
        exit(EXIT_FAILURE);
    }else if(pid == 0)
    {
        //执行单独子进程代码
        strcpy(buffer,"这是子进程写入的数据\n");
    }else{
        //执行单独父进程代码
        sleep(1);
        strcpy(buffer,"这是父进程写入的数据\n");
    }

    //父子进程都要执行的代码
    ssize_t bytes_write = write(fd,buffer,strlen(buffer));
    if(bytes_write == -1)
    {
        perror("write");
        close(fd);
        exit(EXIT_FAILURE);
    }
    printf("写入成功\n");

    close(fd);

    if(pid == 0)
    {
        printf("子进程写入完毕,并释放文件描述符\n");
    }else{
        printf("父进程写入完毕,并释放文件描述符\n");
    }

    return 0;
}

————————io.txt————————

这是子进程写入的数据
这是父进程写入的数据
 

————————日志运行内容————————

写入成功
子进程写入完毕,并释放文件描述符
写入成功
父进程写入完毕,并释放文件描述符

通过日志我们发现父进程和子进程都写入了数据,成功演示了父子进程共享文件描述符的示例,

也就是说父进程打开一个文件,然后fork()创建子进程,父子进程各子向同一个文件描述符写入不同内容,最后各自关闭该描述符。

open函数是在fork之前调用的,此时只有父进程存在,为什么运行的结果显示子进程也写进数据去了呢,因为在fork之后子进程会复制父进程的整个文件描述符表,也就是我们上个代码的结论,在fork之后子进程会复制父进程的地址空间(包括代码、数据、堆栈、文件描述符等),所以此时子进程的fd = 3也指向同一个内核打开文件表项,内核将该表项的引用计数+1变成了2,因为我使用了O_APPEND,所以父子进程都是分别写入的,不会被互相覆盖

  • 子进程 close(fd) → 引用计数 2 → 1不释放内核资源;
  • 父进程 close(fd) → 引用计数 1 → 0内核释放该打开文件表项;
  • 此时文件才真正“关闭”。
     

也就是说刚开始的时候父进程创建三号指向这个结构体,然后他的文件描述符的引用计数是1,当fork创建一个子进程的时候,首先文件描述符的这个表他会原原本本的复制一份,复制完成之后,他就会说我这边也引用了这个对应的文件描述符,所以呢他会指向这个内核,在这个内核里面他会把文件描述符的引用计数加一,子进程调close的时候减一,因为父进程还在引用他,不为零就不会被释放,当父进程也close减一,为零了,他就会被释放调,也就被从内核当中删除掉了(图片来自b站up主尚硅谷,讲的非常好)

————————execve函数的使用————————

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

int main(int argc, char const *argv[])
{
    //跳转之前
    char *name = "banzhang";
    printf("我是%s,编号%d,我现在还在一楼\n",name,getpid());

    //执行跳转
    /**
     * const char *__path:执行程序的路径
     * char *const __argv[]:传入的参数-> 对应执行程序main方法的第二个参数
     * (1):第一个参数固定是程序的名称->执行程序的路径
     * (2):执行程序需要传入的参数
     * (3):最后一个参数一定是NULL
     * char *const __envp[]:传递的环境变量
     * (1):环境变量参数:key = value
     * (2):最后一个参数一定是NULL
     * return:成功根本没办法返回,下面的代码也没有意义,失败返回-1
     * 跳转前后只有进程号保留下来,别的变量都删除了
     * int execve (const char *__path, char *const __argv[],char *const __envp[])
    */
    char *args[] = {"/home/sxf/process_test/erlou",name,NULL};
    char *envs[] = {"PATH=/home/sxf/bin:/usr/local/sbin:/usr/local/bin:/usr\
        /sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap\
        /bin:/snap/bin",NULL};
    int re = execve(args[0],args,envs);
    if (re == -1)
    {
        printf("你没有机会上二楼\n");
        exit(EXIT_FAILURE);
    }
    
    //此处代码没有意义,因为程序跳转了,不会再往下执行了
    return 0;
}

————————erlou.c————————

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    if(argc < 2)
    {
        printf("参数不够不能上二楼\n");
        return 1;
    }
    printf("我是%s,编号%d,我跟海哥上二楼啦\n",argv[1],getpid());
    return 0;
}

————————运行日志的内容————————

我是banzhang,编号17122,我现在还在一楼
我是banzhang,编号17122,我跟海哥上二楼啦

通过程序我们看到,这个函数的用法是从当前程序跳转到另一个程序erlou.c

这里我们特别介绍一下这个函数的参数,第一个参数是要执行的程序的路径,在这里我传的是绝对路径,第二个参数是传给新程序的参数数组,第三个参数是环境变量,如果你第一个参数给的是绝对路径的话,其实这个环境变量数组可以只填NULL,如何获取PATH路径呢,你只需要在你终端输入$PATH输出的结果就是了

接下来让我们将fork函数和execve函数配合使用

————————fork和execve配合使用——————

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

int main(int argc, char const *argv[])
{
    //邀请之前
    char * name = "老学员";

    printf("%s %d继续在一楼精进\n",name,getpid());
    //邀请新学员
    __pid_t pid = fork();
    if(pid == -1)
    {
        printf("邀请新学员失败\n");
        exit(EXIT_FAILURE);
    }else if(pid == 0)
    {
        //新学员在这
        char *new_name = "ergou";
        char *args[] = {"/home/sxf/process_test/erlou",new_name,NULL};
        char *envs[] = {NULL};
        int exR = execve(args[0],args,envs);
        if(exR == -1)
        {
            printf("新学员上二楼失败\n");
            exit(EXIT_FAILURE);
        }
        //新学员上二楼成功  此处代码不执行
    }else
    {
        //老学员在这
        printf("老学员%d,邀请完新学员%d之后,还是在一楼学习\n",getpid(),pid);
    }

    printf("11111");
    return 0;
}

————————erlou.c————————

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    if(argc < 2)
    {
        printf("参数不够不能上二楼\n");
        return 1;
    }
    printf("我是%s,编号%d,我跟海哥上二楼啦\n",argv[1],getpid());
    return 0;
}

————————运行日志内容————————

老学员 17464继续在一楼精进
老学员17464,邀请完新学员17465之后,还是在一楼学习

11111
我是ergou,编号17465,我跟海哥上二楼啦

我们根据这个日志的运行内容可以看到,我们调用了fork函数创建了一个新的子进程,然后在这个新的子进程中呢又调用execve函数跳转到另一个程序erlopu.c中,这就实现了父进程创建出一个子进程,然后子进程去运行另一个程序,父进程自己保持不变。

——————system函数的使用——————

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{
    /**
     * 使用标准库函数创建子进程
     * const char *__command:使用Linux命令直接创建一个子进程
     * return:成功返回0,失败返回编号
     * int system (const char *__command)
    */
    int sysR = system("ping -c 100 www.baidu.com");
    if (sysR != 0)
    {
        perror("system");
        exit(EXIT_FAILURE);
    }
    return 0;
}

system这个函数可以用在程序中调用外部命令,他的参数是一个以NULL结尾的字符串,表示要执行的shell命令,这个函数启动了一个 shell(通常是 /bin/sh),让 shell 去解释并执行传入的命令字符串。

Linux中父进程除了可以启动子进程,还要负责回收子进程的状态,如果子进程结束后父进程没有正常回收,那么子进程就会变成一个僵尸进程------即程序执行完成,但是进程没有完全结束,其内核中PCB结构体没有释放。

————————waitpid函数的使用————————

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

int main(int argc, char const *argv[])
{
    //fork之前
    int subprocrss_status;
    printf("老学员在校区\n");

    __pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
        return 1;
    } else if(pid == 0)
    {
        //新学员
        char *args[] = {"/usr/bin/ping","-c","3","www.baidu.com",NULL};
        char *envs[] = {"PATH=/home/sxf/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin",NULL};
        printf("新学员%d联系海哥20次\n",getpid());
        int exR = execve(args[0],args,envs);
        if (exR < 0)
        {
            perror("execve");
            return 1;
        }
        
    }else {
        //老学员
        printf("老学员%d等待新学员%d联系\n",getpid(),pid);
        waitpid(pid,&subprocrss_status,0);
    }
    printf("老学员等待新学员联系完成\n");
    
    
    return 0;
}

————————日志运行内容————————

老学员在校区
老学员18041等待新学员18042联系
新学员18042联系海哥20次
PING www.a.shifen.com (39.156.70.46) 56(84) bytes of data.
64 bytes from 39.156.70.46 (39.156.70.46): icmp_seq=1 ttl=128 time=41.6 ms
64 bytes from 39.156.70.46 (39.156.70.46): icmp_seq=2 ttl=128 time=74.1 ms
64 bytes from 39.156.70.46 (39.156.70.46): icmp_seq=3 ttl=128 time=53.7 ms

--- www.a.shifen.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 41.599/56.449/74.059/13.394 ms
老学员等待新学员联系完成

我们来看一下waitpid这个函数的参数:

#include <sys/wait.h>

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

第一个参数是指定等待哪个子进程:

pid

含义

> 0

等待指定 PID 的子进程(最常用)<br>例:waitpid(1234, &status, 0)

-1

等待任意子进程(等价于 wait()

0

等待同进程组的任意子进程

< -1

等待进程组 ID = |pid| 的任意子进程

第二个参数是接收子进程退出状态,如果不关心状态,可以传NULL

第三个参数是控制等待行为:

选项

说明

0

阻塞等待:直到指定子进程结束才返回(最常用)

WNOHANG

非阻塞:若子进程未结束,立即返回 0(用于轮询)

WUNTRACED

也报告暂停(stopped) 的子进程(如被 SIGSTOP 暂停)

WCONTINUED

报告从暂停恢复的子进程(需 _GNU_SOURCE

用waitpid等待子进程完成再去接着运行父进程就能有效避免僵尸进程的产生。

最后在总结一下僵尸进程和孤儿进程的区别:

特性

僵尸进程(Zombie Process)

孤儿进程(Orphan Process)

定义

子进程已终止(exit),但父进程尚未调用 wait/waitpid 回收其状态

父进程先于子进程退出,子进程仍在运行

进程状态

已结束,不占用 CPU/内存,但内核 PCB 未释放

仍在正常运行,占用系统资源

存在位置

进程表中保留(PID 仍被占用)

进程表中正常存在,可执行代码

危害

占用 PID 资源(大量僵尸可耗尽 PID 空间)

无危害(会被 init/systemd 收养)

如何产生

父进程 fork 子进程后,子进程 exit,但父进程不 wait

父进程先 exit,子进程还在运行

如何解决

父进程必须调用 wait/waitpid

无需手动处理,系统自动收养

ps 显示

状态为 ZZ+,命令名后带 <defunct>

状态为 S+R+ 等,PPID = 1

生命周期

直到父进程 wait 或父进程退出(此时转为孤儿,被 init wait)

被 init(PID=1)或 systemd 收养,成为其子进程

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐