Linux进程:进程控制

1. 进程创建

1.1 fork

😇在linux中fork函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为子进程,⽽原进程为父进程。

Linux的man手册:
在这里插入图片描述

 #include <unistd.h> // 头文件
 #include <sys/types.h> // 头文件
 pid_t fork(void); //函数原型

返回值说明

在这里插入图片描述

——》解释:pid_t 类型在底层中就是int类型,当进程创建成功时,父进程的fork的返回值为子进程的pid,子进程的fork返回值为0,如果失败则返回 -1

——》进程调用fork,当控制转移到内核中的fork代码后,OS做四件事情

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

使用例子

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

int main()
{

    printf("父进程运行: pid: %d, ppid:%d\n", getpid(), getppid());
	
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        while (1)
        {
            printf("我是子进程,我的pid: %d, ppid: %d,ret = %d\n", getpid(), getppid(),id);
            sleep(1);
        }
    }
    else
    {
        // 父进程
        while (1)
        {
            printf("我是父进程,我的pid: %d, ppid: %d,ret = %d\n", getpid(), getppid(),id);
            sleep(1);
        }
    }


    return 0;
}

——》代码说明:父进程创建子进程,父子进程分别打印自己的pid,ppid,fork的返回值
运行结果
在这里插入图片描述

1.2 写实拷贝

通常,⽗⼦代码共享,⽗⼦再不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷贝的⽅式各自一份副本

修改内容前
在这里插入图片描述
——》对于父子进程开始数据和代码共享的理解:父进程创建子进程,子进程会把父进程内核数据结构全拷贝一份,再填入子进程的相关信息(pid,ppid),但是数据(代码数据)不会先进行拷贝。父子代码与数据在开始时共享就很好理解了,因为子进程把父进程包括页表的映射关系给你全拷贝了,那么父子进程的代码和数据映射到同一片物理内存也就很好理解了。

(父或子任何一方修改了一段数据)修改内容之后
在这里插入图片描述
——》结果:发生写实拷贝,子进程申请物理内存,数据会被先拷贝一份进去,在子进程的页表内重新构建映射关系,再进行写入

🤔疑问1OS怎么知道要触发写实拷贝的?

——》💡1. fork之后,父进程与子进程的数据区都会被权限替换,都会变成只读的(观察第一张图也能发现)。fork之后,子进程只会执行fork之后的代码。

——》💡2. 子进程写入数据——>cpu会发现你对只读的内容进行写入,触发系统错误——>OS发现错误,触发缺页中断——>OS检查原因,如果发现你是对数据区进行写入,那么触发写实拷贝,如果是对代码区进行写入(或者是野指针),那就是代码本身出现问题,直接杀掉进程

——》💡3. 写实拷贝完成,父进程和子进程的数据区会进行权限恢复,都变成可读可写——>然后恢复运行,让两个进程继续执行代码。

🤔疑问2写实拷贝为什么还要进行一次拷贝,直接申请内存再写入不就完了吗?

——》💡可能这份数据还要用,比如++操作,就要用到修改之前的数据。

2.进程终止

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

2.1 main函数返回值

🤔问题引入:从前我们写代码的时候,似乎从来没有关注main函数的返回值,,只知道代码的最后要加上return 0就可以了。main函数的返回值到底是返回给谁的?

——》💡main函数的返回值,是返回给父进程或者系统的

#include <stdio.h>

int main()
{
	printf("我的退出码是10\n");
    return 10;
}

——》echo $?指令,可以查到命令行中,最近一个程序退出时的退出码
在这里插入图片描述
🤔main函数的返回值有什么用

——》可以表明程序运行时的错误原因:0为成功执行,非0都是发生错误——》错误的原因有很多,所以用了不同的数字表明出错的原因,我们可以自己设定错误码,以及约定对应的错误信息。

——》C++也为我们约定了一些退出码,可以通过调用函数查看:

#include <errno.h>
erron ——》表明最近一次执行函数的错误码 //C++约定的错误码

#include <string>
char *strerror(int errnum); //输入C++约定的错误码,返回错误原因字符串

