【Linux】进程控制--进程创建/进程终止/进程等待/进程程序替换/简易shell实现

一、进程创建

1.fork函数

fork函数是Linux中的一个非常重要的系统调用函数,它用于在当前进程下创建一个新的进程,新进程是当前进程的子进程,我们可以使用man 2号手册来查看fork函数:

在这里插入图片描述

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

// 创建一个子进程
pid_t fork(void);

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

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

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

在这里插入图片描述

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以

开始它们自己的旅程,看如下程序

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

int main()
{
    
    
    pid_t id = fork();
    
    if(id == -1)
    {
    
    
        printf("fork fail\n");
        exit(-1);
    }
    else if(id == 0)
    {
    
    
        // child
        while(1)
        {
    
    
            printf("我是子进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
            sleep(1);
        }
    }
    else
    {
    
    
        // parent
        while(1)
        {
    
    
            printf("我是父进程,pid:%d,ppid:%d,id:%d\n",getpid(),getppid(),id);
            sleep(1);
        }
    }
    
    return 0;
}

在这里插入图片描述

结论:fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器

决定。

一个小技巧我们在编写Makefile的时候,目标文件的依赖方法中,可以使用" @ " 来表示形成的目标文件,即依赖关系中 " : " 左边的内容,用 " @"来表示形成的目标文件,即依赖关系中":"左边的内容,用" @"来表示形成的目标文件,即依赖关系中":"左边的内容,用"^“表示目标文件的依赖文件,即依赖关系中”:"右边的内容

在这里插入图片描述

2.fork函数返回值

fork函数有两个返回值,子进程返回0,父进程返回的是子进程的pid

我们学过C/C++之后知道,一个函数的返回值最多只有一个,那么我们如何理解fork函数有两个返回值呢

我们知道,fork函数是一个系统调用,即fork函数是操作系统为我们提供的一个操作接口,所以fork函数也是由操作系统实现的,所以当我们调用fork函数时,其实是操作系统帮我们创建子进程。一个函数在正常的执行的情况下,函数return之前函数的主体功能肯定已经被执行完了,对于fork函数来说,fork函数的作用是创建子进程,所以fork函数在return之前就已经创建了子进程,那么此时就存在两个进程。既然存在两个进程,那么fork函数也就会被返回两次,因为每一个进程都会return,所以fork函数有两个返回值

我们如何理解fork返回之后,给父进程返回子进程的pid,给子进程返回0呢

因为一个父进程可能有多个子进程,而一个子进程只能有一个父进程,父进程需要子进程的pid来判别不同的子进程,而子进程则不需要判别父进程,直接调用getppid即可获得父进程的pid

如何理解同一个id值,怎么可能会保存不同的id值,让if 和 else if 同时执行

我们知道,子进程会拷贝父进程的PCB,数据结构以及页表,但是当一个进程对其数据进程写入的时候,就会发生写时拷贝,改变页表的映射关系,在一个新的空间存储数据,fork函数返回,而返回的本质就是写入,所以,谁先返回就谁先写入,因为进程具有独立性,发生写时拷贝,所以可以if 和else if同时执行

3.写时拷贝

我们看下面的程序:

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

int global_val = 100;

int main()
{
    
    
    pid_t id = fork();
    if(id < 0)
    {
    
    
        printf("fork error\n");
        return 1;
    }
    else if(id == 0)
    {
    
    
        int cnt = 0;
        while(1)
        {
    
    
            printf("我是子进程,pid:%d,ppid:%d | global_val:%d,&global:%p\n", getpid(),getppid(),global_val,&global_val);
            sleep(1);
            cnt++;
            if(cnt == 10)
            {
    
    
                printf("子进程已经更改了全局的变量啦.....\n");
                global_val = 300;
            }
        }
    }
    else
    {
    
    
        while(1)
        {
    
    
            printf("我是父进程,pid:%d,ppid:%d | global_val:%d,&global:%p\n", getpid(),getppid(),global_val,&global_val);
            sleep(2);
        }
    }
    
    return 0;
}

在这里插入图片描述

我们发现,子进程和父进程中global_val变量的地址相同,但是值却不相同,我们知道,操作系统会每一个进程都创建一个进程地址空间和页表,然后通过页表将地址空间映射到物理内存

对于父进程来说,父进程和子进程共享代码和数据,但是为了保证进程的独立性,当其中一个进程需要修改数据的时候,就会发生写时拷贝–操作系统会在物理内存重新开辟一块空间,然后将原空间中的数据拷贝到新的空间,然后在修改映射关系,最后再让进程修改对应的数据

所以表面省父子进程的global_val的 地址相同,但是这只是虚拟地址相同,而物理地址并不相同,所以父子进程的global_val的值并不相同,对于接收fork函数返回值的变量id来说也是如此,先进行return的进程会对id进行写时拷贝,所以对于父子进程来说,id的值也不相同

在这里插入图片描述

4.fork常规用法

fork函数一般用于下面两种场景:

1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

2.一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数

5.fork调用失败的原因

有如下两种原因可能会导致fork函数调用失败:

1.系统中有太多的进程

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

我们可以写一个死循环创建进程的程序来测试我们当前的操作系统能够创建多少个进程:

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

int main()
{
    
    
    int cnt = 0;
    while(1)
    {
    
    
        int ret = fork();
        if(ret < 0){
    
    
            printf("fork error!, cnt: %d\n", cnt);
            break;
        }
        else if(ret == 0){
    
    
            //child
            while(1) sleep(1);
        }
        //partent
        cnt++;
    }
    return 0;
}

上面的代码可能会导致服务器或者虚拟机直接挂掉,所以我这里就不进行测试了,有兴趣的小伙伴可以进行测试一下。

二、进程终止

1.进程退出码

我们运行一个进程是为了让进程为我们完成某一项任务,既然是为了完成一个任务,那么我们就可能会关心该进程完成任务的情况,所以就需要对任务的执行结果进行判定,此时就需要用到进程退出码,进程退出码的作用就是标定一个进程的执行结果是否正确,不同的进程退出码表示不同的执行结果,一般来说,进程退出有三种情况:

1.进程退出(代码跑完),结果正确,此时return 0

2.进程退出(代码跑完),结果不正确,此时 return !0

3.代码没跑完,程序异常了,此时退出码无意义

对于 !0 来说,不同的数字又对应着不同的错误码,我们可以使用系统提供的退出码的映射关系,也可以自己设定不同的退出码所对应的错误信息,我们可以使用C语言提供的strerror函数打印出系统提供的错误码的映射关系:

#include <stdio.h>
#include <string.h>

int main()
{
    
    
    int i = 0;
    for(i = 0; i < 100; ++i)
    {
    
    
        printf("%d:%s\n",i,strerror(i));
    }
    
    return 0;
}

在这里插入图片描述

在Linux中,存在一个变量 " ? " -该变量中始终保存着最近一个进程执行完成时的退出码,我们可以使用" echo $?"来查看最近一个进程的退出码:

在这里插入图片描述

我们可以看到,我们再次输入 "echo #?"指令的时候,打印的值为0,这是由于 echo本身也是一个可执行程序,我们使用 echo查看 ? 时 echo也会被运行,所以我们后面再次查看 $? 时,得到的结果为0

2.进程退出场景

进程退出时一个有三种情景:

1.代码运行完毕且结果正确–此时退出码为0

2.代码运行完毕但是结果错误-此时退出码为非0

3.代码异常终止–此时退出码无意义

3.进程常见退出方法

进程退出有以下三种方法:

1.main函数return返回

2.调用exit终止程序

3.调用_exit终止程序

我们平时最常用的就是通过main函数return返回退出程序,但是其实我们也可以通过库函数exit和系统调用_exit直接终止程序

库函数exit

头文件:stdlib.h
函数原型:void exit(int status);

status:status 定义了进程的终止状态,父进程通过wait来获取该值
    
函数功能:终止程序

在这里插入图片描述

在这里插入图片描述

我们可以看到,exit会将我们的进程直接终止,无论程序代码是否执行完毕

系统调用 _exit

头文件:unistd.h

函数原型:void _exit(int status);

status:status定义了进程的终止状态,父进程通过wait来获取该值
 
函数功能:终止进程

在这里插入图片描述

在这里插入图片描述

【注意】

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

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

exit 和 -exit 的区别

exit 终止进程,会主动刷新缓冲区,_exit终止进程,不会刷新缓冲区

我们以下面的例子来进行说明:

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

int main()
{
    
    
    printf("process is running");
    exit(1);
    // _exit(1);
    printf("process is running done\n");
    return 0;
}

在这里插入图片描述

在这里插入图片描述

我们可以看到,exit 终止进程,会主动刷新缓冲区,_exit终止进程,不会刷新缓冲区,分析如下:

1.由于exit是C语言库函数,而_exit是系统调用,所以可以肯定的是exit的底层是_exit函数,exit是_exit的封装

2.由于计算机体系结构的限制,CPU之间和内存交互,所以数据会先被写入到缓存区,待缓存区刷新时才被打印到显示器上,而上面的程序中,我们没有使用"\n"进行缓冲区的刷新,可以看到,exit最后打印了"process id running",而_exit什么也没有打印,所以exit在终止程序后会刷新缓冲区,而_exit终止程序后不会刷新缓冲区

3.由于exit的底层是_exit,而_exit并不会刷新缓冲区,可以反映出缓冲区不在操作系统内部,而是在用户空间

进程退出不仅有正常的退出,还有不正常的退出,比如Ctrl C终止进程,或者程序中除0,野指针,空指针的解引用等问题,程序就会异常退出

在这里插入图片描述

exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

1.执行用户通过 atexit或on_exit定义的清理函数。

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

3.调用_exit

在这里插入图片描述

三、进程等待

1.为什么要进行进程等待

为什么要进行进程等待呢,有如下原因:

我们创建一个进程的目的是为了让其帮我们完成某种任务,而既然是完成任务,进程在结束前就应该返回任务执行的结果,供父进程或者操作系统进行读取,所以,一个进程在退出的时候,不能立即释放其全部的资源–对于进程的代码和数据,操作系统可以释放,因为该进程已经不会再被执行了,但是该进程的PCB应该被保留下来,因为PCB中存放着该进程的各种状态的代码,其中就包括退出状态代码。对于父子进程来说,当子进程退出后,如果父进程不对子进程的退出状态进行读取,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。从而就会造成内存的泄漏

所以,我们需要父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息,并让操作系统回收子进程的资源(释放子进程的PCB)

进程等待的本质

我们知道,子进程的退出信息是存放在子进程的task_struct中的,所以进程等待的本质就是从子进程task_struct中读取退出信息,然后保存到对应的变量中取

在这里插入图片描述

2.如何进行进程等待

1.wait方法

我们可以通过wait系统调用来进行进程等待

在这里插入图片描述

头文件: sys/types.h  sys/wait.h

函数原型:pid_t wait(int* status)
    
status:输出型参数,获取子进程退出状态

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

我们以以下的例子来说明wait的使用:

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

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

    sleep(5);
    return 0;
}

