Linux 进程控制

目录

一.进程创建

1.fork函数

2.写时拷贝

3.fork返回值

4.fork用法

5.fork调用失败的原因

二.进程终止

1.进程退出码

2.进程退出方法

3.终止时,内核的做法

三.进程等待

1.为什么进行进程等待

2.如何进行进程等待

(1)wait函数

(2)waitpid函数

四.进程程序替换

1.进程程序替换是什么

2.为什么要有进程程序替换

3.如何进行程序替换

(1)execl

(2)execlp

(3)execle

(4)execv

五.简易shell实现


前言:学完进程概念,进程控制就相对比较轻松了。

一.进程创建

1.fork函数

        再次来看fork函数。

        fork之前父进程独立执行,fork之后,父进程和子进程两个执行流分别执行。

        进程具有独立性,代码和数据必须是独立的,因此有了写时拷贝

        fork之后有两个进程,并且fork之后父子代码共享。一般情况下,父子进程共享所有的代码。

        虽然子进程共享了父进程所有的代码,但是因为eip的存在,子进程只能从fork处开始执行。

        CPU中有一个eip:程序计数器

        eip(pc指针):保存当前正在执行指令的下一条指令。

        eip程序计数器会拷贝给子进程,子进程便从eip所指向的代码处开始指向。

        fork之后,OS做了什么呢?

进程 = 内核的进程数据结构 + 进程的代码和数据

        OS会创建子进程的内核数据结构(struct take_struct + struct mm_struct + 页表) + 代码继承父进程,数据以写时拷贝的方式,来进行共享或者独立。

2.写时拷贝

        在进行写时拷贝之前,父子进程因为代码和数据共享,所以空间也是共享的。

         如果父子进程有进程要修改,这里展示的是子进程修改,那么要修改的这个变量,父子进程原来指向的空间相同,但是确定要修改这个变量时,子进程就会重新开一个空间,并把修改后的值存入该空间中,完成写时拷贝。

为什么要有写时拷贝呢?在创建子进程的时候,就把数据分开不行吗?

① 父进程的数据,子进程不一定全用,即使使用,也不一定全部会写入。

        如果直接分开,就可能会浪费空间。

② 最理想的情况是,只有会被父子进程修改的数据,进行分离拷贝,不需要修改的共享。

        这个从技术角度上很难实现。

③ 如果fork的时候,就一直拷贝数据给子进程,那么就会增加fork的成本(内存和时间)

        根据以上三点所述,最终采用写时拷贝(提高了内存的使用率)。

        写时拷贝只会拷贝父子进程修改的,这就是拷贝数据的最小成本,但是拷贝的成本是依旧存在的。

        写时拷贝是一种延时拷贝策略,只有真正要使用空间的时候,才会进行拷贝。(如果你想要空间,但是不立刻使用,就先不给你空间,那么这个空间就可以先给别个需要使用空间的进程)

3.fork返回值

① 子进程返回0

② 父进程返回子进程的pid

4.fork用法

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

② 一个进程要执行一个不同的程序。

5.fork调用失败的原因

① 系统中的进程过多。

② 实际用户的进程数超过限制。

二.进程终止

1.进程退出码

进程退出有三种情况:

① 代码跑完,结果正确。

② 代码跑完,结果不正确。

③ 代码没跑完,程序异常。

我们日常写代码的时候,在main函数中,都是return 0,那么为什么是0呢?

这个return的数字就是与进程退出的正确与否有关:0:success     非0:fail

        如果正确时,就是直接返回0,我们也不需要知道为什么正确,而如果错误,那么我们就会想要知道错误的原因,因此,根据错误的不同,会返回不同的非0的数字。

        这个数字就是进程退出码。

        这里我们可以大致看一下0-100进程退出码对应数字所对应的信息,当然并不需要记住,只是看一看。

#include <stdio.h>

int main()
{
    return 10;
}

这里我们return 10。

 echo $? :这个指令就是用来看上一个进程的退出码的

这里我们可以看到进程退出码是10。与return的数字相同。

这里因为ls -al代码成功执行,所以进程退出码是0。

2.进程退出方法

进程退出有三种方法:

