【Linux】进程控制

发布于:2025-08-30 ⋅ 阅读:(23) ⋅ 点赞:(0)

一、进程创建

1.fork函数初识

在 Linux 中 fork 函数是一个非常重要的函数,它从已存在进程中创建一个新进程,此时新进程为子进程,而原进程为父进程

#include <unistd.h>

pid_t fork(void);

返回值:子进程中返回0,父进程返回子进程id,出错返回-1

进程调用 fork 函数以后,当控制转移到内核中的 fork 代码后,内核做:

•分配新的内存块和内核数据结构给子进程

•将父进程部分数据结构内容拷贝至子进程

•添加子进程到系统进程列表当中

•fork 返回,开始调度器调度

如下图所示:
在这里插入图片描述
代码示例:

int main()
{
    printf("Before: pid is %d\n", getpid());

     pid_t id = fork();
    
    if (id == -1)
    {
        perror("fork()");
        exit(1);
    }

    printf("After: pid is %d, fork return %d\n", getpid(), id);

    sleep(1);

    return 0;
}

运行结果:
在这里插入图片描述

注意: fork之后,父进程和子进程谁先执行完全由调度器决定

2.fork函数返回值

我们再来看一段代码:

int main()
{
    printf("Before fork: pid=%d\n", getpid());
    
    pid_t id = fork();

    if (id > 0) 
    {
        // 父进程执行此分支
        printf("父进程: pid=%d, ppid=%d, 子进程pid=%d\n", 
               getpid(), getppid(), id);
    }
    else if (id == 0) 
    {
        // 子进程执行此分支
        printf("子进程: pid=%d, ppid=%d, fork返回值=%d\n", 
              getpid(), getppid(), id);
    }
    else 
    {
        perror("fork失败");
        return 1;
    }

    // 父子进程都会执行这里
    printf("pid=%d 即将退出\n", getpid());
    return 0;
}

在这里插入图片描述

当我们使用 fork 创建进程以后,思考下面这些问题?

2.1 如何理解 fork 函数有两个返回值问题?
答:父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括创建子进程的进程控制块、创建子进程的进程地址空间、创建子进程对应的页表等等。子进程创建完毕后,操作系统还需要将子进程的进程控制块添加到系统进程列表当中,此时子进程便创建完毕了。

在这里插入图片描述
也就是说,在fork函数内部执行return语句之前,子进程就已经创建完毕了,那么之后的return语句不仅父进程需要执行,子进程也同样需要执行,这就是fork函数有两个返回值的原因。

2.2为什么给父进程返回子进程 PID,给子进程返回 0?
答:父进程需要子进程的 PID 来管理子进程(如等待子进程结束、发送信号等)。

子进程可以通过 getpid() 获取自己的 PID,通过 getppid() 获取父进程的 PID,因此不需要额外返回父进程的 PID。返回 0 是为了让子进程能够区分自己的身份。

2.3如何理解同一个 id 值,怎么可能会保存两个不同的值?换句话说,也就是如何让 if 和 else if 同时去执行各自的代码?
答:fork() 执行后,父进程和子进程拥有独立的内存空间。虽然代码看起来是同一个变量 id(虚拟地址相同),但实际上:
•父进程中的 id 存储子进程的 PID
•子进程中的 id 存储 0(内存发生写时拷贝)

因此,if 和 else if 并非在同一个进程中同时执行,而是分别在父进程和子进程中执行。

3.写时拷贝

当子进程刚刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父进程的数据在内存当中拷贝一份,然后再进行修改。

在这里插入图片描述
这种在需要进行数据修改时再进行拷贝的技术,称为写时拷贝技术。

1、为什么数据要进行写时拷贝?
答:进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程。

2、为什么不在创建子进程的时候就进行数据的拷贝?
答: 子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延时分配),这样可以高效的使用内存空间。

3.数据代码会不会进行写时拷贝?
答:90%的情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进行进程替换的时候,则需要进行代码的写时拷贝。

4. fork 常规用法

• ⼀个⽗进程希望复制⾃⼰,使⽗⼦进程同时执⾏不同的代码段。例如,⽗进程等待客⼾端请求,
⽣成⼦进程来处理请求。

• ⼀个进程要执⾏⼀个不同的程序。例如⼦进程从fork返回后,调⽤exec函数。

5. fork 调用失败的原因

•系统中有太多的进程。

•实际用户的进程数超过了限制

二、进程终止

1. 进程退出场景

进程退出的情况主要分为三种:

• 代码运⾏完毕,结果正确 —> return 0

• 代码运⾏完毕,结果不正确–> return !0

• 代码没跑完,程序异常了,退出码无意义

2.进程退出码

我们都知道main函数是代码的入口,但实际上main函数只是用户级别代码的入口,main函数也是被其他函数调用的,例如在VS2013当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的。

既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。

当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以使用echo $?命令查看最近一次进程退出的退出码信息。

为什么以0表示代码执行成功,以非0表示代码执行错误?
答: 因为代码执行成功不会在意成功背后的原因,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些非0的数字分别表示代码执行错误的原因。

C语言当中的strerror函数可以通过错误码,获取该错误码在C语言当中对应的错误信息:

在这里插入图片描述

运行代码后我们就可以看到各个错误码所对应的错误信息:

在这里插入图片描述
实际上Linux中的ls、pwd等命令都是可执行程序,使用这些命令后我们也可以查看其对应的退出码。
可以看到,这些命令成功执行后,其退出码也是0。
在这里插入图片描述
但是命令执行错误后,其退出码就是非0的数字,该数字具体代表某一错误信息。
在这里插入图片描述
总结: 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。

3. 进程常见退出方法

3.1 return退出

在main函数中使用return退出进程是我们常用的方法。
在这里插入图片描述

main 函数中的 return 0 是什么意思?这个 0 返回给谁的呢?为什么要写 0 呢?

答:这个 0 是该进程所对应的退出码,它能标定进程执行的结果是否正确。

总结:return 是一种常见的退出进程方法,执行 return n 等同于执行 exit(n),因为调用 main 的运行时函数会将 main 的返回值当做 exit 的参数。

3.2 _exit函数

_exit() 是系统调用,会直接终止进程,不进行任何用户空间的清理工作(如刷新缓冲区)。

#include <unistd.h>

void _exit(int status);

例如,以下代码中使用_exit终止进程,则缓冲区当中的数据将不会被输出。
在这里插入图片描述

运行结果:
在这里插入图片描述

3.3 exit 函数

exit() 是 C 库函数,会执行清理操作,包括刷新 stdio 缓冲区,然后调用 _exit (或类似的系统调用) 来终止进程。

例如,以下代码中exit终止进程前会将缓冲区当中的数据输出。
在这里插入图片描述
运行结果:
在这里插入图片描述

return、exit和_exit之间的区别与联系是什么?

答:只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。

使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。
在这里插入图片描述

三、进程等待

1. 进程等待必要性

• 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进⽽造成内存
泄漏。

• 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,“杀⼈不眨眼”的kill -9 也⽆能为⼒,因为谁也
没有办法杀死⼀个已经死去的进程。

• 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是
不对,或者是否正常退出。

• ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息。

2.进程等待的方法

2.1 wait 方法

功能:父进程调用 wait() 会阻塞,直到任意一个子进程结束。

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

pid_t wait(int *status);

返回值:

•成功:返回结束的子进程的 PID。
•失败:返回 -1(如无子进程)。

参数 status:

•指向一个整数变量,用于存储子进程的退出状态码。
•若不关心退出状态,可传入 NULL。

我们可以假设这样一个场景:当父进程休眠 15 秒时,子进程会在运行 10 秒后退出。由于父进程尚未执行 wait() 操作,子进程会进入僵尸状态(Z 状态)并持续约 5 秒。直到父进程休眠结束调用 wait(),僵尸进程的资源才会被回收,状态标记从 Z 变为终止。

代码示例:

int main()
{
    pid_t id = fork();
    
    if (id == 0)
    {
        // child
        int cnt = 10;
        while (cnt)
        {
            printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        exit(0); // 进程退出
    }
    // 父进程
    sleep(15);
    pid_t ret = wait(NULL);
    if (id > 0)
    {
        printf("wait success: %d\n", ret);
    }
    sleep(5);

    return 0;
}

我们写一个监控脚本来看看运行结果:

while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;sleep 1;done

在这里插入图片描述

子进程行为:

•子进程循环 10 秒(cnt 从 10 递减到 1),每秒打印一次信息。
•10 秒后子进程调用 exit(0) 退出,此时子进程变成僵尸进程(Z 状态),等待父进程回收。

父进程行为:

•父进程休眠 15 秒,期间未调用 wait()。
•在子进程退出后的前 5 秒(第 11~15 秒),子进程处于僵尸状态(可通过脚本观察到 Z+ 标记)。
•父进程休眠 15 秒结束后,调用 wait(NULL) 回收子进程,僵尸状态消失。
•父进程再休眠 5 秒后退出。

2.2 waitpid 方法

功能:父进程调用 waitpid() 等待指定子进程或任意子进程结束,并获取其退出状态。

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

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

返回值:

成功:返回结束的子进程的 PID。
失败:返回 -1(如无子进程、参数错误等)。

参数:

pid:

Pid=-1,等待任⼀个⼦进程。与wait等效。
Pid>0.等待其进程ID与pid相等的⼦进程。

status: 指向整数变量,用于存储子进程的退出状态(若不关心可传 NULL)。

•两个与statuus有关的宏函数:

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

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

options: 控制等待行为的标志位,默认为0,表⽰阻塞等待。

•WNOHANG:非阻塞模式,若子进程未结束立即返回 0。

2.3获取子进程status

先来看一段代码:
代码示例 1:

int main()
{
    pid_t id = fork();
    
    if (id == 0)
    {
        // child
        int cnt = 5;
        while (cnt)
        {
            printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        exit(10); // 进程退出
    }
    
    // 父进程
    int status = 0;
    pid_t ret = waitpid(id, &status, 0); // 设置为0表示阻塞等待
    if (id > 0)
    {
        printf("wait success: %d, ret: %d\n", ret, status); 
    }
    sleep(5);

    return 0;
}

运行结果:
在这里插入图片描述
为什么 status 的值是 2560 呢?
答:这是由于对 waitpid 返回的状态码的解读方式不正确导致。

wait 和 waitpid,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。

如果传递 NULL,表示不关心子进程的退出状态信息;否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。

status参数并非直接返回子进程的退出码,而是一个32 位整数,包含多个状态标志位,也就是说不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低 16 比特位):
在这里插入图片描述
在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。
其他位:存储更多状态信息(如进程是否暂停、是否继续执行等)。

而在上面的例子中:

子进程调用 exit(10),退出码为 10。
10 的二进制表示为 00001010。
当这个值被存储在 status 的 8-15 位时,实际数值为 10 << 8 = 2560(十进制)。

所以我们重新对代码进行修改:
代码示例 2:

int main()
{
    pid_t id = fork();
    
    if (id == 0)
    {
        // child
        int cnt = 5;
        while (cnt)
        {
            printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        exit(10); // 进程退出
    }
    
    // 父进程
    int status = 0;
    pid_t ret = waitpid(id, &status, 0); // 设置为0表示阻塞时等待
    if (id > 0)
    {
        printf("wait success: %d, singal number: %d, child exit code: %d\n", ret, (status & 0x7F), (status >> 8) & 0xFF); 
    }
    sleep(5);

    return 0;
}

运行结果:
在这里插入图片描述

解释一下(status & 0x7F)和 (status >> 8) & 0xFF):

1(status & 0x7F)
0x7F的二进制是0000 0000 0000 0000 0000 0000 0111 1111(低 7 位全为 1)。
status & 0x7F会屏蔽所有高位,只保留低 7 位,即信号编号。
示例:若子进程被 SIGTERM(信号 15)终止,status & 0x7F结果为 152(status >> 8) & 0xFF
status >> 8将整个状态码右移 8 位,把退出码部分移到低 8 位。
0xFF的二进制是0000 0000 0000 0000 0000 0000 1111 1111(低 8 位全为 1)。
(status >> 8) & 0xFF会屏蔽右移后的高位,只保留低 8 位的退出码。
示例:若子进程exit(10)(status >> 8) & 0xFF结果为 103、假设子进程正常退出,退出码为 10:

status的二进制表示:
0000 0000 0000 0000 0000 1010 0000 0000
(退出码 10 位于第 8-15 位,其余位为 03.1 计算信号编号
  status:     0000 0000 0000 0000 0000 1010 0000 0000  
& 0x7F:       0000 0000 0000 0000 0000 0000 0111 1111  
---------------------------------------------  
  结果:       0000 0000 0000 0000 0000 0000 0000 0000 → 十进制值为0  
  
结论:信号编号为 0,表示子进程未被信号终止(正常退出)。

3.2 计算退出码
1. 右移8:  0000 0000 0000 0000 0000 1010 0000 0000 >> 80000 0000 0000 0000 0000 0000 0000 1010  

2.0xFF:   0000 0000 0000 0000 0000 0000 0000 1010  
& 0xFF:       0000 0000 0000 0000 0000 0000 1111 1111  
---------------------------------------------  
  结果:       0000 0000 0000 0000 0000 0000 0000 1010 → 十进制值为10  
结论:退出码为 10,与子进程exit(10)一致。

总结如下:
•child exit code 是子进程通过 exit(n) 或 return n 返回的退出状态值(即代码中的 exit(10) )。它反映了子进程执行的结果。其中:

0:表示子进程正常结束(成功执行完毕)。
非 0:通常表示子进程异常结束,非 0 值可用于指示具体错误类型(由程序自定义,如 1 表示参数错误,2 表示文件不存在等)。

•sig number 表示终止子进程的信号编号。如果子进程是被信号杀死的(而非正常退出),该值会指示具体是哪个信号。

0:表示子进程正常退出(通过 exit() 或 return),未收到任何终止信号。
非 0:表示子进程被信号终止。例如:11 对应 SIGSEGV(段错误)。

2.4 阻塞等待 与 非阻塞等待

2.4.1感性理解

再谈进程退出:

•当进程退出时,会进入僵尸状态,此时进程虽然终止运行,但其 task_struct 结构会保留,其中记录了进程的退出状态码等信息。

wait/waitpid 作为系统调用,由操作系统内核执行。内核通过这两个接口可以访问并读取子进程的 task_struct 数据。

因此,父进程调用 wait/waitpid 获取的子进程退出信息,实际上是内核从子进程的 task_struct 中提取并返回的。

进程退出搞明白了,那么阻塞等待和非阻塞等待又是啥呢?别急,我们先来看两个小故事。

故事一:

你约李四一起去吃饭,你已经到了李四楼下,然后你给李四打电话,可李四好一会儿才接听。你便对他说:“你先别挂电话呀”。说完,你就一直拿着电话等待李四的回应,并且时不时的问李四收拾好了没有

过了大概 30 分钟,电话那头传来李四的声音:“张三,走吧,我已经到楼下了,都能看到你了呢”。听到这话,你赶忙挂了电话,也清楚了李四现在的状态,知道他已经下楼了。想到马上就能见到李四,你心里很高兴,随后便和李四开开心心地去吃饭了。

其中,你不挂电话是为了检测李四的状态,那么这种就叫做:阻塞等待。

故事二:

我给你打电话,然后问道:“你现在状态好了没呀?” 你回复说还没好呢,那我就说:“那咱们先把电话挂了吧,你先好好收拾,我就在楼下等你。” 李四听了,回了句 “行”,于是双方就这么约定好了。

之后,张三又给李四打了个电话,再次问李四:“你现在好了没?” 李四回答说还没好呢,张三便挂了电话。挂了电话后,张三就在楼底下待着,一会儿看看书,一会儿玩玩手机,偶尔还会和几个半生不熟的朋友打打招呼。

又等了一两分钟,张三实在等不及了,就又给李四打电话问:“好了没呀?” 李四还是说还没好,让张三再等等,张三无奈地回了句 “好吧”,接着就把电话挂了,继续在楼下耐心等候。

就这样,前前后后张三给李四打了十几通电话呢。大概过了二三十分钟,李四终于回复说:“我好了啊,我已经看到你了,我已经到楼下了。”随后,两人开开心心地去吃饭了。

张三给李四打电话的本质是进行状态检测:若李四处于未就绪状态,张三会直接挂断电话,这一单次行为属于非阻塞操作;而张三反复多次进行这种非阻塞尝试的过程,即构成了轮询机制。

我们可以把打电话的过程类比为系统调用 wait/waitpid
•你 → 父进程
•李四 → 子进程

当父进程调用 wait/waitpid 等待子进程时,若采用传统的阻塞式方式:

•若子进程未退出,父进程会被阻塞在 wait/waitpid 调用处
•直到子进程退出后,wait/waitpid 才会返回结果

这就是第一种情况:阻塞式等待

而非阻塞等待的机制则不同:

•父进程调用 wait/waitpid 检测子进程状态时
•若子进程未退出,系统会立即返回结果(不会阻塞父进程)
•父进程可以继续执行其他任务

这种每次检测后立即返回的方式,就是非阻塞等待。

2.4.2 进程的阻塞等待方式

代码示例:

int main()
{
    pid_t id = fork();
    assert(-1 != id);

    if (0 == id)
    {
        // child
        int cnt = 5;
        while (cnt)
        {
            printf("child running, pid is %d, ppid is %d\n", getpid(), getppid());
            cnt --;
            sleep(1);
        }
        exit(111);    
    }
    // parent
    // 1. 让OS释放子进程的Z状态
    // 2. 获取子进程的退出结果
    // 在等待期间, 子进程没有退出的时候, 父进程只能阻塞等待
    int status = 0; // child的退出信息
    int ret = waitpid(id, &status, 0);  //waitpid的第三个参数options为0,表示默认使用阻塞模式
    if (ret > 0) // 等待成功
    {        
        // 判断是否正常退出
        if (WIFEXITED(status)) // 正常退出为真
        {
            // 判断子进程运行结果是否OK
            printf("wait child 5s success, exit code: %d\n", WEXITSTATUS(status));
        }
        else 
        {
            // 
            printf("child exit not normal!\n");
        }
    }

    return 0;
}

运行结果:
在这里插入图片描述

2.4.3进程的非阻塞等待方式

在下面这段代码里,子进程每三秒会打印一次信息,并且会持续运行十秒。父进程则以每秒一次的频率对其进行非阻塞检查,一旦子进程结束运行,父进程就能马上获取到它的退出状态。

int main()
{
    pid_t id = fork();
    assert(-1 != id);

    if (0 == id)
    {
        // child
        int cnt = 10;
        while (cnt)
        {
            printf("child running, pid is %d, ppid is %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt --;
            sleep(3);
        }
        exit(111);    
    }
    
    // parent
    int status = 0;
    while (1)
    {
        pid_t ret = waitpid(id, &status, WNOHANG); // WNOHANG: 非阻塞 --> 子进程没有退出, 父进程检测之后, 立即返回
    
        if (ret == 0)
        {
            // waitpid调用成功 && 子进程没有退出
            // 子进程没有退出, 我的waitpid没有等待失败, 仅仅是检测到了子进程没退出.
            printf("wait done, but child is running...\n");
        }
        else if (ret > 0)
        {
            // 1. 等待成功 --> waitpid调用成功, 并且子进程退出了
            printf("wait successful, exit code: %d, signal code: %d\n", (status >> 8)&0xFF, status & 0x7F);
            break;
        }
        else 
        {
            // waitpid调用失败
            printf("waitpid call failed\n");
            break;
        }
        sleep(1);
    }
    return 0;
}

运行结果:
在这里插入图片描述

非阻塞等待:waitpid(id, &status, WNOHANG) 以非阻塞模式检查子进程状态:

函数返回值:
•ret == 0:子进程仍在运行
•ret > 0:子进程已退出,返回值为子进程 PID
•ret < 0:等待失败(如子进程不存在)。

解析退出状态:
•(status >> 8) & 0xFF:提取子进程的退出状态码(exit(111) 中的 111)。
•status & 0x7F:提取终止子进程的信号编号(正常退出时为 0)。
•轮询间隔:父进程每次检查后 sleep(1),避免 CPU 资源浪费。

执行流程:

•父进程创建子进程后进入循环,每秒检查一次子进程状态。
•子进程运行 10 秒,期间父进程每次检查都会输出 “wait done, but child is running…”。
•子进程退出后,父进程捕获到退出状态,打印退出码(111)和信号码(0),然后结束。

关键点:

•非阻塞等待:父进程无需挂起,可以继续执行其他任务(这里是每秒检查一次)。
•资源回收:通过 waitpid 确保子进程不会变成僵尸进程(Zombie Process)。
•状态解析:status 参数包含子进程的退出状态和终止信号信息,通过位运算提取。

我们为什么需要非阻塞等待?它的优势体现在哪里?

答:
回到之前的例子:张三给李四打电话后,如果李四尚未就绪,张三不会一直干等着。他可以在楼下自由活动,比如:看看手机消息、与旁人闲聊几句,甚至掏出《C和指针》研读一番。这种处理方式在计算机领域被称为非阻塞等待。

非阻塞等待的核心优势在于:它不会让父进程陷入 “停滞” 状态。父进程在等待子进程的过程中,完全可以并行处理其他任务。这种始终处于「运行 / 就绪态」,可继续执行其他任务,使得系统资源得到更高效的利用。

这种模式在需要同时处理多个任务的程序中尤为重要。例如,服务器程序可以在等待子进程处理特定请求的同时,继续响应其他客户端的连接请求,从而显著提升系统的并发处理能力。

那么我们可以通过函数指针数组和非阻塞等待,展示如何在 Linux C 中实现轻量级的任务调度系统,代码如下:

#define NUM 10

typedef void (*func_t)(); // func_t 是一个函数指针类型,指向「无参数、返回值为 void 的函数」。

func_t handlerTask[NUM];

// 任务1
void task1()
{
    printf("handle task 1\n");
}

// 任务2
void task2()
{
    printf("handle task 2\n");
}

// 任务3
void task3()
{
    printf("handle task 3\n");
}

// 任务4
void task4()
{
    printf("handle task 4\n");
}

void loadTask()
{
    memset(handlerTask, 0, sizeof(handlerTask));
    handlerTask[0] = task1;
    handlerTask[1] = task2;
    handlerTask[2] = task3;
}

int main()
{
    pid_t id = fork();
    assert(-1 != id);

    if (0 == id)
    {
        // child
        int cnt = 10;
        while (cnt)
        {
            printf("child running, pid is %d, ppid is %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        exit(111);    
    }
    
    loadTask();

    // parent
    int status = 0;
    while (1)
    {
        pid_t ret = waitpid(id, &status, WNOHANG); // WNOHANG: 非阻塞 --> 子进程没有退出, 父进程检测之后, 立即返回
    
        if (ret == 0)
        {
            // waitpid调用成功 && 子进程没有退出
            // 子进程没有退出, 我的waitpid没有等待失败, 仅仅是检测到了子进程没退出.
            printf("wait done, but child is running...parent running other things\n");
            for (int i = 0; handlerTask[i] != NULL; ++ i)
            {
                handlerTask[i](); // 采用回调的方式, 让父进程在空闲的时候执行其它任务
            }
        }
        else if (ret > 0)
        {
            // 1. 等待成功 --> waitpid调用成功, 并且子进程退出了
            printf("wait successful, exit code: %d, signal code: %d\n", (status >> 8)&0xFF, status & 0x7F);
            break;
        }
        else 
        {
            // waitpid调用失败
            printf("waitpid call failed\n");
            break;
        }
        sleep(1);
    }
    return 0;
}

运行结果:
在这里插入图片描述这种设计模式在服务器编程中很常见,可以在等待 IO 操作的同时继续处理其他任务,提高系统并发能力。

2.5 wait() 和 waitpid() 函数的三种核心行为模式

Linux 系统中 wait() 和 waitpid() 函数的三种核心行为模式,它们完全由子进程的当前状态决定:

• 如果⼦进程已经退出,调⽤wait/waitpid时,wait/waitpid会⽴即返回,并且释放资源,获得⼦进程退出信息。

• 如果在任意时刻调⽤wait/waitpid,⼦进程存在且正常运⾏,则进程可能阻塞。

• 如果不存在该⼦进程,则⽴即出错返回。

如下图所示:
在这里插入图片描述

总结:当我们讨论僵尸进程时,一个关键问题是:进程退出后,其状态信息究竟存储在哪里?

实际上,进程变为僵尸状态时:
1)代码段和数据段 会被操作系统回收,因为进程已停止运行。
2)** 进程控制块(PCB)** 必须被保留,其中存储了两项关键信息:
•退出状态码(exit code)
•终止信号(if any)

这些信息被保存在 PCB 中,直到父进程通过 wait() 或 waitpid() 系统调用读取。具体流程是:

•子程退出时,操作系统将退出状态写入其 PCB。
•父进程调用 wait() 获取该 PCB 中的状态信息(通过status参数)。
•父进程解析状态后,子进程的 PCB 才会被彻底释放。

在应用层,子进程的退出情况可归纳为三类:

•正常结束且结果正确(退出码通常为 0)
•正常结束但结果错误(退出码为非零值,如 1、2 等)
•异常终止(由信号触发,如 SIGSEGV、SIGFPE 等)

3. 进程等待总结

定义:

•进程等待是操作系统提供的一种同步机制,通过 wait/waitpid 等系统调用,允许父进程获取子进程的运行状态并回收其资源。

核心目的:

•资源回收:释放子进程结束后残留的僵尸进程(Zombie Process)
•状态获取:获取子进程的退出状态码(exit status)或终止信号(termination signal)

实现方式:

// 阻塞式等待:父进程暂停直到子进程退出
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, 0);

// 非阻塞式等待:立即返回并继续执行父进程
pid_t waitpid(pid_t pid, int *status, WNOHANG);

•阻塞模式:父进程挂起直到子进程状态变更
•非阻塞模式:通过轮询机制周期性检查子进程状态
•状态解析:使用 WIFEXITED/WEXITSTATUS 等宏解析返回值

典型应用场景:

•多进程服务器程序的子进程管理
•任务调度系统中的异步任务监控
•需要精确控制子进程生命周期的应用

四、进程程序替换

1. 替换原理

⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种 exec 函数以执⾏另⼀个程序。当进程调⽤⼀种 exec 函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤ exec 并不创建新进程,所以调⽤ exec 前后该进程的 id 并未改变。
在这里插入图片描述

我们思考一个问题:当进行进程程序替换时,有没有创建新的进程?
答:没有。进程程序替换之后,该进程对应的PCB、进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存当中的数据和代码发生了改变,所以并没有创建新的进程,而且进程程序替换前后该进程的pid并没有改变。

子进程进行进程程序替换后,会影响父进程的代码和数据吗?
答:不会。子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。
在这里插入图片描述

2. 替换函数

其实有六种以 exec 开头的函数,统称为 exec 函数:

#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 execve(const char *path, char *const argv[], char *const envp[]);

2.1函数解释

•这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回;

•如果调用出错则返回 -1。

•所以 exec 函数只有出错的返回值而没有成功的返回值。

2.2 命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记:

• l(list) : 表⽰参数采⽤列表
• v(vector) : 参数⽤数组
• p(path) : 有 p ⾃动搜索环境变量 PATH
• e(env) : 表⽰⾃⼰维护环境变量

如下:
在这里插入图片描述
下面我们分别来使用一下这些接口:

a. execl

函数原型

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

参数说明
•path:要执行的程序的绝对路径或相对路径(相对于当前工作目录)。例如:“/usr/bin/ls”
•arg:传递给新程序的第一个参数,通常是程序名本身(与 argv[0] 对应)。例如:“ls”
•…:可变参数列表,后续参数为传递给新程序的命令行参数(以 NULL 结尾)。例如:“-l”, “-a”, NULL

返回值
•成功:不返回(原进程的代码段、数据段等被新程序完全替换)。
•失败:返回 -1,并设置 errno(如文件不存在、权限不足等)。

代码示例:

int main()
{
    printf("process is running...\n");
    pid_t id = fork();
    assert(id != -1);

    if (0 == id)
    {
        sleep(1);
        // child
        // 类比: 命令行怎么写, 这里就怎么传
        execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
        exit(1); // exec一定出错了
    }
    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0) // 等待成功
    {
        printf("wait successful, exit_code is %d, signal_code is %d\n", (status >> 8)&0xFF, status & 0x7F);
    }
}

运行结果:
在这里插入图片描述

使用进程替换来执行 ls 命令,此时父进程等待成功就得到了退出结果,运行之后可以看到退出码和推出信号也是没问题的。

注意这个退出结果是子进程执行 ls 的执行结果。(执行成功,所以它是 0)

b. execlp

函数原型

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

参数说明
•file:要执行的程序名(无需完整路径)。例如:“ls”。
注意:系统会自动在 PATH 环境变量指定的目录中搜索该程序。
•arg:传递给新程序的第一个参数,通常是程序名本身(对应 argv[0])。例如:“ls”。
•…:可变参数列表,后续参数为传递给新程序的命令行参数(以 NULL 结尾)。例如:“-l”, “-a”, NULL。

代码实现:

int main()
{
    printf("process is running...\n");
    pid_t id = fork();
    assert(id != -1);

    if (0 == id)
    {
        sleep(1);
        // child            
        execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);
        exit(1); // exec一定出错了
    }
    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0) // 等待成功
    {
        printf("wait successful, exit_code is %d, signal_code is %d\n", (status >> 8)&0xFF, status & 0x7F);
    }
}

