前言

进程控制是 Linux 系统编程的核心基石,直接决定了程序如何创建、终止、等待子进程,以及如何实现多任务协作。理解进程终止的场景、进程等待的意义、程序替换的原理,不仅能解决僵尸进程、资源泄漏等实际问题,更能让你掌握编写多进程程序、模拟 shell 等高级功能的底层逻辑。

本文将从进程终止的三种场景与退出方法入手,深入剖析进程等待(wait/waitpid)的实现机制与核心作用,详解 exec 函数族的程序替换原理与用法,最终通过完整代码实现简易 shell,串联所有知识点。全文兼顾理论深度与实操性,每个核心接口都配套示例代码,文末还附上高频面试习题及解析,帮助你夯实基础、检验学习成果。无论你是刚接触 Linux 多进程编程的初学者,还是想深耕底层的开发者,都能通过本文彻底理清进程控制的完整逻辑链,实现从 “理解原理” 到 “实战应用” 的跨越。

进程终止

进程退出的场景

代码运行完毕,结果正确 --退出码为0

代码运行完毕,结果不正确 – 退出码为非0,具体是多少要看是什么问题

代码异常终止 --比如整数以0,此时就是异常终止–这个时候的退出码不重要了–因为代码都运行不完

进程出现异常,本质是进程收到了对应的信号

父进程会关心子进程运行的情况

查询退出码的办法:$?–里面有最近一次进程退出时的退出码

eg: echo $?

C标准库里面有官方的退出码:errno,然后可以用strerror(errno)转换成字符串形式的错误描述

当系统调用(如openreadwrite等)或库函数(如mallocfopen等 )调用失败时,errno会被设置为对应的错误码

用法:
 int* ptr = (int*)malloc(1000*1000*1000*4);
    if (ptr == NULL) 
        printf("error: %d - %s\n", errno, strerror(errno));

注意:strerror不只是只能识别errno的错误码哈

1

当然,也可以自己设计一套自己的退出码体系这样

进程常见的退出方法

1.return 2.exit 3._exit

区别:

exitreturn的区别:在main函数里面,returnexit一样 但是在调用的函数里面,exit是退出当前进程,return是结束这个函数

exit_exit的区别:exit在退出进程前会刷新缓冲区,关闭流,执行用户定义的清理函数,但是_exit不会

在这里插入图片描述

eg: 用法: return 0;   exit(0);    _exit(0);

引申:标准输出流会先把数据写入缓冲区中,合适的时候再进行刷新

–由上图可知,缓冲区肯定不在内核中

进程等待

进程等待:通过系统调用wait/waitpid,来对子进程进行状态检测与回收的功能

–如果没有这个,子进程的僵尸状态将一直保持(除非等到init自动调用wait)

进程等待的意义:

1.僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏问题

2.父进程需要通过进程等待来获得子进程的退出情况(父进程可能关心,可能不关系哈)

通过进程等待可以保证父进程是多个进程里面最后一个退出的进程

然后进程等待有两种方法:一种是wait,一种是waitpid

父进程只能等待自己的子进程哈–等待子进程的子进程或者其他人的进程都是不行的

引申:子进程结束,也不是需要立马就去回收哈

问题:父进程要拿子进程的状态数据,为什么必须要用wait等系统调用?

–因为进程具有独立性

进程等待的作用机理:

子进程执行完毕后,其PCB里面还存储着退出信息,父进程调用wait这些时,就会让内核去查找子进程的PCB,读取退出信息到status

wait

wait是等待到任意一个子进程退出时就释放那个子进程(子进程的状态会从Z变成X)

想让wait回收多个进程的话就搞个循环

pid_t wait(int*status);
头文件是:
#include <sys/types.h>
#include <sys/wait.h>

用法eg: pid_t ret = wait(NULL);

返回值是pid_t类型的(Linux下其实就是int),成功的话就返回那个子进程的pid,失败就返回-1

这个status是输出型参数,不关心子进程退出信息的话就传个NULL

目前的话只研究status的低16个比特位

低7位存的是终止信号是啥–0的话表示没有异常

在这里插入图片描述

低第8位是core dump状态

剩下那8位是退出码–可以通过退出码看程序有无出错以及出错的原因

在这里插入图片描述

这个status还有两个宏:(变成其他变量名也行哈)

WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

引申:

在这里插入图片描述

这个<defunct>也表示这个进程正在僵尸状态