① return:只能在main函数中用

② exit:终止进程,刷新缓冲区

③ _exit:直接终止进程,不会有任何刷新操作

exit和_exit的差别不大,但是一般exit用的是最多的。

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

int main()
{
    printf("hello proc");
    sleep(1);
    exit(111);
}

这里我们首先可以看到,exit终止进程时,可以得到该退出码,与return一样。

再就是注意这里打印出了hello proc,因为exit刷新了缓冲区之后才终止进程。

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

int main()
{
    printf("hello proc");
    sleep(1);
    _exit(111);
}

使用_exit后就不会打印出来了,因为_exit不会刷新缓冲区。 

3.终止时,内核的做法

        进程 = 内核结构 + 进程代码和数据

        内核结构是take_struct和mm_struct,进程终止时,OS可能并不会释放该进程的内核数据结构(是否释放取决于当时空闲的空间大小)。

        创建进程时要进行开辟空间和初始化,而终止进程一般并不会去释放这个内核的空间,而是让这个空间内的代码和数据无效,等到下次创建进程时,直接进行初始化,而不再需要开辟空间,进而提高了CPU的效率(进程的创建和终止是非常频繁的)。

        因为内核具有数据结构缓冲池:slab分派器。

三.进程等待

1.为什么进行进程等待

① 子进程退出时,父进程如果不管它,就可能导致子进程进入僵尸进程,进而造成内存泄漏。并且,进程一旦变成僵尸进程,即使是kill也无法杀死一个已经死去的进程。

② 我们需要知道父进程派给子进程的任务完成的如何,因此子进程运行完成,结果的对错,或者是否正常退出,都应该告诉父进程。

总结:父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

2.如何进行进程等待

(1)wait函数

pid_t wait(int* status)

这里我们先不去了解status,先使用NULL代替。

我们先来验证上面的解决僵尸进程。wait可以解决子进程Z状态,让子进程进入X状态退出。

#include <stdio.h>        
#include <unistd.h>    
#include <stdlib.h>                    
#include <string.h>        
#include <sys/types.h>    
#include <sys/wait.h>                     
                 
int main()                                                                              
{                
    pid_t id = fork();                                    
    if(id == 0)    
    {                             
        // child    
        while(1)    
        {    
            printf("我是子进程,我正在运行...pid: %d\n", getpid());    
            sleep(1);      
        }
    }
    else
    {
        printf("我是父进程: pid: %d, 我准备等待子进程\n", getpid());
        sleep(20);
        pid_t ret = wait(NULL);

        if(ret < 0)
        {        
            printf("等待失败!\n");
        }
        else
        {
            printf("等待成功: result: %d\n", ret);
        }

        sleep(20);
    }
    
    return 0;                                                                           
}

        这里我们让子进程变成死循环,然后我们在运行的过程中kill掉子进程,看进程状态的变化。这里父进程最开始是等待20秒的。

kill子进程之前进程状态:

kill子进程之后进程状态:

父进程等待20s结束之后:

        

        通过以上3张图,我们可以很明显的看出,在子进程被kill之后,进入了Z(僵尸)状态,然后等到父进程结束sleep之后,因为调用了wait()函数,子进程消失了。因为子进程不再是Z状态了,而是变成了X直接退出了(这里X的时间非常短,所以没有看到)。

而这张图,就显示了wait的返回值是子进程的pid。

(2)waitpid函数

        上面的wait函数并不是重点,这个waitpid函数才是,waitpid不仅包括了wait的功能,还有额外的用处。

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

使用之前要写上头文件:

#include <sys/types.h>

#include <sys/wait.h>

         这个函数有3个参数。

① 参数pid

        首先第一个参数pid,如果传的pid > 0,该传的值为多少,就代表等待哪一个子进程(指定等待),如果传 -1,就等待任意进程,,类似上面的wait。

② 参数status

        第二个参数status:这个参数是一个输出型参数,通过调用该函数,可以从函数内部拿出来特定的函数。具体作用是从子进程的take_struct中拿出子进程退出的退出码,或是拿到特定的信号。

 