c. execv

函数原型

int execv(const char *path, char *const argv[]);

参数说明

•path:要执行的程序的绝对路径或相对路径(相对于当前工作目录)。例如:“/bin/ls”。
•argv:传递给新程序的参数数组,类型为 char *const [],必须以 NULL 指针结尾。数组的第一个元素 argv[0] 通常是程序名本身(与命令行调用方式一致)。

代码实现:

int main()
{
    printf("process is running...\n");
    pid_t id = fork();
    assert(id != -1);

    if (0 == id)
    {
        sleep(1);
        // child
        char *const argv[] = { "ls", "-a", "-l", NULL };
        execv("/usr/bin/ls", argv);
        exit(1); // exec一定出错了
    }
    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0) // 等待成功
    {
        printf("wait successful, exit_code is %d, signal_code is %d\n", (status >> 8)&0xFF, status & 0x7F);
    }
}

d. execvp

函数原型

int execvp(const char *file, char *const argv[]);

参数说明

•file:要执行的程序名(无需完整路径)。例如:“ls”。
注意:系统会自动在 PATH 环境变量指定的目录中搜索该程序。
•argv:传递给新程序的参数数组,类型为 char *const [],必须以 NULL 指针结尾。
数组的第一个元素 argv[0] 通常是程序名本身(与命令行调用方式一致)。