waitpid

pid_ t waitpid(pid_t pid, int *status, int options);
头文件:
#include <sys/types.h>
#include <sys/wait.h>

返回值是pid_t类型,为0表示等待的条件还没有就绪,不等了;<0表示失败,>0返回的是子进程的pid

status跟上面的那个一样

这个pid的话,如果填-1,就是等待任一一个子进程;填>0的数的话,就是等待ID跟这个pid相等的子进程

这个options的话,可以填WNOHANG:这样的话就表示非阻塞轮询+父进程可以干自己的事(但是活不能太重) --不想这样的话就填0

引出阻塞:
如果子进程还没退出,父进程在wait的时候此时叫做阻塞状态--父进程也不会去干其他事

进程程序替换

概念:就是在程序运行到程序替换的时候,直接去执行另一个程序了,这个程序的后续将不再执行

–进程还是原来那个进程

–程序替换只进行进程的程序代码和数据的替换

–在程序替换中,环境变量也是原来的(除非用的execleexecvpe)

子进程进行程序替换,是不会影响父进程的

Linux中的可执行程序是ELF格式的 可执行程序的入口地址在那个ELF的表头

用于程序替换的6个函数

exec函数只有出错的返回值(-1),没有成功的返回值

–因为函数调用成功的话就去执行新程序了,不再返回

这里列的是函数调用接口–当然还要系统调用接口eg:execve

#include <unistd.h>//头文件

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
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[]);
//cahr*后面的const表示东西写进去就不能变了

这几个函数统称为exec函数

命名的记忆:
带l是参数采用可变参数列表的形式  带v是参数采用的数组方式 --这里要填的其实就是命令行上填的东西(把换行换成NULL就行了)
带e是自己传环境变量  带p是文件去PATH里面找

参数的含义:
带envp[]的要自己导入环境变量:可以传系统的eg:environ(记得extern声明)
path的话表示要传路径进去 file的话写文件名就行,会去环境变量PATH里面找
argv[]的话表示命令行参数用数组传过去

注意:这里的不管是可变参数列表还是数组的方式:最后那个都得是NULL
eg: char *const argv[] = {"ls","-a","-l",NULL};
用法:
execl("/user/bin/ls","ls","-a","-l",NULL);
execlp("ls","ls","-a","-l",NULL);
execle("/user/bin/ls","ls","-a","-l",NULL,environ);
execv("/bin/ps", argv);//argv是数组

注意:execlp("./otherExe","./otherExe",NULL);
     execlp("./otherExe","otherExe",NULL);   也行
只要第一个参数传对,第二个参数传的是路径也行--
带p的传路径也是可以的

程序替换时想给子进程传递增多的环境变量的方法:

1.父进程的地址空间里面putenv

2.彻底替换–eg:execle(这里填的环境变量不是追加!!!)

引申:C++的源文件常见后缀:.cpp .cc .cxx

代码里面把程序替换成其他语言的程序也是可以的

–无论是可执行程序还是脚本,都能跨语言调用–原因:所以语言运行起来,本质都是进程

模拟实现shell

这里的话是粗略模拟实现Linux的命令行

包括对个别内建命令的识别(比如echo --但是没考虑到eg: echo "aaaa")

但是不包括eg:Linux里面命令行的Tab键的作用

自己模拟实现的shell能执行普通命令的原因:普通命令是可执行程序,相当于运行程序了

内建命令的话要单独处理,因为需要父进程进行操作

#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44

int lastcode = 0;
int quit = 0;
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
char pwd[LINE_SIZE];

// 自定义环境变量表
char myenv[LINE_SIZE];

const char *getusername()
{
    return getenv("USER");
}

const char *gethostname()
{
    return getenv("HOSTNAME");
}

void getpwd()
{
    getcwd(pwd, sizeof(pwd));
}

void interact(char *cline, int size)
{
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd);
    char *s = fgets(cline, size, stdin);//会把\n也读进来,size-1的话就是\n不读进来
    assert(s);
    (void)s;//用来骗编译器--release没有assert(s),s只定义了但没使用
    cline[strlen(cline)-1] = '\0';//\0 \n这些算一个字符哈
}

//这样就可以读到ls -a -l这样了
int splitstring(char cline[], char *_argv[])//返回的是命令行参数个数
{
    int i = 0;
    argv[i++] = strtok(cline, DELIM);
    while(_argv[i++] = strtok(NULL, DELIM));
//这个;要注意哈while后面是空语句的话要跟个;
//这样写的话还把最后一个位置置为了NULL
    return i - 1;
}