我们可以使用一个监控脚本来检测子进程从创建到终止到被父进程回收过程中状态的变化:

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

在这里插入图片描述

我们可以看到,最开始父子进程都处于睡眠状态,之后子进程运行5s之后,此时由于父进程还要休眠10s,所以没有对子进程进行回收,所以子进程变成僵尸进程,10s过后,父进程使用wait系统调用对子进程进行进程等待,所以子进程由僵尸状态变成了死亡状态

2.waitpid方法

我们也可以使用waitpid来进行进程等待

在这里插入图片描述

头文件:sys/types.h sys/wait.h

函数原型:pid_t waitpid(pid_t pid,int* status,int option);

pid :pid=1,等待任意一个子进程,与wait等效,pid > 0,等待其进程与pid相等的子进程;

status:输出型参数,获取子进程退出状态,不关心则可以直接设置为NULL

option:等待方式,option = 0 -> 阻塞等待,option = WNOHANG -> 非阻塞等

返回值:waitpid调用成功时返回被等待进程的pid;如果设置了WNOHANG,且waitpid发现没有已退出的子进程可收集,则返回0,调用失败则返回-1

我们以以下的例子来说明waitpid的使用:

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


int main()
{
    
    
    pid_t id = fork();
    if(id == 0)
    {
    
    
        //子进程
        int cnt = 5;
        while(cnt)
        {
    
    
            printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }
        exit(12); //进程退出
    }
    // 父进程
    sleep(10);
    int status = 0; // 不是被整体使用的,有自己的位图结构
    pid_t ret = waitpid(id, &status, 0);
    if(ret > 0)
    {
    
    
        printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status>>8)&0xFF);
    }

    sleep(5);
    return 0;
}