status的内部构造如上图所示,我们只需要注意该整数的低16个比特位。

        这是一个位图,最低7位是进程异常退出,接收到的特定的信号;次低8位用来接收子进程的退出码。

#include <stdio.h>        
#include <unistd.h>    
#include <stdlib.h>                    
#include <string.h>        
#include <sys/types.h>    
#include <sys/wait.h>                     
                 
int main()                                                                              
{                
    pid_t id = fork();                                    
    if(id == 0)    
    {                             
        int cnt = 5;
        // child
        while(1)                                                                        
        {
            printf("我是子进程,我正在运行...pid: %d\n", getpid());
            sleep(1);
            cnt--;
            if(!cnt)
            {
                break;
            }
        }
        exit(13);    

    }
    else
    {
        int status = 0;
        printf("我是父进程: pid: %d, 我准备等待子进程\n", getpid());
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            printf("wait success, ret: % d, 我所等待的子进程的退出码: %d, 退出信号是: %d\n", ret, (status >> 8) & 0xFF, status & 0X7F);
    }
    
    return 0;                                                                           
}

        我们要想得到这个status的最低7位和次低8位就要通过位操作,(status >> 8) & 0xFF就可以得到次低8位,status & 0x7F就可以得到最低7位。

  

        运行该进程,因为该进程正确运行了,所以退出信号为0,而退出码就是我们所写在子进程中的exit()中的值。

        如果我们让子进程出错:

#include <stdio.h>        
#include <unistd.h>    
#include <stdlib.h>                    
#include <string.h>        
#include <sys/types.h>    
#include <sys/wait.h>                     
                 
int main()                                                                              
{                
    pid_t id = fork();                                    
    if(id == 0)    
    {                             
        // child
        while(1)                                                                        
        {
            printf("我是子进程,我正在运行...pid: %d\n", getpid());
            sleep(1);
            int a = 10 / 0;
        }
        exit(13);    

    }
    else
    {
        int status = 0;
        printf("我是父进程: pid: %d, 我准备等待子进程\n", getpid());
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            printf("wait success, ret: % d, 我所等待的子进程的退出码: %d, 退出信号是: %d\n", ret, (status >> 8) & 0xFF, status & 0X7F);
    }
    
    return 0;                                                                           
}

这里我们写入一个int a = 10 / 0,因为这个,子进程一定会出错。

这里运行后,就可以看到退出信号是:8

如果退出信号不为0,就说明代码没跑完,那么无论退出码是几都没有用了。

实际上,信号是没有0的:

在实际使用中,我们并不需要通过位操作来获得退出码和退出信号,可以通过系统中的宏来得到。

① WIFEXITED(status):若为正常终止子进程返回的状态,则为真(查看进程是否正常退出)【即退出码】,这里WIFEXITED为非零时,才是正常退出的。

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

wifexited

使用案例:

#include <stdio.h>        
#include <unistd.h>    
#include <stdlib.h>                    
#include <string.h>        
#include <sys/types.h>    
#include <sys/wait.h>                     
                 
int main()                                                                              
{                
    pid_t id = fork();                                    
    if(id == 0)    
    {                             
        // child
        while(1)                                                                        
        {
            printf("我是子进程,我正在运行...pid: %d\n", getpid());
            sleep(1);
        }
        exit(10);    

    }
    else
    {
        int status = 0;
        printf("我是父进程: pid: %d, 我准备等待子进程\n", getpid());
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            if(WIFEXITED(status))
            {
                printf("子进程是正常退出的,退出码: %d\n", WEXITSTATUS(status));                         
            }
    }
    
    return 0;                                                                           
}

        中间还有一个core dump,下面来介绍一下这个core dump:

#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // 子进程
        int *p = nullptr;
        *p = 1000; // 野指针问题
        exit(1);
    }

    // 父进程
    int status = 0;
    waitpid(id, &status, 0);
    printf("exitcode: %d, signo: %d, core dump flag: %d\n", (status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);

    return 0;
}

        这里因为野指针的问题,会发送11号信号,这里的core dump是0。

 这里我们在man手册中看一下信号码。

        这里可以发现,一般是Core的都是因为我们自己的代码问题导致的异常,出现这种问题的,可能是要我们调试的。

 