Linux常见退出码
在这里插入图片描述
备注

  • 退出码 0 表⽰命令执⾏⽆误,这是完成命令的理想状态。
  • 退出码 1 我们也可以将其解释为“不被允许的操作”,例如在没有sudo权限的情况下使⽤yum,再例如除以 0 等操作也会返回错误码;
  • 130(SIGINT或C)和143(SIGTERM )等终止信号是非常典型的,它们属于
  • 128+n 信号,其中n代表终止码,可以使用strerror函数来获取退出码对应的描述。

测试用例

#include <iostream>
#include <string>
#include <cstdio>
#include <string.h>
#include <errno.h>

int main()
{
	//代码在该目录下,打开log.txt文件
    FILE *fp = fopen("./log.txt", "r");
    if (fp == nullptr)
    {
    	//如果打开失败,打印错误码与错误信息
        printf("after: errno : %d, errstring: %s\n", errno, strerror(errno));
        return errno;
    }
}

——》补充:在该目录下没有log.txt的文件,所以一定会打开错误

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

2.2 进程常见退出方法

1️⃣正常退出
  • return ——>在main函数中return,进程退出。
  • exit ——>在程序的任何地方,都能直接退出进程。
#include <unistd.h>
 void exit(int status);
  • _exit ——>系统调用的退出进程。
#include <unistd.h>
 void _exit(int status);
参数:status 定义了进程的终⽌状态,⽗进程通过wait来获取该值

🤔exit VS _exit,二者是什么关系?区别?

——》💡exit与_exit的关系是上下层关系!_exit是系统调用,而exit是glibc封装_exit的函数,在exit内,最后也会调⽤_exit,但在调⽤_exit之前,还做了其他⼯作:

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

  2. 👉关闭所有打开的流,所有的缓存数据均被写入(刷新语言级缓冲区)

  3. 👉调⽤_exit

在这里插入图片描述

测试用例

#include <iostream>
#include <cstdio>
#include <unistd.h>

//第一个main
int main()
{
    printf("hello");
    exit(0);
}

运行结果:刷新了缓存区
Lvision@hcss-ecs-3f22:~/linex_-ubuntu/G-Linux$ ./test
hello
Lvision@hcss-ecs-3f22:~/linex_-ubuntu/G-Linux$ 

//第二个main
int main()
{
    printf("hello");
    _exit(0);
}
运行结果:未刷新缓存区
Lvision@hcss-ecs-3f22:~/linex_-ubuntu/G-Linux$ ./test
Lvision@hcss-ecs-3f22:~/linex_-ubuntu/G-Linux$ 

2️⃣异常退出

ctrl + c OS通过对进程发信号,进程执行信号的默认行为终止程序。可以是代码运行错误,os主动对进程发信号,杀掉进程;也可以是shell上输入kill命令,用户主动告诉os向进程发送对应信号。

3.进程等待

进程等待必要性:

  • 之前讲过,子进程退出,父进程如果不进行处理,就可能造成’僵尸进程”的问题,进而造成内存泄漏
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill-9也无能为力,因为谁也没有办法杀死一个已经死去的进程
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息,子进程就不会进入僵尸状态

进程等待作用:

  1. 回收子进程的僵尸状态
  2. 一直等待一个子进程,一直阻塞,直到该子进程退出,然后回收子进程

3.1 wait / waitpid

1️⃣ wait

Linux的man文档

在这里插入图片描述

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

	pid_t wait(int *wstatus);

💡说明:使用wait函数后,父进程会随机等待任意子进程。

——》返回值:成功则返回被等待进程pid,失败则返回-1
——》参数:wstatus为输出型参数,获取⼦进程退出状态不关心则可以设置成为NULL

2️⃣ waitpid

Linux的man文档
在这里插入图片描述

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

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

💡说明:输入指定子进程的pid,父进程会等待该pid

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

——》参数

  • pidPid= -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子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程则立即出错返回