void NormalExcute(char *_argv[])
{
    pid_t id = fork();
    if(id < 0){
        perror("fork");
        return;
    }
    else if(id == 0){
        //让子进程执行命令
        execvp(_argv[0], _argv);
//用这个或者execvpe会好些;因为有_argv,并且没有绝对路径提供给这个函数
        exit(EXIT_CODE);
    }
    else{
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid == id) 
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}

int buildCommand(char *_argv[], int _argc)
{
    if(_argc == 2 && strcmp(_argv[0], "cd") == 0){
        chdir(argv[1]);
        getpwd();
        sprintf(getenv("PWD"), "%s", pwd);
//将环境变量PWD设置成pwd的值   --PWD是环境变量,不是指令!
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "export") == 0){
        strcpy(myenv, _argv[1]);
        putenv(myenv);
        return 1;
    }
    else if(_argc == 2 && strcmp(_argv[0], "echo") == 0){
        if(strcmp(_argv[1], "$?") == 0)
        {
            printf("%d\n", lastcode);
            lastcode=0;
        }
        else if(*_argv[1] == '$'){
            char *val = getenv(_argv[1]+1);//注意这里的+1的含义
            if(val) printf("%s\n", val); 
       }
        else{
            printf("%s\n", _argv[1]);//打出$
        }

        return 1;
    }

    // 特殊处理一下ls,让他有"颜色"
    if(strcmp(_argv[0], "ls") == 0)
    {
        _argv[_argc++] = "--color";
        _argv[_argc] = NULL;
    }
    return 0;
}

int main()
{
    while(!quit){
        // 1. 交互问题,获取命令行
        interact(commandline, sizeof(commandline));
        // 2. 子串分割的问题,解析命令行
        int argc = splitstring(commandline, argv);
        if(argc == 0) continue;

        // 3. 指令的判断 
        //内键命令,本质就是一个shell内部的一个函数
        int n = buildCommand(argv, argc);
        // 4. 普通命令的执行
        if(!n) NormalExcute(argv);
    }
    return 0;
}

引申: strtok:C语言中用作字符串的分割

char *strtok(char *str, const char *delim);

str在首次调用时传入待分割的字符串,后续调用传入NULL,表示 “从上次分割的位置继续”

delim:分隔符

chdir:改变进程当前的工作目录

int chdir(const char *path);
会改成path的

putenv:直接让环境变量表指向参数的地址–所以这个环境变量要小心失效–这个参数最好搞在堆上

引申:环境变量表里面存的是环境变量的地址

Linux的shell命令行的一开始的环境变量是从哪搞的?

–当用户登录时,shell读取了目录里面的某个文件,那里面保存了导入环境变量的方式

引申:有些编译器对定义了但没使用的变量是会报警告的–此时可以eg:(void)s;这样来骗编译器

作业部分

下面哪些属于,Fork后子进程保留了父进程的什么?[多选]  (AC)
A.环境变量
B.父进程的文件锁,pending alarms和pending signals
C.当前工作目录
D.进程号
通过fork和exec系统调用可以产生新进程,下列有关fork和exec系统调用说法正确的是? [多选](AB)
A.fork生成的进程是当前进程的一个相同副本
B.fork系统调用与clone系统调用的工作原理基本相同
//clone函数的功能是创建一个pcb,fork创建进程以及后边的创建线程本质内部调用的clone函数实现
C.exec生成的进程是当前进程的一个相同副本//exec是程序替换函数,本身并不创建进程
D.exec系统调用与clone系统调用的工作原理基本相同

不算 main 这个进程自身,创建了多少个进程(B)

int main(int argc, char* argv[])
{
   fork();
   fork() && fork() || fork();
   fork();
}

A.18

B.19

C.20

D.21

解法:

在这里插入图片描述

1,2进程的话,创建了3,4进程,1,2进程又创建了5,6进程–他们给1,2进程返回的都是自己的pid,然后在自己进程中给自己返回的是0

5,6进程&&前面的那个fork拿到的值是父进程拿到的同款值–他跟父进程的区别是在产生5,6进程的fork那里开始的

引申:逻辑与(&&)的优先级高于逻辑或(||

所以:三个fork那里可以写成:(fork() && fork()) || fork()

Logo

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

更多推荐