在这里插入图片描述

我们可以看到,waitpid和wait还是有很大区别,waitpid可以传递id来指定等待特定的子进程,也可以指定option来指明等待方式

【总结】

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:

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

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

如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。

如果不存在该子进程,则立即出错返回

3.获取子进程status

我们在上面的程序中,打印sig number和child exit code的时候,打印的格式分别为status & 0x7F), (status>>8)&0xFF);这是由于status的位图结构决定我们这是使用的:

我们知道,wait和waitpid都有一个参数该参数是一个输出型参数,由操作系统填充,如果传递的参数是 NULL,则表示不关心子进程的退出状态的信息,否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程

status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图,其中,我们只需要研究status的低16比特位

在这里插入图片描述

我们可以看到,status低两个字节的内容被分成了两个部分–第一个字节的前七位表示退出信号,最后一位表示core dump标志,第二个字节表示退出的状态,退出状态即表示进程退出时的退出码

对于正常退出的程序来说,退出信号和core dump都标志为0,退出状态等于退出码,对于异常终止的程序来说,退出信号为不同终止原因对应的数字,此时退出状态就没有意义

所以status正确的读取方式如下:

printf("exit signal:%d,exit code:%d\n",(status & 0x7f),(status>>8 & 0xff));

其中,status按位与上0x7f表示保留低七位,其余九位全部置为0,从而得到退出信号