测试用例1:父进程等待,子进程再退出。

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <errno.h>
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        printf("errno : %d, errstring: %s\n", errno, strerror(errno));
        return errno;
    }
    else if(id == 0) // 子进程
    {
        int cnt = 5;
        while(cnt)
        {
            printf("子进程运行中, pid: %d\n", getpid());
            cnt--;
            sleep(1);
        }
    }else
    {
        //设置输出型参数
        int status = 0;
        //使用waitpid
        pid_t rid = waitpid(id, &status, 0);
        //rid > 0 表示等待成功
        if(rid > 0)
        {
            //WIFEXITED(status):若为正常终止子进程返回的状态,则为真,否则为假
            if(WIFEXITED(status))
            {
                printf("子进程成功退出!, rid: %d, status code: %d\n", rid, WEXITSTATUS(status)); // ??
            }
            else
            {
                printf("子进程异常退出!\n");
            }
        }
        else
        {   //等待失败
            perror("waitpid");
        }

        while(true)
        {
            printf("我是父进程, pid: %d\n", getpid());
            sleep(1);
        }
    }
}

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

状态检测:

//状态检测指令
while :; do ps ajx | head -1 && ps ajx | grep wait ; sleep 1 ;echo "--------------------------" ; done

在这里插入图片描述

``
——》运行结果说明:fork之后,父进程等待子进程,在子进程运行结束之前一直阻塞在waitpid中;子进程退出后,没有变成Z状态存在内存中,直接被释放了。

测试用例2:子进程异常退出

 if(id == 0) // 子进程
    {
        int cnt = 5;
        while(cnt)
        {
            printf("子进程运行中, pid: %d\n", getpid());
            cnt--;
            sleep(1);
        }
        //异常行为
        int * ptr = nullptr;
        *ptr = 100;
    }e

运行结果
在这里插入图片描述
——》运行结果说明:异常退出,子进程也没有进入僵尸状态而被回收

测试用例3:子进程先退出,父进程再等待。
在这里插入图片描述

——》运行结果说明:子进程先退出,进入僵尸状态,父进程睡眠结束后,立即回收子进程。

3.2 获取子进程status

在这里插入图片描述

——》说明:wait和waitpid,都有一个status参数,该参数是一个输出型参数,执行完函数会被OS填充——》如果传递NULL,表示不关心子进程的退出状态信息。——》否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程

——》无论是否关心status,是否关心子进程的退出信息,都要进行wait操作,因为要回收子进程的僵尸状态!!

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

在这里插入图片描述

1️⃣正常退出的status

在这里插入图片描述

——》对应正常终止的子进程,status位图的高8位是退出码信息,通过位图的方式也可以成功输出子进程退出码——》WEXITSTATUS(status)同样是通过位图操作来获取子进程的退出码的!

——》对于被信号所杀掉而终止的进程,他的低7

测试代码1对应正常终止的子进程,验证是否可以用位图的方式来获取子进程的退出码

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <errno.h>
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        printf("errno : %d, errstring: %s\n", errno, strerror(errno));
        return errno;
    }
    else if(id == 0) // 子进程
    {
        int cnt = 5;
        while(cnt)
        {
            printf("子进程运行中, pid: %d\n", getpid());
            cnt--;
        }
        exit(123);
    }else //父进程
    {
        //先睡5s
        sleep(5);
        //设置输出型参数
        int status = 0;
        //使用waitpid
        pid_t rid = waitpid(id, &status, 0);
        //rid > 0 表示等待成功
        if(rid > 0)
        {
            //
            if(WIFEXITED(status))
            {
                printf("子进程成功退出!, rid: %d, status code: %d\n", rid, (status>>8) & 0xFF ); // ??
            }
            else
            {
                printf("子进程异常退出!\n");
            }
        }
        else
        {   //等待失败
            perror("waitpid");
        }

        while(true)
        {
            printf("我是父进程, pid: %d\n", getpid());
            sleep(1);
        }
    }
}

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

3.2.2 重谈进程退出

我们进程退出通常由三种情况:

  1. 代码跑完了,结果是对的,此时是return 0 ;
  2. 代码跑完了,结果是不对的,此时是return !0;
    ——》前两种结果的正确与否,通过退出码判定。
  3. 第三种就是,进程异常了,比如出现栈溢出,除0,野指针操作,OS用信号提前终止了你的进程,在进程退出信息中,会记录下来自己的退出信号。
