Linux 进程控制(创建/退出/等待/替换)

目录

进程创建

fork()函数

fork返回值

fork写时拷贝

fork失败原因

fork用法

进程退出

退出场景

常见的退出方法

正常退出

异常退出

_exit()系统调用

exit()函数

_exit()和exit()的区别

return 

进程等待

进程等待的必要性

进程等待的方法

wait()

waitpid()

参数int* status

进程程序替换

         进程替换的原理

替换函数exec族函数

1. execv(参数格式是数组)

2.execl(参数格式是列表)

3.execvp / execlp(不带替换程序的路径)

4.execle / execve(需要自己设置环境变量) 


什么是进程, 在另一篇博客中,戳链接( ̄︶ ̄)↗https://blog.csdn.net/qq_41071068/article/details/103213364 

进程创建

Linux 中我们可以说一个进程就是一个PCB, 即 一个task_struct, 那么创建进程也就是创建PCB, 即是创建task_struct

Linux 中说到进程创建,  就不得不提到 fork()函数. fork()在Lnux下是非常重要的一个函数 .

fork()函数

从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程

fork()在函数内部会调用clone这个系统调用接口

pid_t  fork ()

头文件unistd.h

fork返回值

fork函数返回值 : (返回值类型为pid_t, 实际等同于int)

  • 子进程在执行fork()时返回 0
  • 父进程在执行fork()时, fork()创建子进程, 返回子进程的PID (PID是一个大于0的整数)
  • 父进程在用fork()创建子进程失败时返回 -1

因为fork运行有多种结果, 所以往往fork之后要根据fork的返回值进行分流(例如用 if 写多个分支), 来看个例子 .

testfork.c 如下

#include<stdio.h>
#include<unistd.h>
int main(){
    pid_t pid = fork();
    if(pid == -1){
        perror("fork error");
    }
    else if(pid == 0){
        printf("子进程\n");
    }
    else{
        printf("父进程\n");
    }
    return 0;
}

编译执行如下, 可以看到, 当父进程用fork() 创建子进程成功后, 返回了其子进程的pid, 然后继续执行, 直到执行打印语句后子进程才执行, 如下 :

但并不是父进程创建了子进程, 父进程就一定会先执行完,才执行子进程, 也可能是父进程执行到一半, 甚至刚调用fork()创建完子进程后, 就立即转而执行子进程. 这取决于CPU的调度. 比如说下面这段代码 .

#include<stdio.h>
#include<unistd.h>
int main(){
    pid_t pid = fork();
    if(pid == -1){
        perror("fork error");
    }
    else if(pid == 0){
        printf("子进程执行\n子进程pid:%d\n", getpid());
    }
    else{
        printf("父进程开始执行\n");
        sleep(5);
        printf("父进程执行\n父进程pid:%d\n", getpid());
        printf("父进程运行结束\n");
    }
    return 0;
}

可以看到, 父进程执行到一半开始执行子进程了, 就此次运行结果分析, 由于父进程中sleep()函数, 致使父进程进入睡眠状态

(sleeping)(这种睡眠是可中断的, 当sleep()执行完, 就会中断睡眠, 进入就绪状态(或者说进入运行队列), 等待分配时间片), 子进程

当被创建后, 一直处于就绪状态(一直处于运行队列中), 等待分配时间片, 当父进程睡眠时, 子进程拿到了时间片, 子进程执行 . 当子

进程执行完, 父进程拿到时间片后, 父进程继续运行 . 

所以就有, fork创建子进程之前, 父进程独立运行, 创建子进程之后, 谁先运行取决于调度器的调度

进程调用fork,  内核会做出以下操作

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程 ( 此时已经创建了子进程的PCB即Linux下的task_struct )
  • 添加子进程到系统进程列表当中 ( 即添加子进程PCB)
  • fork返回,调度器开始调度

fork写时拷贝

fork 创建子进程采取分时拷贝的策略 .

即,  父进程创建子进程时,但是并没有直接给子进程开辟内存,拷贝数据,而是跟父进程映射到同一位置,但是如果父进

