目录

1.进程创建

1-1 fork函数

1-2 写时拷贝

1-3 fork的常规用法

1-4 fork调用失败的原因

2.进程终止

2-1 进程退出场景

2-2 进程常⻅退出⽅法

2-2-1 退出码

2-3-2 _exit函数

2-3-3 exit函数

2-3-4 return退出

3.进程等待

3-1 进程等待必要性

3-2 进程等待的⽅法

3-2-1 wait⽅法

3-2-2 waitpid⽅法

3-2-3 获取⼦进程status

3-2-4 阻塞与⾮阻塞等待

4.进程程序替换

4-1 替换原理

4-2 替换函数

4-2-1 函数解释

4-2-2 命名理解

5.自主Shell命令行解释器

源码


1.进程创建

1-1 fork函数

上节文章中我们已经对fork()有了基本的认识,在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程。

头文件: #include <unistd.h>

函数: pid_t fork(void);

返回值:⾃进程中返回 0 ,⽗进程返回⼦进程 id ,出错返回-1

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

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

• 将⽗进程部分数据结构内容拷⻉⾄⼦进程

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

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

fork之前⽗进程独⽴执⾏,fork之后,⽗⼦两个执⾏流分别执⾏。注意,fork之后,谁先执⾏完 全由调度器决定。

1-2 写时拷贝

通常,⽗⼦代码共享,⽗⼦再不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷⻉的⽅ 式各⾃⼀份副本。具体⻅下图:

因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离!完成了进程独⽴性的技术保证! 写时拷⻉,是⼀种延时申请技术,可以提⾼整机内存的使⽤率

1-3 fork的常规用法

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

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

1-4 fork调用失败的原因

• 系统中有太多的进程

• 实际⽤⼾的进程数超过了限制

2.进程终止

进程终⽌的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2-1 进程退出场景

• 代码运⾏完毕,结果正确

• 代码运⾏完毕,结果不正确

• 代码异常终⽌

2-2 进程常⻅退出⽅法

正常终⽌(可以通过echo $?来查看进程退出码)

1. 从main返回

2. 调⽤exit

3. _exit

异常退出:

• ctrl+c echo  ,信号终⽌

2-2-1 退出码

退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令 是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。 代码 1 或 0 以外的任何代码都被视为不成功。

Linux Shell 中的主要退出码:

• 退出码 0 表⽰命令执⾏⽆误,这是完成命令的理想状态。

• 退出码 1 通常表示发生了通用错误,比如权限不足、语法错误等。例如在没有sudo权限的情况下使⽤ yum;再例如除以 0 等操作也会返回错误码 1。

• 130 ( SIGINT 或 ^C )和 143 ( 128+n 信号,其中 n 代表终⽌码。 let a=1/0 SIGTERM )等终⽌信号是⾮常典型的,它们属于128+n 信号,其中 n 代表终⽌码。

• 可以使⽤strerror函数来获取退出码对应的描述。

2-3-2 _exit函数

头文件: #include <unistd.h>

函数: void _exit(int status);

参数: status 定义了进程的终⽌状态,⽗进程通过 wait 来获取该值

• 说明:虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执⾏$?发现 返回值是255。(后面讲)

2-3-3 exit函数

头文件: #include <unistd.h>

函数: void exit(int status);

exit最后也会调⽤_exit,但在调⽤_exit之前,还做了其他⼯作:

1. 执⾏⽤⼾通过atexit或on_exit定义的清理函数。

2. 关闭所有打开的流,所有的缓存数据均被写⼊

3. 调⽤_exit

2-3-4 return退出

return是⼀种更常⻅的退出进程⽅法。执⾏returnn等同于执⾏exit(n),因为调⽤main的运⾏时函数会 将main的返回值当做exit的参数.运行时环境会调用 exit(N);,将 N 作为退出状态码传递给操作系统。

3.进程等待

3-1 进程等待必要性

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

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

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

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

3-2 进程等待的⽅法

3-2-1 wait⽅法

#include

#include

pid_t wait(int* status);