status右移8位得到退出状态,再按位与上0xff是为了得到防止右移时高位补1的情况

WIFEXITED与WEXITSTATUS宏

Linux提供了WIFEXITED与WEXITSTATUS宏来帮助我们获取status中的退出状态和退出信号,而不再需要我们自己执行按位操作

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

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

4.进程的阻塞等待方式与非阻塞等待方式

waitpid函数的第三个参数用于指定父进程的等到方式

在这里插入图片描述

其中,option代表阻塞等待方式,option为WNOHANG代表非阻塞等待

阻塞式等待即当父进程执行到waitpid函数时,如果子进程还没有退出,那么父进程就只能阻塞在waitpid函数,直到子进程退出,父进程通过waitpid读取退出信息后才能执行后面的代码

而非阻塞式等待则是,当父进程执行到waitpid函数时,父进程会直接读取子进程的状态并返回,然后接着执行后面的代码,不会等待子进程退出

轮询

轮询是指父进程在非阻塞的状态的前提下,以循环的方式不断对子进程进行进程等待,只带子进程退出

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

#define NUM 10

typedef void (*func_t)(); //函数指针

func_t handlerTask[NUM];

//样例任务
void task1()
{
    
    
    printf("handler task1\n");
}
void task2()
{
    
    
    printf("handler task2\n");
}
void task3()
{
    
    
    printf("handler task3\n");
}

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

int main()
{
    
    
    pid_t id = fork();
    // fork返回-1 直接断言断死
    assert(id != -1);
    if(id == 0)
    {
    
    
        //child
        int cnt = 10;
        while(cnt)
        {
    
    
            printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
            sleep(1);
        }

        exit(10);
    }

    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 success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
            break;
        }
        else
        {
    
    
            // waitpid调用失败
            printf("waitpid call failed\n");
            break;
        }
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

5.进程等待总结

1.为了读取子进程的退出结果以及回收子进程的资源,我们需要进程等待

2.进程等待的本质是父进程从子进程的task_struct 中读取退出信息,然后保存到status中

3.我们可以通过wait和waitpid系统调用获取退出信息,完成进程等待

4.status参数是一个输出型参数,父进程通过wait/waitpid函数将子进程的退出信息写入到status中

5.status以位图的方式存储,包括退出状态和退出信号,如果退出信号不为0,那么退出状态就没有意义

6.我们可以使用系统提供的宏 WIFEXITED和WEXITSTATUS来分别获取status中的退出状态和退出信号

7.进程等待方式分为阻塞式等待方式和非阻塞式等待方式,阻塞式等待方式用0来标识,非阻塞式等待方式用宏WONHANG来进行标识