程或子进程有一方想要修改内存中的数据时,那么对于改变的这块内存,需要重新给子进程开辟内存,并且更新页表信息.

这样做, 提高创建子进程的性能, 并且能节省内存 . 如下图 :

                  图1. 父子进程共享代码与数据                                                          图2. 父子进程代码共享, 数据独立

图里涉及到了虚拟内存与分页式内存管理的内容, 简单说一下, 分页式内存管理可以将一段程序加载到不连续的物理空间上,但是

从虚拟地址空间来看依旧是连续的, 用以解决内存使用率低的问题 .

mm_struct结构体也叫内存描述符,其中记录虚拟内存各个段的起始地址, 结束地址, 通过这种方式描述了进程的虚拟地址空间, 

每一个进程都会有唯一的mm_struct结构体, mm_struct记录在task_struct中.

页表: 页表中存储的是虚拟地址和物理地址的映射关系, 即页号到物理块的地址映射. 通过虚拟地址得到页号与页内地址(或者叫页内偏移),  在页表中通过页号找到物理块号.  然后,  物理地址 = 物理块号 x 页面大小 + 页内偏移  就得到了物理地址

来段代码感受一下

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
    pid_t pid = fork();
    int data = 0;
    if(pid == -1){
        perror("fork erro");
    }
    else if(pid == 0){
        printf("子进程执行\n");
        data = 10;
        printf("data = %d\n", data);
        printf("data地址: %p\n", &data);
    }
    else{
        sleep(2);
        printf("父进程执行\n");
        printf("data = %d\n", data);
        printf("data地址: %p\n", &data);
    }
    return 0;
}

代码中, 先让父进程睡上2秒, 这时会执行子进程, 子进程修改了data的值为10, 但子进程结束后, 父进程继续执行打印出的data

还是0, 两个进程所打印的值不同,  但父子进程中data的地址都是一样的.  我们知道数据不同, 则数据一定存储在不同的物理地址

上, 打印的的变量地址依旧相同, 这是因为取地址&得到的并不是物理地址(在所有有关地址的操作中, 我们只能接触到虚拟地址),

而是虚拟地址, 虚拟地址虽然相同, 但是父子进程有着不同的mm_struct, 即有着不同的页表, 这父子进程的data相同的(虚拟)地址

通过不同的页表映射到不同的物理地址上.

运行结果如下图:

fork失败原因

  • 系统进程数达到太多, 达到上限(系统会有一个进程数的限定, 可以修改)
  • 内存不足 (fork创建子进程需要创建新的PCB, 写时拷贝可能还会分配新的数据空间)

fork用法

fork()函数当然不是为了创建子进程而创建子进程, 创建子进程目的有两种:

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段.  

    例如: 父进程等待客户端请求,生成子进程来处理请求。
     
  • 一个进程要执行一个不同的程序 . 例如: 子进程从fork返回后,调用exec函数族(用来做进程替换的一些列函数, 下面再说)替换子进程. 比如, 我们用的Shell就是一个程序,   一般为默认为bash, 我们执行一些非Shell内建命令时, 实际就是一个Shell创建子进程, 再进行进程替换的过程

进程退出

退出场景

  1. 正常符合预期退出
  2. 正常不符合预期退出
  3. 异常退出(执行过程中异常奔溃, 还未执行完)

常见的退出方法

正常退出

  1. main函数返回 ( return )
  2. 调用 exit( int status )函数
  3. 使用 _exit( int status )系统调用接口

可以使用ench $? 来查看进程退出码

异常退出

  1. 向进程发送信号导致进程异常退出(如 Ctrl+c)
  2. 代码错误导致进程运行时奔溃异常退出

说明 : 第二种情况是代码错误导致异常终止没什么说的, 就是代码问题

第一种情况是 Unix / Linux 系统中的信号是系统响应某些状况而产生的事件,是进程间通信的一种方式。信号可以由一个进程发

送给另外进程,也可以由核发送给进程 .

信号处理程序