返回值: 成功返回被等待进程 pid ,失败返回-1 。

参数: 输出型参数,获取⼦进程退出状态 , 不关⼼则可以设置成为 NULL

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

int main()
{
pid_t id = fork();
    if(id == 0)//child
    {
    printf("i am child");
    exit(0);
    }else if(id == -1)
    {
    printf("fork error");
    }
    wait(NULL);
    printf("child killed  i am father");
}

3-2-2 waitpid⽅法

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

   返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:
        pid:
        Pid=-1,等待任一个子进程。与wait等效。
        Pid>0,等待其进程ID与pid相等的子进程。
                status:输出型参数
                WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否   是正常退出)
                WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的   退出码)
options:默认为0,表示阻塞等待

       WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

        如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
       如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
       如果不存在该子进程,则立即出错返回。


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

int main()
{
pid_t id = fork();
    if(id == 0)//child
    {
    printf("i am child\n");
    exit(0);
    }else if(id == -1)
    {
    printf("fork error\n");
    }

    int status = 0;
    pid_t wid = waitpid(id,&status,0);

    if(id > 0 )
    {
    printf("wait sucesss %d,sig number %d,child exit code %d\n",wid,(status & 0x7F),(status>>8)&0xFF);
//sig number 与child exit code 下面讲
    }
}

3-2-3 获取⼦进程status

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

• 如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。

• 否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。

sig number: 如果子进程正常退出,退出码可能是 0;如果子进程因为某种错误而终止,退出码可能是其他非零值。

child exit code : 取子进程的退出码

• status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16 ⽐特位)

3-2-4 阻塞与⾮阻塞等待

wait与waitpid都是默认阻塞等待的,我们上边已经写过就不做演示.

非阻塞等待优点

  1. 提高效率:父进程不会被阻塞,可以继续执行其他任务。

  2. 灵活性:可以定期检查子进程状态,同时处理其他事件。

  3. 避免僵死进程:及时回收子进程资源,防止子进程成为僵尸进程。

  4. 响应性:父进程可以快速响应其他事件,如用户输入或信号。

  5. 资源利用:避免因等待子进程而浪费 CPU 时间,提高系统资源利用率。

//非阻塞等待
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>

typedef void (*handler_t)();  // 函数指针类型
std::vector<handler_t> handlers;  // 函数指针数组

void fun_one() {
    printf("这是一个临时任务1\n");
}

void fun_two() {
    printf("这是一个临时任务2\n");
}

void Load() {
    handlers.push_back(fun_one);
    handlers.push_back(fun_two);
}

void handler() {
    if (handlers.empty())
        Load();
    for (auto iter : handlers)
        iter();
}

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        printf("%s fork error\n", __FUNCTION__);
        return 1;
    } else if (pid == 0) {  // 子进程
        printf("child is running, pid is : %d\n", getpid());
        sleep(5);
        exit(1);
    } else {  // 父进程
        int status = 0;
        pid_t ret = 0;
        do {
            ret = waitpid(-1, &status, WNOHANG);  // 非阻塞式等待
            //-1表示等待任意子进程
            if (ret == 0) {
                printf("child is running\n");
            }
            handler();
        } while (ret == 0);

        if (WIFEXITED(status) && ret == pid) {
            printf("wait child 5s success, child return code is :%d.\n",
                   WEXITSTATUS(status));
        } else {
            printf("wait child failed, return.\n");
            return 1;
        }
    }
    return 0;
}

非阻塞等待运行截图(子进程未退出时,父进程会执行其他任务)

4.进程程序替换

fork() 之后,⽗⼦各⾃执⾏⽗进程代码的⼀部分如果⼦进程就想执⾏⼀个全新的程序呢?进程的程序 替换来完成这个功能! 

程序替换是通过特定的接⼝,加载磁盘上的⼀个全新的程序(代码和数据),加载到调⽤进程的地址空间 中!

4-1 替换原理

⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀ 种exec函数以执⾏另⼀个程序。

当进程调⽤⼀种exec函数时,该进程的⽤⼾空间代码和数据完全被新程 序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变。