2️⃣异常退出的status

在这里插入图片描述

——》异常退出的子进程通常是因信号被终止,此时status的底7位用于记录子进程被终止的信号

测试用例:通过kill指令让os向子进程发送信号通过位图获取子进程的终止信号

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
        printf("errno : %d, errstring: %s\n", errno, strerror(errno));
        return errno;
    }
    else if(id == 0) // 子进程
    {
        int cnt = 5;
        while(cnt)
        {
        	//子进程一直死循环
            printf("子进程运行中, pid: %d\n", getpid());
        }
        exit(123);
    }else //父进程
    {
        //先睡5s
        sleep(5);
        //设置输出型参数
        int status = 0;
        //使用waitpid
        pid_t rid = waitpid(id, &status, 0);
        //rid > 0 表示等待成功
        if(rid > 0)
        {
            
            if(WIFEXITED(status))
            {
            
                printf("子进程成功退出!, rid: %d, status code: %d\n", rid, (status>>8) & 0xFF ); 
            }
            else
            {	
            	//使用位图获取子进程的终止信号 
                printf("子进程异常退出!,退出信号为%d\n",status & 0x7F);
            }
        }
        else
        {   //等待失败
            perror("waitpid");
        }

        while(true)
        {
            printf("我是父进程, pid: %d\n", getpid());
            sleep(1);
        }
    }
}

在这里插入图片描述

3.2.4 Linux的信号列表

在这里插入图片描述
——》通过kill - l 可以查看。

3.2.5 阻塞与非阻塞等待

通过waitpid的第三个参数,可以设置waitpid的方式是阻塞等待还是非阻塞等待,我们上面的代码都是使用默认的阻塞等待。——》阻塞等待就是执行waitpid函数时,只要子进程不退出,父进程一直阻塞——》反之,非阻塞等待父进程就不会阻塞,会继续执行代码。

测试用例:设置第三个参数为 WNOHANG ,为⾮阻塞式等待;0则是阻塞等待