信号处理程序是进程在接收到信号后, 系统对信号的响应. 根据具体信号的涵义, 相应的默认信号处理程序会采取不同的信号处理方式:

  • 终止进程运行, 并且产生core dump (核心转储文件)(记录一些错误信息, 方便用户查看)
  • 终止进程运行
  • 忽略信号,进程继续执行 .
  • 暂停进程运行 .
  • 如果进程已被暂停,重新调度进程继续执行 .

前两种方式会导致进程异常退出. 实际上,大多数默认信号处理程序都会终止进程的运行。

在进程接收到信号后,如果进程已经绑定自定义的信号处理程序, 进程会在用户态执行自定义的信号处理程序. 如果没有绑定,内

核会执行默认信号程序终止进程运行,  导致进程异常退出 .

例如: kill()函数, 在Shell中执行kill 指令, 在终端用键盘发送信号, 如:Ctrl+c , 都是发送信号来终止进程

_exit()系统调用

void _exit (int status)

头文件 : unistd.h

参数 :status 定义了进程的终止状态,父进程通过wait() 来获取该值(wait() 函数, 用于进程等待, 下面说).

说明 :虽然status是int,占但是仅有低8位可以被父进程所用. 在下面小结进程等待中详细说

功能 :   直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构

exit()函数

void exit (int status)

头文件 : stdlib.h

参数status 与_exit中同理

exit() 底层封装了 _exit 系统调用, 在底层调用_exit之前,  还做了下面的工作

  • 执行用户通过 atexit 或 on_exit定义的清理函数。
  • 关闭所有打开的流,所有的缓存数据均被写入 (即刷新缓冲区)
  • 调用 _exit()

_exit()和exit()的区别

  • 最大的区别是 exit()函数在调用 _exit() 系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件 (即刷新缓冲
    区), 然后将控制权交给内核 . _exit() 则是执行后立即返回给内核,而exit()要先执行一些清除操作,
     
  • 调用_exit函数时,会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新缓冲区, exit函数是在_exit函数之上的一个封装,其会调用_exit,并在调用之前先刷新缓冲区。
     
  • 补充: exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。由于Linux的标准函数库中,由于内存中都有一片缓冲区.  每次读文件时,会连续的读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区读取.  同样,每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但也给编程代来了一点儿麻烦。比如有一些数据,认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit()函数直接将进程关闭,缓冲区的数据就会丢失。因此,要想保证数据的完整性,就一定要使用exit()函数。

举栗子

#include<stdio.h>
#include<stdlib.h>
int main(){
    printf("hello world!");
    exit(0);
    return 0;
}

#include<stdio.h>
#include<unistd.h>
int main(){
    printf("hello world!");
    _exit(0);
}

可以看到, 并没有输出hello world! ,  这就_exit()没有刷新缓冲区, 导致在缓冲区的字符串没有打印到显示器就直接刷新, 造成数据丢失 .(注意hello world! 后面不能有\n, 因为\n会刷新输出缓冲区,  影响结果) 

return 

return是一种更常见的退出进程方法. 执行return n等同于执行exit(n), 因为main中 return n时, 系统会将main的返回值当做 exit()

的参数


进程等待

                                                                            

谁要等待? 等待什么? 为什么要等待 ?

首先要知道进程终止或退出的时候会发生什么,  进程退出时会关闭所有文件描述符, 释放在用户空间分配的内存, 但是PCB却会暂时保留, 里面存着退出状态, 比如一个进程正常退出, PCB里面就放着进程的退出状态(也就是退出码). 如果是异常退出( 前面说到, 肯定是收到信号了), 那么PCB里面存放着导致该进程终止的信号.

然后, 我们知道, 子进程的退出状态讲道理是该由父进程回收( 自己生的孩子, 自己就得负责 ), 也就是说, 父进程必须得等待子进程退出接收子进程的退出状态.   (即, 父进程要等待, 等待子进程退出, 为了拿到子进程的退出状态)