代码实现:

int main()
{
    printf("process is running...\n");
    pid_t id = fork();
    assert(id != -1);

    if (0 == id)
    {
        sleep(1);
        // child
        char *const argv[] = { "ls", "-a", "-l", "--color=auto", NULL };
        execvp("ls", argv);
        exit(1); // exec一定出错了
    }
    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0) // 等待成功
    {
        printf("wait successful, exit_code is %d, signal_code is %d\n", (status >> 8)&0xFF, status & 0x7F);
    }
}

用 test.c 去调用 mybin.c

上面所有的话题都在执行系统命令,如果我想执行我自己写的程序呢?比如用 test.c 去调用 mybin.c?

其实也很简单,代码如下:

// test.c
int main()
{
    printf("process is running...\n");
    pid_t id = fork();
    assert(id != -1);

    if (0 == id)
    {
        sleep(1);
        // child
        
        execl("./mybin", "mybin", NULL);

        exit(1); // exec一定出错了
    }
    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0) // 等待成功
    {
        printf("wait successful, exit_code is %d, signal_code is %d\n", (status >> 8)&0xFF, status & 0x7F);
    }
}

// mybin.c
int main()
{
    printf("这是另一个 C 程序\n");
    return 0;
}

同时我们还要把 Makefile 修改一下:

.PHONY:all
all: mybin myproc 

mybin:mybin.c
	g++ -o $@ $^
myproc:test.cpp
	g++ -o $@ $^
.PHONY:clean
clean:
	rm -f myproc mybin

运行结果:
在这里插入图片描述

注意,可以使用程序替换调用任何后端语言对应的可执行程序。

e. execle

函数原型

int execle(const char *path, const char *arg, ..., char * const envp[]);

参数说明
•path:要执行的程序的绝对路径或相对路径(相对于当前工作目录)。例如:“/bin/ls”。
•arg:传递给新程序的第一个参数,通常是程序名本身(对应 argv[0])。例如:“ls”。
•…:可变参数列表,后续参数为传递给新程序的命令行参数(以 NULL 结尾)。例如:“-l”, “-a”, NULL。
•envp:自定义环境变量数组,类型为 char *const [],必须以 NULL 指针结尾。每个元素格式为 “NAME=VALUE”(如 “PATH=/usr/bin”)。

test.c 代码如下:

int main()
{
    printf("process is running...\n");
    pid_t id = fork();
    assert(id != -1);

    // 修改子进程中的部分代码
	if (0 == id)
    {
        sleep(1);
        // child
        
        extern char **environ;
        putenv((char*)"MYENV=87654321"); // 将指定环境变量导入到系统中environ指向的环境变量表中
        execle("./mybin", "mybin", NULL, environ);

        exit(1); // exec一定出错了
    }
    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0) // 等待成功
    {
        printf("wait successful, exit_code is %d, signal_code is %d\n", (status >> 8)&0xFF, status & 0x7F);
    }
}