8.由于非阻塞式等待不会等待子进程退出,所以我们需要使用轮询的方式来不断的获取子进程的退出信息

四、进程程序替换

1.创建子进程的目的

创建子进程由两个目的:

1.想让子进程执行父进程代码的一部分,执行父进程对应的磁盘代码中的一部分

2.想让子进程执行一个全新的程序,让子进程想办法,加载磁盘上指定的程序,执行新程序的代码和数据

2.什么是进程程序替换

对于创建子进程的第二个目的–让子进程来执行一个不同的程序就是程序替换

进程程序替换是指父进程使用fork函数来创建子进程后,子进程通过调用exec系列的函数来执行另一个程序,当进程调用某一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,然后执行新的程序

但是原进程的task_struct和mm_struct以及进程的id后不会改变,页表的映射关系可能会发生改变,所以调用exec系列函数时并不会创建一个新的进程,而是让原来的进程去执行另外一个新程序的代码和数据

3.进程程序替换的原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数

以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动

例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。所以进程程序替换就是用新的程序的代码和数据区替换原进程物理内存中的代码和数据。

在这里插入图片描述

4.如何进行进程程序替换

(1) 替换函数

Linux提供了一系列的exec函数来实现进程程序替换,其中包括六个库函数和一个系统调用

在这里插入图片描述

在这里插入图片描述

我们可以看到,实现进程程序替换的系统调用函数就只有一个–execve,其他的一系列的exec函数都是为了满足不同的替换场景而对execve系统调用进行的封装,其底层还是调用execve

六个库函数如下

#include <unistd.h>

extern char **environ;

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

这些函数一旦调用成功,就代表着原程序的代码和数据已经被新的程序替换了,也就是说,原来的程序的后续的语句就都不会再被执行了,所以exec函数调用成功之后没有返回值,因为该返回值接收已经没有意义了。只有exec函数调用失败,原程序可以继续往下执行时,exec返回才有意义

【总结】这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回-1所以exec函数只有出错的返回值而没有成功的返回值

(2) 函数命名理解

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

l(list) : 表示参数采用列表

v(vector) : 参数用数组

p(path) : 有p自动搜索环境变量PATH路径下搜索文件,即对于替换Linux指令相关程序时不需要我们带路径

e(env) : 表示自己维护环境变量

在这里插入图片描述

(3)函数的使用

我们想要执行一个程序,首先需要找到该可执行程序,二是指定程序执行的方式,即按照怎样的方式去执行,对于exec函数来说,'p’和非’p’用来找到程序,'l’和’v’用来指定程序执行方式,'e’用来指定环境变量

execl && execlp

exec函数的使用其实很简单,第一个参数为我们需要替换的程序的路径,如果该程序在PATH环境中,且exec函数带有"p",我们可以不带理解,只写函数名

我们以linux指令"ls"为例,我们知道,ls是Linux中"usr/bin"目录下的一个可执行程序,且该程序处于PATH环境变量中,那么如果我们需要替换该程序,exec函数的第一个参数如下:

execl("/usr/bin/ls",...); // 需要带路径
execlp("ls",..); // 可以不带路径

我们需要注意的是,带"p"的exec函数可以不带;路径的前提是被替换程序处于PATH环境变量中,如果没有处于PATH环境变量中,那么我们 仍然需要带路径

第二个参数是我们如何执行我们的程序,这里我们需要记住一点:在Linux命令行中给程序如何执行我们就如何传递参数。需要注意的是,命令行中多个指令是以空格为分割的一整个字符串,而exec函数中我们需要对不同的选项进行分割,即每一个选项都要单独分为一个字符串,所以可以看到exec函数中存在可变参数列表"…",同时,我们现在需要将最后一个可变参数设置为NULL,表示参数传递完毕

// 命令行中怎么传递就怎么传递  ls -a -l
execl("/usr/bin/ls","ls","-a","l",NULL);
execlp("ls","ls","-a","l",NULL);

我们需要注意的是,Linux中ls 其实是使用alise命令设置别名的,所以我们执行ls的时候默认带了"-color=auto"选项,它让不同类型的文件带有不同的颜色