#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;
    pid = fork();
    if (pid < 0)
    {
        printf("%s fork error\n", __FUNCTION__);
        return 1;
    }
    else if (pid == 0)
    { // child
        printf("child is run, pid is : %d\n", getpid());
        sleep(5);
        exit(1);
    }
    else
    {
        int status = 0;
        pid_t ret = 0;
        do
        {
            //设置第三个参数为 WNOHANG ,为⾮阻塞式等待
            ret = waitpid(-1, &status, WNOHANG); // ⾮阻塞式等待

            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;
}

运行结果等待的同时,父子进程同时运行代码,等待成功后返回值给ret。

在这里插入图片描述

4.进程替换

我们知道,fork()之后,父子代码共享,父子各自执⾏父进程代码的⼀部分——》但如果子进程就想执行⼀个全新的程序呢,运行其他文件的代码进程的程序替换来完成这个功能

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

4.1 进程替换的原理

先来快速见一见,程序替换的简单使用:

#include <unistd.h>
#include <iostream>
int main()
{
	//我替换的程序为bin目录下的ls命令
    execl("/bin/ls","ls","-l","-a",nullptr);
    return 0;
}

运行结果:
在这里插入图片描述
——》我们发现,我自己写的代码执行后,竟然执行了ls命令!这其中就是程序替换的作用!

🤔他的原理是什么,他又是如何做到的?

——》当进程调用⼀种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的第一行代码开始执行——》但!是!程序替换不会替换PCB(task_struct),也就不会替换进程地址空间和页表,程序替换只替换代码区和数据区的相关数据!——》这说明,调⽤⼀种exec函数,是没有创建新进程的

在这里插入图片描述

——》没有创建新进程,进程的task_struct不变——》这意味着进程的pid,ppid等相关的进程信息也是不变的!程序替换后,父进程还是父进程,自己还是自己!

快速验证1:程序替换后pid不变

//other.c
#include <stdio.h>
#include <unistd.h>
int main()
{
    printf("%d\n", getpid());
}

//exec.cc
#include <unistd.h>
#include <iostream>
int main()
{
    printf("我是exec,我的pid为%d\n", getpid());
    //程序替换函数
    execl("./other","other",nullptr);
    return 0;
}

运行结果: 程序替换前后,pid都相同

在这里插入图片描述

快速验证2: exec函数返回值问题

在这里插入图片描述
——》exec的返回值只有 -1,表示替换失败…为什么exec没有替换成功后的返回值——》因为exec成功替换后,接下来的代码执行不会是原来的代码了,获取返回值没有意义;反之,如果接收到返回值并使用,就一定说明程序替换失败了!

替换成功

//替换成功 不会执行printf指令
#include <unistd.h>
#include <iostream>
int main()
{
    int n = execl("./bin/ls","-l","-a",nullptr);
    printf("返回值为%d\n",n);
    return 0;
}

在这里插入图片描述
替换失败

#include <unistd.h>
#include <iostream>
int main()
{
    int n = execl("/bin/lsssss","ls","-l","-a",nullptr);
    printf("替换失败!返回值为%d\n",n);
    return 0;
}

在这里插入图片描述

4.2 进程替换的函数

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

Linux的man文档
在这里插入图片描述

#include <unistd.h>

       extern char **environ;

       int execl(const char *pathname, const char *arg, ...
                       /* (char  *) NULL */ );
       int execlp(const char *file, const char *arg, ...
                       /* (char  *) NULL */);
       int execle(const char *pathname, const char *arg, ...
                       /*, (char *) NULL, char *const envp[] */);
       int execv(const char *pathname, char *const argv[]);
       int execvp(const char *file, char *const argv[]);
       int execvpe(const char *file, char *const argv[],
                       char *const envp[]);

4.2.1 函数解释

返回值
在这里插入图片描述

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

参数解释

  • pathname为文件路径(绝对路径或者相对路径都可以)。

  • arg为命令行参数,在命令行怎么写,参数就怎么传,最后以nullptr结尾
    在这里插入图片描述

  • file 带有该参数的exec,不用写全路径,该函数会在环境变量的路径下找file名的文件。

4.2.2 命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记。这些函数名以exec为基础,后面加 l , v , p , e,分别表达不同的意思和不同的功能

  • l(list) : 表示命令行参数采用列表的形式传
  • v(vector) : 参数用数组的形式传
  • p(path): 有p自动搜索环境变量PATH,在PATH下寻找文件。
  • e(env): 表示自己维护环境变量,需要多传一个自己维护的环境变量表。

测试用例

#include <unistd.h>
#include <iostream>
#include <sys/wait.h>

extern char **environ;
const std::string myenv = "HELLO=AAAAAAAAAAAAAAAAAAAA";
int main()
{

    // 为environ 添加新的环境变量
    putenv((char *)myenv.c_str());

    // 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // 调用 other 的命令行参数
        char *const argv1[] = {
            (char *)"other",
            nullptr};

        // 调用 ls 的命令行参数
        char *const argv2[] = {"ls", "-a", "-l", NULL};

        char *const env[] = {
            (char *)"HELLO=bite",
            (char *)"HELLO1=bite1",
            (char *)"HELLO2=bite2",
            (char *)"HELLO3=bite3"};

        // 1. l 参数采用列表方式传入
        // execl("/bin/ls", "ls", "-l", "--color", "-a", nullptr);

        // 2. v 参数采用数组方式传入
        // execv("/bin/ls", argv2);

        // 3. p 会在环境变量下查找
        // execlp("ls", "ls", "--color", "-aln", nullptr);

        // 4.e 传入自己维护的环境变量
        execvpe("./other", argv1, env);
        exit(1);
    }
    else if (id > 0)
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid > 0)
        {
            printf("等待子进程成功!\n");
        }
    }
}
  1. execl
    在这里插入图片描述

  2. execv
    在这里插入图片描述

  3. execlp
    在这里插入图片描述

  4. execvpe
    在这里插入图片描述