mybin.c 代码如下:

int main()
{
    printf("PATH: %s\n", getenv("PATH"));
    printf("PWD: %s\n", getenv("PWD"));
    printf("MYENV: %s\n", getenv("MYENV"));

    printf("这是另一个 C 程序\n"); 
    return 0;
}

运行结果:
在这里插入图片描述

注意:该函数适用于替换自己的程序,同时使用自己设置的环境变量的情况

f.execve

这个接口和前面所讲的 5 个接口不同,这才是真正的执行程序替换的系统调用接口,而上面那些接口都是基于系统调用(该接口)做的封装。

所以 execve 在 man 手册第 2 节,其它函数在 man 手册第 3 节。

函数原型

int execve(const char *filename, char *const argv[], char *const envp[]);

代码实现:

int main()
{
    printf("process is running...\n");
    pid_t id = fork();
    assert(id != -1);

    if (0 == id)
    {
        sleep(1);
        // child
        char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
        char *const argv[] = {"ps", "-ef", NULL};
        execve("/usr/bin/ps", argv, envp);

        exit(1); // exec一定出错了
    }
    // father
    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if (ret > 0) // 等待成功
    {
        printf("wait successful, exit_code is %d, signal_code is %d\n", (status >> 8)&0xFF, status & 0x7F);
    }
}

运行结果:
在这里插入图片描述

3.总结

如下图所示,是 exec 函数族的完整的例子:
在这里插入图片描述

实践、实现自定义shell

shell也就是命令行解释器,其运行原理就是:当有命令需要执行时,shell创建子进程,让子进程执行命令,而shell只需等待子进程退出即可。
在这里插入图片描述
其实shell需要执行的逻辑非常简单,其只需循环执行以下步骤:

  1. 获取命令⾏
  2. 解析命令⾏
  3. 建⽴⼀个⼦进程(fork)
  4. 替换⼦进程(execvp)
  5. ⽗进程等待⼦进程退出(wait)

根据这些思路,和我们前面的学的技术,就可以自己来实现一个 shell 了。

代码实现:

#include <iostream>
#include <ctype.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>
#include <sys/stat.h>
#include <fcntl.h>

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "

// 下面是shell定义的全局数据

// 1. 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0; 

// 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;

// 3. 别名映射表
std::unordered_map<std::string, std::string> alias_list;

// 4. 关于重定向,我们关心的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3

//#define TrimSpace(CMD,index) do{
//    while(isspace(*(CMD+index)))
//    {
//        index++;
//    }
//}while(0)

int redir = NONE_REDIR;
std::string filename;

// for test
char cwd[1024];
char cwdenv[1024];

// last exit code
int lastcode = 0;