所以我们在ls进程程序替换时如果我们想要让不同类型的文件表现为不同的颜色的话,那么我们需要显式的传递"-color=auto"选项

execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);
execlp("ls","ls","-a","-l","--color=auto",NULL);

下面我们以一个具体的例子来演示如何进行进程程序替换:

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

int main()
{
    
    
    pid_t id = fork();
    if(id == -1)
    {
    
    
        perror("fork");
        exit(1);
    }
    else if(id == 0)
    {
    
    
        printf("pid:%d,child process running..\n",getpid());
        int ret = execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);
        
        if(ret == -1)
        {
    
    
            printf("process exec fail\n");
            exit(1);
        }
       	printf("pid:%d,child process done..\n",getpid());
        return 0;
    }
    
    int status;
    pid_t ret = waitpid(id, &status, 0);
    if(ret == -1)
    {
    
    
        perror("waitpid");
        return 1;
    }
    else
    {
    
    
        printf("wait success: exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
    }
    
    return 0;
}

在这里插入图片描述

我们可以看到,我们在命令行当使用"ls -a -l"和我们使用进程程序替换得到的结果是一样的

execv && execvp

exec函数中"v"代表参数使用数组的形式进行传递–argv是一个指针数组,数组里面的每一个元素都是指针,每一个指针都指向一个参数(字符串),同样,最后一个元素指向NULL,代表参数传递完毕

我们还是以ls指令为例来进行演示:

char* argv[]={
    
    
    (char*)"ls",
    (char*)"-a",
    (char*)"-l",
    (char*)"--color=auto",
    NULL  
 };
execlv("/usr/bin/ls",argv);                                                               
execvp("ls",argv);      

由于"ls","-a"等字符串都是常量字符串,而argv里面的参数是char* const而不是const char* 的,所以我们这里需要强转一下,不强转问题也不大

在这里插入图片描述

execle && execvpe

exec函数中的"e"代表环境变量-和argv一样,envp也是一个指针数组,数组里面的每一个元素都是一个指针,指向一个环境变量(字符串),我们可以显式的初始化envp来传递我们自定义的环境变量,但是这也代表了我们放弃了字体默认的环境变量

char *const envp_[] = {
    
    
            (char*)"MYENV=11112222233334444",
            NULL
        };
execle(".mybin","./mybin",NULL,envp);

mybin.c

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

int main()
{
    
    
    // 系统就有
    printf("PATH:%s\n", getenv("PATH"));
    printf("PWD:%s\n", getenv("PWD"));
    // 自定义
    printf("MYENV:%s\n", getenv("MYENV"));

    printf("我是另一个C程序\n");
    printf("我是另一个C程序\n");
    printf("我是另一个C程序\n");
    printf("我是另一个C程序\n");
    printf("我是另一个C程序\n");
    printf("我是另一个C程序\n");
    printf("我是另一个C程序\n");
    printf("我是另一个C程序\n");
    printf("我是另一个C程序\n");

    return 0;
}

myexec.c

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