进程等待的必要性

  • 要是父进程先于子进程退出, 那么子进程就成了孤儿进程, 1号进程(孤儿院院长)等待领养, 1号进程要领养, 还得等子进程退出.
  • 要是子进程先于父进程退出, 退出时为了保存退出状态 , 子进程一些资源并没有释放, 如果此时父进程没有关注到子进程的退出(也就父进程是没有拿到子进程的退出状态), 子进程伤心欲绝, 就会变成 "僵尸进程",变成一个连kill -9 都杀不死的进程, 因为谁也杀不死一个已经死去的进程 (只有重新来过, 也就是重启机器才能解诀僵尸进程),  "僵尸进程" 会造成资源泄露, 占用进程数的问题 .

简单来说, 就是父进程需要知道, 派给子进程的任务完成的如何, 结果对还是不对,或者是否正常退出. 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息. 

说了一大堆, 如何等待

进程等待的方法

  • wait()

头文件 : sys/wait.h

pid_t  wait (int* status)

返回值:  成功返回被等待进程PID,  失败则返回 -1. (成功指的是被子进程正常退出, 失败指的是被子程序异常终止)
参数:  int* status, 是输出型参数,获取子进程退出状态, 将其退出状态存到status所指向的内存空间, 不关心被子进程退出状态则可以设置statusNULL

wait() 阻塞的, 在wait 返回之前, 阻塞等待子进程退出, 再返回 .

阻塞与非阻塞

  • 阻塞:   为了完成一个功能, 若发起调用, 当前不具备完成条件, 则一直等待达到完成条件, 调用才会结束
  • 非阻塞: 与阻塞相反, 若发起调用, 当前不具备完成条件, 立即返回
  • 最大区别是, 调用是否立即返回
  • waitpid()