4-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[]);

4-2-1 函数解释

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

• 如果调⽤出错则返回-1 

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

4-2-2 命名理解

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

• l(list) : 表⽰参数采⽤列表

• v(vector) : 参数⽤数组

• p(path):有p⾃动搜索环境变量PATH

• e(env):表⽰⾃⼰维护环境变

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

int main()
{
    // execl("/bin/ls","ls","-l",NULL);

    // 利用execl执行自己写的程序(mybin.exe)
    execl("./mybin", "mybin", NULL);

    // 带p时环境变量不需要写路径     
    execlp("ps", "ps", "-ef", NULL);

    char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
    // 使用 execle,需要自己组装环境变量
    execle("/bin/ps", "ps", "-ef", NULL, envp);

    char *const argv[] = {"ps", "-ef", NULL};
    // 使用 execv,需要指定完整路径
    execv("/bin/ps", argv);

    // 使用 execvp,可以使用环境变量 PATH,无需写全路径
    execvp("ps", argv);

    // 使用 execve,需要自己组装环境变量
    execve("/bin/ps", argv, envp);

    // 如果所有 exec 调用都失败,程序会到这里
    exit(0);
    return 0;
}

还有一个问题如何传入自定义的环境变量呢

//第一种
char *const envp_[]
{
(char*)"myenv=112233",
NULL
};
execle("程序地址","执行程序指令",NULL,envp_);
//但是这种方法有一个缺陷我们虽然传入自定义变量但是当我们调用环境变量无法显示


//第二种
//将自定义环境变量直接添加到默认的环境变量中
extern char** environ;
putenv((char*)"myenv=112233");
execle("程序地址","执行程序指令",NULL,environ);

事实上,只有execve是真正的系统调⽤,其它五个函数最终都调⽤execve,所以execve在man⼿册第2节, 其它函数在man⼿册第3节。这些函数之间的关系如下图所⽰。

下图exec函数簇⼀个完整的例⼦:

5.自主Shell命令行解释器

下图的时间轴来表⽰Shell事件的发⽣次序。其中时间从左向右。shell由标识为sh的⽅块代表,它随着时 间的流逝从左向右移动。shell从⽤⼾读⼊字符串"ls"。shell建⽴⼀个新的进程,然后在那个进程中运 ⾏ls程序并等待那个进程结束。

shell读取新的⼀⾏输⼊,建⽴⼀个新的进程,在这个进程中运⾏程序并等待这个进程结束。 所以要写⼀个shell,需要循环以下过程:

1. 获取命令⾏

2. 解析命令⾏

3. 建⽴⼀个⼦进程(fork)

4. 替换⼦进程(execvp)

5. ⽗进程等待⼦进程退出(wait)

根据这些思路,和我们前⾯的学的技术,就可以⾃⼰来实现⼀个shell了。

源码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <assert.h>

#define NUM 1024
#define OPT_NUM 64

char lineCommand[NUM];
char *myargv[OPT_NUM]; // 指针数组

int main()
{
    // 输出提示符
    printf("用户名@主机名 当前路径# ");
    fflush(stdout);

    while (1) // 无限循环,等待用户输入
    {
        // 获取用户输入,输入的时候,输入\n
        char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
        assert(s != NULL);
        (void)s;

        // 清除最后一个\n,abcd\n
        lineCommand[strcspn(lineCommand, "\n")] = 0;

        // 如果输入为空,继续下一次循环
        if (lineCommand[0] == '\0') continue;

        // 字符串切割
        myargv[0] = strtok(lineCommand, " ");
        int i = 1;
        while (myargv[i++] = strtok(NULL, " "));

        // 检查是否有命令输入
        if (myargv[0] == NULL) continue;

        // 执行命令
        pid_t id = fork();
        assert(id != -1);

        if (id == 0)
        {
            execvp(myargv[0], myargv);
            perror("execvp"); // 如果execvp失败,输出错误信息
            exit(1);
        }
        waitpid(id, NULL, 0); // 等待命令执行完成
    }

    return 0;
}

Logo

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

更多推荐