int main(int argc, char *argv[])
{
    
    
    printf("process is running...\n");
    pid_t id  = fork();
    assert(id != -1);

    if(id == 0)
    {
    
    
        execle("./mybin", "mybin", NULL, envp_); //自定义环境变量
        exit(1); //must failed
    }

    int status = 0;
    pid_t ret = waitpid(id, &status, 0);
    if(ret>0) printf("wait success: exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
}

在这里插入图片描述

我们可以看到,这里我们只获取到了自定义的环境变量MYENV,而系统的环境变量PATH和PWD则是没有被获取到

我们可以通过传递environ来获取系统的环境变量

if(id == 0)
{
    
    
    extern char** environ;
    execle("./mybin", "mybin", NULL, environ); //自定义环境变量
    exit(1); //must failed
}

在这里插入图片描述

但是这个时候我们又不能够获取我们自定义的环境变量,那么我们该如何同时获取到自定义环境变量和系统环境变量呢,这个时候我们可以使用putenv函数将自定义环境变量导入到系统环境变量中,然后通过传递环境变量environ来实现

在这里插入图片描述

putenv((char*)"MYENV=4443332211"); //将指定环境变量导入到系统中 environ指向的环境变量表
execle("./mybin", "mybin", NULL, environ); //实际上,默认环境变量你不传,子进程也能获取

在这里插入图片描述

五、实现一个简易的shell

1.shell的初步实现

我们实现一个简易的命名行解释器大概需要分为一下几个步骤:

1.输出提示符,即我们平时写指令的左边的提示符

2.从终端获取命令进行指令输入

3.解析输入的命令

4.创建子进程

5.进程程序替换

6.进程等待

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

#define NUM 1024 // 一个指令的最大长度
#define OPT_NUM 64  // 一个指令的最多选项

char lineCommand[NUM];  // 保存输入命令的数组
char* myargv[OPT_NUM];  // 保存选项的数组

int main()
{
    
    
    while(1)
    {
    
    
        // 输出提示符
    	printf("[用户名@主机名 当前路径]$");
    	fflush(stdout);
    
    	// 从键盘(stdin)获取指令输入
    	char* s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);//最后一个位置来保存极端场景下的\0
    	if(s == NULL)
    	{
    
    
        	perror("fgets");
        	exit(1);
    	}
    
    	lineCommand[strlen(lineCommand)-1] = '\0', //消除命令行中最后的换行符
    
    	// 将输入的字符串解析为说个字符串存放到myargv数组中,即字符串切割
    	myargv[0] = strtok(lineCommand," ");
    	int i = 1;
    	while(myargv[i++] = strtok(NULL," "));
    
    	// 创建子进程
    	pid_t id = fork();
    
    	if(id == -1)
    	{
    
    
        	perror("fork");
        	exit(1);
    	}
    	else if(id == 0)
    	{
    
    
        	// 子进程进行进程程序替换
        	execvp(myargv[0],myargv);
        	exit(1);
    	}
        else
        {
    
    
         	int status = 0;
    		pid_t ret = waitpid(id, &status,0);
    
    		if(ret == -1)
    		{
    
    
        		perror("waitpid");
        		exit(1);
    		}   
        }
    }
   	
    return 0;
}

在这里插入图片描述

这样我们就完成了Linux中的一些基本指令了,但是我们发现,我们使用ls的时候没有颜色的功能,我们可以在程序中对ls指令进行单独的判断,然后手动的为其加上"–color=auto"选项

if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0)
{
    
    
    myargv[1++] = (char*)"--color=auto";
}

在这里插入图片描述

2.什么是当前路径

我们在运行我们上面的程序的时候会发现一个问题,当我们使用cd更换路径的时候,再使用pwd命令还是显示我们原来的路径

在这里插入图片描述

我们在解决这个问题之前,我们需要先理解什么是当前路径:

在这里插入图片描述

我们可以看到,在test程序运行起来之后,在系统中一共有两个路径,一个exe路径是指test可执行程序在磁盘中的路径,而cwd(current working directory) 为当前进程的工作目录,就我们平常所说的当前路径

在Linux中,我们可以使用chdir这个系统调用来改变进程的工作目录:

在这里插入图片描述

在这里插入图片描述

在理解什么是当前进程的工作目录之后,我们就可以解释为什么我们的shell执行cd命令后目录不会改变了

myshell是通过创建子进程的方式去执行命令行中的各种指令的,也就是说,cd命令是由子进程去执行的,那么自然被改变的也是子进程的工作目录,父进程的工作目录则不会改变

而我们使用pwd指令来查看当前路径时,cd指令对应的子进程已经执行完毕退出了,此时myshell又会给pwd创建一个新的进程,且这个子进程的工作目录和父进程的工作目录相同,所以pwd打印出来的工作目录不会改变

我们想要解决这个问题,就需要使用chdir将父进程的工作目录修改为指定的目录即可,所以这里我们呀需要对指令进行单独的判断

// cd 改变父进程的路径
if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0)
{
    
    
    if(myargv[1] != NULL)
    {
    
    
        chdir(myargv[1]);// myargv[1]中保存着指定路径
    }
    continue;  // 下面的语句不需要在继续执行了,以为你cd的目的已经达到了
}

在这里插入图片描述

3.内建命令/外部命令

Linux中的命令一共分为两种–内建命令和外部命令