4.2.3 子进程环境变量
  1. 子进程环境变量的获取,一般是由父进程继承下来。程序替换后,环境变量表默认不变,命令行参数由传入的参数提供。
  2. 如果子进程需要传入全新的命令行参数表,则需要在带有e的exec函数中提供。
在这里插入代码片`#include <unistd.h>
#include <iostream>
#include <sys/wait.h>

const std::string myenv = "HELLO=AAAAAAAAAAAAAAAAAAAA";
int main()
{
   // 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
    	//全新的环境变量表
        char *const env[] = {
            (char *)"HELLO=bite",
            (char *)"HELLO1=bite1",
            (char *)"HELLO2=bite2",
            (char *)"HELLO3=bite3"};
        //带有e的exec函数中传入全新的环境变量表
        execvpe("./other", argv1, env);
        exit(1);
    }
    else if (id > 0)
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid > 0)
        {
            printf("等待子进程成功!\n");
        }
    }
}
  1. 如果需要在原有的环境变量表中添加新的环境变量呢?
#include <unistd.h>
#include <iostream>
#include <sys/wait.h>

extern char **environ;
int main()
{
    // 为environ 添加新的环境变量
    putenv((char *)myenv.c_str());

    // 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
    	//传入更新后的环境变量表
        execvpe("./other", argv1, environ);
        exit(1);
    }
    else if (id > 0)
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if (rid > 0)
        {
            printf("等待子进程成功!\n");
        }
    }
}
4.2.3 补充

在这里插入图片描述

——》这5个程序替换函数,实质上都不是真正的系统调用函数,他们是C语言封装的一系列程序替换接口,以传参方式的差别,满足不同的场景。

在这里插入图片描述
——》事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man⼿册第3节。

5. 自主Shell命令行解释器

补充概念内建命令

——》我们的命令行解释器解释命令后的大多数执行操作,都是让子进程去做的,即创建子进程,然后子进程替换对应的程序去执行。但是,还有一些命令是不能由子进程去做的,比如“ cd ” , 如果让子进程去做“cd”,子进程成功切换了工作目录,但是父进程还在原来的目录啊!“cd”命令不就无效了吗?所以,诸如“cd”这种需要父进程自己来执行的命令,被称为内建命令

shell模拟代码:

  • 不支持管道 | ,↑ ↓ 箭头切换历史命令。
  • 仅支持部分内建命令,解释一些简单的命令。
#include<iostream>
#include<cstring>
#include<cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//#include<direct.h>
using namespace std;

const int basesize = 1024; //命令行字符串大小
const int envnum = 64;//环境变量数量
const int argvnum = 64;
//全局命令行参数列表
char *gargv[argvnum];
int gargc = 0;

//当前进程环境变量表
char* genv[envnum];

// 当前工作路径
char pwd[basesize];
char pwdenv[basesize];


//全局的变量
int lastcode = 0; //上一条指令的退出码
void InitEnv()
{
    extern char ** environ;
    int index = 0;
    while (environ[index])
    {
        genv[index] = (char*)malloc(strlen(environ[index]) + 1);
        strncpy(genv[index],environ[index],strlen(environ[index])+1);
        index++;
    }
    genv[index] = nullptr;
}


string GetUser()
{
    string username = getenv("USER");
    return username.empty() ? "None" : username;
}


string GetHost()
{
	//我这台机器找不到HOSTNAME相关的环境变量,就直接用string了
    string hostname = "hcss-ecs-3f22";
    return hostname.empty() ? "None" : hostname;
}

string Getpwd()
{
    // /C:\Config
    // PWD=C:\Config
    if (getcwd(pwd,sizeof(pwd)) == nullptr) return "None";
    snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
    putenv(pwdenv);
    return pwd;
}


string LostHost()
{
    string pwd =  Getpwd();
    if (pwd == "/" || pwd == "None") return pwd;
    int index = pwd.rfind('/');
    if (index == std::string::npos)
    {
        return pwd;
    }
    return pwd.substr(index + 1);
}


string MakeCommandLine()
{
    char command_line[basesize];
    snprintf(command_line, basesize, "[%s@%s %s]$", GetUser().c_str(), GetHost().c_str(), LostHost().c_str());
    return command_line;
}


void PrintCommandLine() // 1. 命令行提示符
{
    printf("%s", MakeCommandLine().c_str());
    fflush(stdout);
}


bool GetCommandLine(char command_buffer[], int size)//2 获取命令行提示符
{
	char * result = fgets(command_buffer,size,stdin);
	if(!result)
	{
		return false;
	}
	command_buffer[strlen(command_buffer)-1] = 0;
	if(strlen(command_buffer) == 0 ) return false;
	return true;
}

void PraseCommand_line(char command_buffer[]) // 3 分析指令
{
	memset(gargv,0,sizeof(gargv));
	gargc = 0;
	const char * tep = " ";
	gargv[gargc++] = strtok(command_buffer,tep);
	while((bool)(gargv[gargc++] = strtok(nullptr,tep)));
	gargc--;
}

void debug()
{
    printf("argc: %d\n", gargc);
    for(int i = 0; gargv[i]; i++)
    {
        printf("argv[%d]: %s\n", i, gargv[i]);
    }
}

bool ExecuteCommand()//执行命令
{
	pid_t id = fork();
	if(id < 0) return false;
	if(id == 0){
		execvpe(gargv[0],gargv,genv);
		exit(1);
	}
	int status = 0;
	pid_t rid = waitpid(id,&status,0);
	if(rid > 0)
	{
		if(WIFEXITED(status))
		{
			lastcode = WIFEXITED(status);
		}
		else
		{
			lastcode = 114514;
		}
		return true;
	}
	return false;
}


void addenv(const char * item)
{
        int index = 0;
        while(genv[index])
        {
                index++;
        }
        genv[index] =(char *)malloc(strlen(item)+1);
        strncpy(genv[index], item , strlen(item) +1);
        index++;
        genv[index] = nullptr;
}


//检查是否为内建命令
bool CheckAndExecBuiltCommand()
{
	if(strcmp(gargv[0],"cd") == 0)
	{
		if(gargc == 2)
		{
			chdir(gargv[1]);
			lastcode = 0;
		}
		else
		{
			lastcode = 1;
		}
		return true;
	}
	else if(strcmp(gargv[0], "export") ==  0)
	{
		if(gargc == 2)
		{
			addenv(gargv[1]);
			lastcode = 0;
		}
		else
		{
			lastcode = 2;
		}
		return true;
	}
	else if(strcmp(gargv[0] , "env") == 0)
	{
		
		for(int  i = 0; genv[i] ; i++)
		{
			printf("%s\n",genv[i]);
		}
		return true;
	}
	else if(strcmp(gargv[0] , "echo") == 0)
	{
		//echo $?
		//echo wdf
		if(gargv[1][0] =='$' )
		{
			if(gargv[1][1] == '?')
			{
				printf("%d\n",lastcode);
				lastcode = 0;
			}
			else
			{
				lastcode = 114514;
			}
		}
		else
		{
			printf("%s\n",gargv[1]);
			lastcode = 0;
		}
		return true;
	}
	return false;
}



int main()
{
    InitEnv();
    char command_buffer[basesize];
    while (true)
    {
        PrintCommandLine();// 1. 打印命令行提示符
        if(!GetCommandLine(command_buffer,basesize))//2 获取命令行提示符
    	{
			continue;
		}
		PraseCommand_line(command_buffer);

		//3 分析指令
		if(CheckAndExecBuiltCommand() == 1)
		{
			continue;
		}
		//4 执行命令
		ExecuteCommand();
    }
    return 0;
}

运行结果实例:
在这里插入图片描述
本文就到这里,感谢你看到这里❤️❤️! 我知道一些人看文章喜欢静静看,不评论🤔,但是他会点赞😍,这样的人,帅气低调有内涵😎,美丽大方很优雅😊,明人不说暗话,要你手上的一个点赞😘!如果你不点赞的话,瓦达西😭😭!

十分十分感谢你能看完我这么长的“流水账”!希望你能从我的文章学到一点点的东西❤️❤️

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值