这个core dump在服务器中默认是0,是关闭的。 

可以通过ulimit -c来增大空间。

然后我们再次调用:

现在core dump就变成1了。

这时我们ll,会发现多了一个core.的文件,后面的.28339是引起异常的进程。

这种机制就叫做核心转储,会把进程在运行中,对应的异常上下文数据,core dump到磁盘上,方便调试。

        这里我们创建时使用了-g选项后,进行gbd调试时,直接输入core-file [对应的core.文件],就可以看到异常的位置。

        那么为什么服务器中的core dump一般是关闭的呢?

(1) core dump需要配合gdb和-g选项,能用-g的一定是调试Debug版本,能上服务器的都是Release版本的,没法调试。

(2) 各种大厂服务因错误挂掉了后,是没有时间去调试的,一般都是去重启该服务,并且很多都是自动重启的,那么自动重启了很多次之后,每次都有一个core dump,那么过多之后,就可能把空间占满,出现问题。

③ 参数options

        再就是第三个参数options:传值0,为阻塞等待,使用宏WNOHANG为非阻塞等待。

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

阻塞等待:当我们调用某些函数的时候,因为条件不就绪,需要我们阻塞等待。

        本质:就是当前进程自己变成阻塞状态,等条件就绪(任意的软硬件条件)的时候,再被唤醒。

非阻塞等待:当我们调用某些函数的时候,不需要我们一直等待,可以先去做别的事情,也会进行轮调检测查看处理状态。

之前写的都是阻塞等待,就不演示阻塞等待了,这里来演示一下非阻塞等待的。

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

typedef void (*handler_t)();

//方法集
std::vector<handler_t> handlers;

void fun1()
{
    printf("hello, 我是方法1\n");
}
void fun2()
{
    printf("hello, 我是方法2\n");
}

void Load()
{
    //加载方法
    handlers.push_back(fun1);
    handlers.push_back(fun2);
}

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //子进程
        while(1)
        {
            printf("我是子进程, 我的PID: %d, 我的PPID:%d\n", getpid(), getppid());
            sleep(3);
        }

        exit(104);
    }
    else if(id >0)
    {
        //父进程
        // 基于非阻塞的轮询等待方案
        int status = 0;
        while(1)
        {
            pid_t ret = waitpid(-1, &status, WNOHANG);
            if(ret > 0)
            {
                printf("等待成功, %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);
                break;
            }
            else if(ret == 0)
            {
                //等待成功了,但是子进程没有退出
                printf("子进程没好,那么我父进程就做其他事情了...\n");
                if(handlers.empty()) 
                    Load();

                for(auto f : handlers)
                {
                    f(); //回调处理对应的任务
                }
                sleep(1);
            }
            else{
                //出错了
            }
        }
    }
    else
    {
        //do nothing
    }

    return 0;
}

 

这里子进程和父进程都在运行,父进程并没听有因为要等待子进程而耽误自己运行。

四.进程程序替换

1.进程程序替换是什么

        子进程执行的是父进程的代码片段。

        如果我们想让创建出来的子进程,去执行全新的程序怎么办?

        就使用进程程序替换。

        程序进程替换的原理:

① 将磁盘中的程序,加载到内存结构

② 重新建立页表映射,谁执行程序替换,就重新建立谁的映射(子进程)。

     效果:让父进程和子进程彻底分离,并让子进程执行一个全新的程序。

2.为什么要有进程程序替换

        在linux编程的时候,往往需要子进程做两件种类事情:

① 让子进程执行父进程的代码片段(服务器代码)

② 让子进程执行磁盘中的一个权限的程序(shell),想让客户端执行对应的程序,通过我们的进程,执行其他人写的进程代码等待。(需要C/C++变为Python/Java/Shell等等)

3.如何进行程序替换

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

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

 以上7个函数是专门用来进行程序替换的函数。

l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

        我们如果想执行一个全新的程序,需要做这两个事情:

① 先找到这个程序在哪

② 对程序可能携带的选项进行执行

(1)execl

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

        第一个参数是寻找环境变量PATH的路径,第二个参数是参数包,需要我们写参数指令,最后必须是NULL,表示参数传递完毕。

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