pid_ t  waitpid (pid_t pid, int *status, int options

参数:
pid :

  • pid = -1 :等待任一个子进程
  • pid >  0 : 等待其进程ID与pid相等的子进程。

int* status:

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

后面详细看status和WIFEXITED, WEXITSTATUS这两个宏

options: options = 1时, waitpid是非阻塞的, options = 其他值时 waitpid() 是阻塞的

  • WNOHANG: 值为1 的宏 

返回值

有三种返回值
=-1 : waitpid()出错
= 0 : 没有等到子进程退出
>0 :  等到子进程退出, 返回子进程PID

详细来看:

  • 给 options 传WNOHANG 或 1, 此时waitpid()是非阻塞的,
    1. 如pid此时指定,即pid>0, 已如指定pid的进程在调用waitpid()时退出,  waitpid()返回子进程PID, 如发现指定pid的进程
        还没有退出, 不予等待, 直接返回 0.
    2. 若pid不指定,即pid = -1,若在waitpid()调用之前已有子进程退出(任意子进程都可以), 则返回该子进程的PID,
        若在调用waitpid()之前没有任何子进程退出, 则返回 0
  • 若给options传入非1的值, 如等不到子进程退出, 会一直阻塞
  • 如果调用waitpid()出错,  则返回-1, 这时errno会被设置成相应的值以指示错误所在 .

PS: 当waitpid是waitpid(-1, &status, 非1) 等效 wait (&status)
       不关心子进程退出码时 waitpid(-1, NULL, 非1) 等效 wait (NULL)

参数int* status

前面说到. wait/waitpid都可以获取到退出的子进程的退出状态, 那么获取到的状态在哪儿保存呢 ?

这就需要我们事先申请一块内存空间来保存, 即 int status; 用这个int型变量来保存退出状态, 那么wait/waitpid又如何知道, 要将退

出状态给我们事先申请好的这个变量呢? 传入这个变量的指针, 由操作系统填充.

前面说到, 进程在退出时, 会有正常退出和异常退出(终止), 那么一个int 型变量如何保存这两种不同些情况的的退出状态呢 ?

如下图:

可以看到, 在正常退出时, status这个int型变量, 只有低16位用到了

即, 在正常退出时, 低16位中的高8位存的是子进程得退出码, 后8位全为0,

在异常退出时, 底7位存的是进程的异常退出信号值, 第8位是core dump 标志, 即核心转储文件标志, 为了用户方便查看异常终止

的信息, 此时低16位中的高8位未用

所以, 在wait/waitpid结束后,  要先取出dtatus的低7位看子进程是不是异常终止, 如果不是, 再来看退出码, 如下:

if (!(status & 0x7f)) {
    printf("子进程退出码为\n", (status >> 8) & 0xff);
}
else {
    printf("子进程异常终止\n");
}

这样写很麻烦, 而且还容易出错, 所以系统就将 !(status & 0x7f) 和 (status >> 8) & 0xff 封装成了两个宏

WIFEXITED WEXITSTATUS , 既上面的代码可以写成如下:

if (WIFEXITED(status)) {
    printf("子进程退出码为\n", WEXITSTATUS(status));
}
else {
    printf("子进程异常终止\n");
}

举栗子

wait.c 

用wait() 模拟父母接孩子的场景, 父母就像wait() 阻塞一样, 不管多久, 都会等着我们, 直到接到我们.

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main(){
    printf("一位爸爸来学校接孩子放学了~\n");
    pid_t pid = fork();
    if(pid < 0){
        perror("fork erro");
        exit(-1);
    }
    else if(pid == 0){
        sleep(30);
        exit(30);
    }
    int status = -1;
    wait(&status);
    if (!(status & 0x7f)) {
        printf("%d分钟后接上了孩子\n", (status >> 8) & 0xff);                
    }
    else {
        printf("孩子在幼儿园发烧送医院了!\n");            
    }
    return 0;
}

在30秒后, 打印出了30分钟后接上了孩子 

如果我们在另一个窗口下用kill -9 杀死子进程, 此时子进程就是异常终止了, 运行如下:

 waitpid.c

用waitpid模拟出租车司机在车站等待拉乘客的场景, 出租车司机会一直去询问旅客乘不乘车, 当长的时间拉不到人, 就离开去别的地方拉乘客了

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main(){
    printf("一位出租车司机在车站拉乘客~\n");
    pid_t pid = fork();
    if(pid < 0){
        perror("fork erro");
        exit(-1);                  
    }
    else if(pid == 0){
        sleep(5);
        exit(5);                           
    }
    int status = -1;
    while(waitpid(-1, &status, WNOHANG) == 0){
        printf("没有人打车, 继续等...\n");
        sleep(1);                           
    }
    if(WIFEXITED(status)){
        printf("这次等了%d分钟才拉到乘客(子进程退出码)\n", WEXITSTATUS(status));
        printf("有人打车了, 出发~\n");
    }
    else{
        printf("没有等到乘客(子进程异常退出)\n");
    }
    return 0;

}


进程程序替换

为什么要进行进程替换 ?

前面说到,  fork()创建子进程, fork创建的子进程要么和父进程执行一样的代码, 要么执行不同的代码分支(通过fork的返回值控制),

但这样还是不够灵活. 假如要有很多的功能已经用别的程序实现好了, 那么就不需要在父进程中控制父子进程执行不同的代码分

支, 让子进程在自己的分支中完成这些功能, 而是可以直接拿一个已有的程序替换掉子进程. 使子进程的代码完全变成所替换程序

的代码. 这样就方便了很多, 而且在子进程需要完成较为复杂的功能或是多项功能时, 分支就显得力不从心了.

所以进程往往要调用exec族中的某一个函数来进行程序替换, 让一个进程来执行另一个程序. 当进程调用一种exec函数时,该进程

的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec族中的函数并不会创建新进程,所以调用exec

前后被替换进程的iD并未改变。

进程替换的原理

如上图, 进程替换时, 替换的是PCB映射在内存中的代码和数据. 这样, 该进程PID虽然没有变, 但已经物是人非, 已经不是原来的那

个进程了.

如进行进程程序替换? 前面也说到了, exec族函数

替换函数exec族函数

exec族函数共有六个, 功能都是进程程序替换, 但多个不同的函数接口使得使用更加灵活. 

其中exceve()是系统调用接口, 其余5个底层都封装了execve().

函数原型如下 :

int execl(const char *path, const char *arg0, ... /*, (char *)0 */);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg0, ... /*,(char *)0, char *const envp[]*/);
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg0, ... /*, (char *)0 */);
int execvp(const char *file, char *const argv[]);