const char *GetUserName()
{
    const char *name = getenv("USER");
    return name == NULL ? "None" : name;
}

const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    return hostname == NULL ? "None" : hostname;
}

const char *GetPwd()
{
    //const char *pwd = getenv("PWD");
    const char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
        putenv(cwdenv);
    }
    return pwd == NULL ? "None" : pwd;
}

const char *GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL ? "" : home;
}

void InitEnv()
{
    extern char **environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;

    //本来要从配置文件来
    //1. 获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        // 1.1 申请空间
        g_env[i] = (char*)malloc(strlen(environ[i])+1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
    g_env[g_envs] = NULL;

    //2. 导成环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}

//command
bool Cd()
{
    // cd argc = 1
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
    }
    else
    {
        std::string where = g_argv[1];
        // cd - / cd ~
        if(where == "-")
        {
            // Todu
        }
        else if(where == "~")
        {
            // Todu
        }
        else
        {
            chdir(where.c_str());
        }
    }
    return true;
}

void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

// / /a/b/c
std::string DirName(const char *pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir == SLASH) return SLASH;
    auto pos = dir.rfind(SLASH);
    if(pos == std::string::npos) return "BUG?";
    return dir.substr(pos+1);
}

void MakeCommandLine(char cmd_prompt[], int size)
{
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
    //snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}

void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

bool GetCommandLine(char *out, int size)
{
    // ls -a -l => "ls -a -l\n" 字符串
    char *c = fgets(out, size, stdin);
    if(c == NULL) return false;
    out[strlen(out)-1] = 0; // 清理\n
    if(strlen(out) == 0) return false;
    return true;
}

// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
    g_argc = 0;
    // 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
    g_argv[g_argc++] = strtok(commandline, SEP);
    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    g_argc--;
    return g_argc > 0 ? true:false;
}

void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]->%s\n", i, g_argv[i]);
    }
    printf("argc: %d\n", g_argc);
}

bool CheckAndExecBuiltin()
{
    //如果内键命令做重定向,更改shell的标准输入,输出,错误
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {
    }
    else if(cmd == "alias")
    {
       // std::string nickname = g_argv[1];
       // alias_list.insert(k, v);
    }

    return false;
}

int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        int fd = -1;
        // 子进程检测重定向情况
        if(redir == INPUT_REDIR)
        {
            fd = open(filename.c_str(), O_RDONLY);
            if(fd < 0) exit(1);
            dup2(fd,0);
            close(fd);
        }
        else if(redir == OUTPUT_REDIR)
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
            if(fd < 0) exit(2);
            dup2(fd, 1);
            close(fd);
        }
        else if(redir == APPEND_REDIR)
        {
            fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
            if(fd < 0) exit(2);
            dup2(fd, 1);
            close(fd);
        }
        else
        {}
        // 进程替换,会影响重定向的结果吗?不影响
        //child
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    int status = 0;
    // father
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
    return 0;
}

void TrimSpace(char cmd[], int &end)
{
    while(isspace(cmd[end]))
    {
        end++;
    }
}

void RedirCheck(char cmd[])
{
    redir = NONE_REDIR;
    filename.clear();
    int start = 0;
    int end = strlen(cmd)-1;

    //"ls -a -l >> file.txt" > >> <
    while(end > start)
    {
        if(cmd[end] == '<')
        {
            cmd[end++] = 0;
            TrimSpace(cmd, end);
            redir = INPUT_REDIR;
            filename = cmd+end;
            break;
        }
        else if(cmd[end] == '>')
        {
            if(cmd[end-1] == '>')
            {
                //>>
                cmd[end-1] = 0;
                redir = APPEND_REDIR;
            }
            else
            {
                //>
                redir = OUTPUT_REDIR;
            }
            cmd[end++] = 0;
            TrimSpace(cmd, end);
            filename = cmd+end;
            break;
        }
        else
        {
            end--;
        }
    }
}

int main()
{
    // shell 启动的时候,从系统中获取环境变量
    // 我们的环境变量信息应该从父shell统一来
    InitEnv();

    while(true)
    {
        // 1. 输出命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))
            continue;

        // 3. 重定向分析 "ls -a -l > file.txt" -> "ls -a -l" "file.txt" -> 判定重定向方式
        RedirCheck(commandline);

 //       printf("redir: %d, filename: %s\n", redir, filename.c_str());

        // 4. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
        if(!CommandParse(commandline))
            continue;
        //PrintArgv();

        // 检测别名
        // 5. 检测并处理内键命令
        if(CheckAndExecBuiltin())
            continue;

        // 6. 执行命令
        Execute();
    }
    //cleanup();
    return 0;
}

函数和进程之间的相似性

我们的 exec/exit 就像 call/return 一样,一个 C 程序有很多函数组成,而一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值,每个函数都有它的局部变量,不同的函数通过 call/return 系统进行通信。

这种通过参数和返回值,在拥有私有数据的函数间通信的模式,是结构化程序设计的基础。Linux 鼓励将这种应用于程序之内的模式扩展到程序之间。

如下图所示:
在这里插入图片描述
一个 C 程序可以 fork/exec 另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过 exit(n) 来返回值。调用它的进程可以通过 wait(&ret) 来获取 exit 的返回值。


网站公告

今日签到

点亮在社区的每一天
去签到