内建命令是shell程序的一部分,其功能在bash源代码中,不需要派生子进程来执行,也不需要借助外部程序文件来运行,而是由shell进程本身内部的逻辑来完成,外部命令则是通过创建子进程,然后进行进程程序替换,运用外币程序文件等方式来完成

我们可以使用type命令来区分Linux中的内置命令为外部命令

在这里插入图片描述

我们对cd指令就是以内置命令的方式来进行处理的–myshell遇到cd命令时,由自己直接改变工作目录,处理完毕直接continue,而不会通过创建子进程的方式来完成,不过对于pwd我们没有单独处理成内建命令

同时我们发现echo也是一个内建命令,这也解释了为什么"echo$ 变量"可以查看本地变量和"echo$?"可以获取最近的一个进程的退出码,原因如下:

本地变量只是在当前进程有效,但是使用echo查看本地变量时,shell并不会创建子进程,而是直接在当前进程中查找,所以可以查看本地变量

shell可以通过进程等待的方式获取一个子进程的退出状态,然后将其保存在?变量中,当命令行输入"echo$?"时,直接输出?中的内容,然后将?置为0(echo正常退出的退出码),也不需要创建子进程

所以我们也可以在我们的shell程序加入echo命令了:

int  lastCode = 0; // 保存退出码
int  lastSig = 0; //保存退出信号
if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
{
    
    
    if(strcmp(myargv[1], "$?") == 0)
    {
    
    
        printf("%d, %d\n", lastCode, lastSig);
    }
    else
    {
    
    
        printf("%s\n", myargv[1]);
    }
    continue;
}

// fork之后添加的内容
lastCode = ((status >> 8) & 0xff);
lastSig = (status & 0x7f);

在这里插入图片描述

4.shell 完整代码

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

#define NUM 1024 // 一个指令的最大长度
#define OPT_NUM 64  // 一个指令的最多选项

char lineCommand[NUM];  // 保存输入命令的数组
char* myargv[OPT_NUM];  // 保存选项的数组

int  lastCode = 0;
int  lastSig = 0;

int main()
{
    
    
    while(1)
    {
    
    
        // 输出提示符
    	printf("[用户名@主机名 当前路径]$");
    	fflush(stdout);
    
    	// 从键盘(stdin)获取指令输入  输入\n
    	char* s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);//最后一个位置来保存极端场景下的\0
    	if(s == NULL)
    	{
    
    
        	perror("fgets");
        	exit(1);
    	}
    	//消除命令行中最后的换行符
    	lineCommand[strlen(lineCommand)-1] = '\0', 
    
    	// 将输入的字符串解析为说个字符串存放到myargv数组中,即字符串切割
        // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
    	myargv[0] = strtok(lineCommand," ");
    	int i = 1;
        if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0)
		{
    
    
    		myargv[1++] = (char*)"--color=auto";
		}
        // 如果没有子串了,strtok->NULL, myargv[end] = NULL
    	while(myargv[i++] = strtok(NULL," "));
        
    	// cd 改变父进程的路径
        // 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
        // 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
		if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0)
		{
    
    
    		if(myargv[1] != NULL)
    		{
    
    
        		chdir(myargv[1]);// myargv[1]中保存着指定路径
        	}
    		continue;  // 下面的语句不需要在继续执行了,因为你cd的目的已经达到了
		}
        
        if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
		{
    
    
    		if(strcmp(myargv[1], "$?") == 0)
    		{
    
    
        		printf("%d, %d\n", lastCode, lastSig);
    		}
    		else
    		{
    
    
        		printf("%s\n", myargv[1]);
    		}
    		continue;
		}
        
    	// 创建子进程
    	pid_t id = fork();
    
    	if(id == -1)
    	{
    
    
        	perror("fork");
        	exit(1);
    	}
    	else if(id == 0)
    	{
    
    
        	// 子进程进行进程程序替换
        	execvp(myargv[0],myargv);
        	exit(1);
    	}
        else
        {
    
    
         	int status = 0;
    		pid_t ret = waitpid(id, &status,0);
    
    		if(ret == -1)
    		{
    
    
        		perror("waitpid");
        		exit(1);
    		}   
            lastCode = ((status >> 8) & 0xff);
			lastSig = (status & 0x7f);
        }
    }
   	
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_67582098/article/details/134704955