头文件: unistd.h 

返回值 :

  • 这六个函数返回值相同. 
  • 当函数调用失败, 返回 -1. 
  • 当调用成功, 即加载新的程序, 替换后的进程启动开始执行,exec族函数不再返回 .
  • 特殊的地方是, 函数调用成功, 不返回

参数:

虽然有六个, 不好记, 但好在有规律. 可以发现, 函数名都是在exce的基础上, 加上l, v, e,  p形成新的函数名, 加哪个字母都有各自的

含义 , 如下所示:

  • l / v 必须有一个, 也只能有一个, 带l参数格式是列表,  带 v 参数格式是 数组 
  • p   有p, 会自动搜索环境变量PATH, 则可以不带路径, 只要文件名(但文件必须放在PATH环境变量指定路径).  没有p, 则必须指定文件路径
  • e    有e, 则不使用当前环境变量, 需要自己设置环境变量,  没带e, 则使用当前环境变量, 无需设置环境变量

如下表:

函数名 参数格式 函数是否自带路径(通过PATH) 是否使用当前环境变量
execl 列表 不带, 需要制定文件路径 使用
execlp 列表 , 但文件必须的放在指定目录 使用
execle 列表 不带, 需要制定文件路径 使用, 需要自己设置环境变量
execv 数组 不带, 需要制定文件路径 使用
execvp 数组 , 但文件必须的放在指定目录 使用
execve 数组 不带, 需要制定文件路径 使用, 需要自己设置环境变量

来看一下这六个函数具体如何使用.

1. execv(参数格式是数组)

#include<stdio.h>
#include<unistd.h>
int main() {
    char* arg[] = {"ls","-a","-l","/",NULL };//参数数组;//参数数组
    execv("/bin/ls", arg);
    printf("hello world!\n");
    return 0;
}

 可以看到进程被ls程序替换后, 只会执行ls的代码, 并不会再输出 hello world!

 如下, 还可以通过main函数的参数传入, 我们用Shell调用ls等命令就是这个原理.

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc, char* argv[]){
    char* arg[] = {"ls","-a","-l",NULL };//参数数组
    pid_t pid = fork();
    if(pid == -1){
        perror("fork");
    }
    else if(pid == 0){
        printf("替换子进程\n");
        execv("/bin/ls", argv);
    }
    else{
        sleep(1);
        printf("替换父进程\n");
        execv("/bin/ls", arg);
    }
    printf("hello world!\n");
    return 0;
}

 

2.execl(参数格式是列表)

#include<stdio.h>
#include<unistd.h>
int main(){
    execl("/bin/ls", "ls", "/", NULL);
    printf("hello world!\n");
    return 0;
}

3.execvp / execlp(不带替换程序的路径)

execvp

#include<stdio.h>
#include<unistd.h>
int main(int argc, char* argv[]){
    execvp("ls", arg);
    printf("hello world!\n");
    return 0;
}

 execlp

#include<stdio.h>
#include<unistd.h>
int main(){
    char* arg[] = {"ls","-a","-l",NULL };//参数数组
    execlp("ls", "-a", "-l", NULL);
    printf("hello world!\n");
    return 0;
}

 

4.execle / execve(需要自己设置环境变量) 

execve

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(int argc, char* argv[]){
    char* envp[] = {"PATH=/home/test", NULL};
    execve("/bin/env", argv, envp);
    return 0;
}

execle

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
    char* envp[] = {"PATH=/home/test", NULL};
    execle("/bin/env", "", NULL, envp);
    return 0;
}

学习完进程的创建和替换后, 就可以利用这些知识, 写一个自己的Shell了

具体代码, 持续更新

发布了223 篇原创文章 · 获赞 639 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_41071068/article/details/103251340