int main()
{
    printf("我是父进程,我的pid是: %d\n", getpid());
    execl("/usr/bin/ls", "ls", "-l", "-a", NULL); //带选项
    //execl("/usr/bin/top", "top", NULL); //不带选项
    //execl("/usr/bin/pwd", "pwd", NULL); //不带选项

    printf("我执行完毕了,我的pid是: %d, ret: %d\n", getpid(), ret);
    
    return 0;
}

        一旦替换成功,就将当前进程的代码和数据全部替换了。

        这里我们可以看到,后面的printf是代码,但是并没有输出,因为它被替换了,替换后该代码就不存在了。

        因此这个程序替换函数并不需要判断返回值,只要替换成功,就不会有返回值;而失败的时候,必然会继续向后执行,最多通过返回值得到什么原因导致的替换失败。

        这里调用下面的execl不带选项的结果如下:

(2)execlp

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

这个与上面多了个p,就可以自动搜索环境变量PATH了

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


int main()
{
    printf("我是父进程,我的pid是: %d\n", getpid());
    pid_t id = fork();
    if(id == 0)
    {
        //child
        // 让子进程执行全新的程序,以前是执行父进程的代码片段
        
        printf("我是子进程,我的pid是: %d\n", getpid());
       
        execlp("ls", "ls", "-a", "-l", NULL);// 这里出现了两个ls, 含义不一样

        exit(1); //只要执行了exit,意味着,execl系列的函数失败了
    }
    // 一定是父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if(ret == id)
    {
        sleep(2);
        printf("父进程等待成功!\n");
    }

    return 0;
}

        带p的exec可以在执行命令的时候,去默认的搜索路径去搜索,而不需要我们手动的写路径,并且结果是相同的。

(3)execle

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

有了e可以自己维护环境变量

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


int main()
{
    //环境变量的指针声明
    extern char**environ;

    printf("我是父进程,我的pid是: %d\n", getpid());
    pid_t id = fork();
    if(id == 0)
    {
        //child
        // 让子进程执行全新的程序,以前是执行父进程的代码片段
        
        printf("我是子进程,我的pid是: %d\n", getpid());

        char *const env_[] = {
            (char*)"MYPATH=YouCanSeeMe!!",
            NULL
        };
        //e: 添加环境变量给目标进程,是覆盖式的!
        execle("./mycmd", "mycmd", NULL, env_);
       
        exit(1); //只要执行了exit,意味着,execl系列的函数失败了
    }
    // 一定是父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if(ret == id)
    {
        sleep(2);
        printf("父进程等待成功!\n");
    }

    return 0;
}

        mycmd.cpp:

#include <iostream>
#include <stdlib.h>

int main()
{
    std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;
    std::cout << "-------------------------------------------\n";

    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    return 0;
}

        带e的exec可以维护自己的环境变量。

        这里通过程序替换,去替换到mycmd中,并且维护的环境变量为MYPATH。

如果我们不想修改环境变量,可以这样写:

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


int main()
{
    //环境变量的指针声明
    extern char**environ;

    printf("我是父进程,我的pid是: %d\n", getpid());
    pid_t id = fork();
    if(id == 0)
    {
        //child
        // 让子进程执行全新的程序,以前是执行父进程的代码片段
        
        printf("我是子进程,我的pid是: %d\n", getpid());

        char *const env_[] = {
            (char*)"MYPATH=YouCanSeeMe!!",
            NULL
        };
        //e: 添加环境变量给目标进程,是覆盖式的
        execle("./mycmd", "mycmd", NULL, environ);

        exit(1); //只要执行了exit,意味着,execl系列的函数失败了
    }
    // 一定是父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if(ret == id)
    {
        sleep(2);
        printf("父进程等待成功!\n");
    }

    return 0;
}

 再修改一下mycmd:

#include <stdlib.h>

int main()
{
    std::cout << "PATH:" << getenv("PATH") << std::endl;
    std::cout << "-------------------------------------------\n";
    std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;
    std::cout << "-------------------------------------------\n";

    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    std::cout << "hello c++" << std::endl;
    return 0;
}

这里会发现PATH就是系统的环境变量,而MYPATH没了我们的维护就为空了。 

当然,如果我们去添加后,就可以输出出来了。

(4)execv

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

这里有了v,就可以用让参数数组实现。而l是列表。

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


int main()
{
    printf("我是父进程,我的pid是: %d\n", getpid());
    pid_t id = fork();
    if(id == 0)
    {
        //child
        // 让子进程执行全新的程序,以前是执行父进程的代码片段
        
        printf("我是子进程,我的pid是: %d\n", getpid());

        char *const argv_[] = 
        {
            (char*)"ls",
            (char*)"-a",
            (char*)"-l",
            (char*)"-i",
            NULL
        };

        execv("/usr/bin/top", argv_);

        exit(1); //只要执行了exit,意味着,execl系列的函数失败了
    }
    // 一定是父进程
    int status = 0;
    int ret = waitpid(id, &status, 0);
    if(ret == id)
    {
        sleep(2);
        printf("父进程等待成功!\n");
    }

    return 0;
}

(5)execvp

(6)execvpe

这俩也是如此,根据l、v、p、e的不同有不同的输入。

        上面6个是一起的,只有(7)是单独的,因为(7)execve才是真正的系统接口,上面的6个都是对系统接口execve的封装。

        那么为什么会有这么多封装的接口呢? 这个就像函数重载一样,是为了适配更多的应用场景。

        最后,再记一下l、v、p、e的不同作用。

l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

五.简易shell实现

        shell中有很多命令是通过创建子进程来执行的,但是有一些命令是不能通过创建子进程执行的,这就是内建命令。例如cd等,如果依旧创建子进程执行,就会使命令失效,因为仅仅子进程的位置变了,而shell本身没有变化。

        那么内建命令如何做呢,让父进程shell自己执行,不让子进程执行。

        环境变量的数据是在进程的上下文中。

        环境变量会被子进程继承下去,所以环境变量有全局属性。

        当进行程序替换的时候,当前进程的环境变量非但不会被替换,而且是继承父进程的。

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

#define SEP " "
#define NUM 1024
#define SIZE 128

char command_line[NUM];
char *command_args[SIZE];

char env_buffer[NUM];

extern char**environ;

//对应上层的内建命令
int ChangeDir(const char * new_path)
{
    chdir(new_path);

    return 0; // 调用成功
}

void PutEnvInMyShell(char * new_env)
{
    putenv(new_env);
}

int main()
{
    //shell 本质上就是一个死循环
    while(1)
    {
        // 1. 显示提示符
        printf("[李四@我的主机名 当前目录]# ");
        fflush(stdout);

        // 2. 获取用户输入
        memset(command_line, '\0', sizeof(command_line)*sizeof(char));
        fgets(command_line, NUM, stdin); //键盘,标准输入,stdin, 获取到的是c风格的字符串, '\0'
        command_line[strlen(command_line) - 1] = '\0';// 清空\n
        // 3. 字符串切分"ls -a -l -i" -> "ls" "-a" "-l" "-i"
        command_args[0] = strtok(command_line, SEP);
        int index = 1;

        // 给ls命令添加颜色
        if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) 
        {
            command_args[index++] = (char*)"--color=auto";
        }

        // strtok截取成功,返回字符串其实地址
        // 截取失败,返回NULL
        while(command_args[index++] = strtok(NULL, SEP));

        // 4. 内建命令
        if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
        {
            ChangeDir(command_args[1]); //让调用方进行路径切换, 父进程
            continue;
        }
        if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
        {
            // 目前,环境变量信息在command_line,会被清空
            // 此处我们需要自己保存一下环境变量内容
            strcpy(env_buffer, command_args[1]);
            PutEnvInMyShell(env_buffer); //export myval=100, BUG?
            continue;
        }

        // 5. 创建进程,执行
        pid_t id = fork();
        if(id == 0)
        {
            // child
            // 6. 程序替换
            execvp(command_args[0], command_args);
            exit(1); //执行到这里,子进程一定替换失败
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            printf("等待子进程成功: sig: %d, code: %d\n", status & 0x7F, (status>>8) & 0xFF);
        }
    }
}

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰果滴

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值