当前位置: 首页 > news >正文

Linux之进程控制进程终止进程等待进程的程序替换替换函数实现简易shell

文章目录

    • 一、进程创建
      • 1.1 fork的使用
    • 二、进程终止
      • 2.1 终止是在做什么?
      • 2.2 终止的3种情况&&退出码的理解
      • 2.3 进程常见退出方法
    • 三、进程等待
      • 3.1 为什么要进行进程等待?
      • 3.2 取子进程退出信息status
      • 3.3 宏WIFEXITED和WEXITSTATUS(获取进程终止情况和退出码)
      • 3.4 进程的阻塞和非阻塞等待
    • 四、进程的程序替换
      • 4.1 创建子进程的目的?
      • 4.2 进程的程序替换
        • 4.2.1 单个进程的程序替换
        • 4.2.2 父进程派生子进程的程序替换
      • 4.3 进程替换原理
    • 五、替换函数
    • 六、自己实现简易shell
      • 6.1 shell代码实现
      • 6.2 什么是当前路径?(当前进程的工作目录 && cd底层实现用chdir)
      • 6.3 shell内建/内置命令(shell自己执行的命令,而不是派生子进程进行程序替换来执行)

一、进程创建

1.1 fork的使用

  • 我们可以使用man指令来查看一下
man 2 fork
  • 子进程会复制父进程的PCB,之间代码共享,数据独有,拥有各自的进程虚拟地址空间。

在这里插入图片描述

  • 这里就有一个代码共享,并且子进程是拷贝了父进程的PCB,虽然他们各自拥有自己的进程虚拟地址空间,数据是拷贝过来的,通过页表映射到同一块物理内存中,在上篇文章【Linux进程地址空间详解】也讲解过写时拷贝,这里就不再过多赘述~
  • 大概流程可以看一下下图:

在这里插入图片描述

  • 在上图中我们还可以看到返回类型是pid_t如果创建子进程失败,会返回小于0的数字,而如果创建子进程成功,该函数则会返回俩个值。它会给子进程返回0值,而给父进程返回子进程的pid(一个大于0的数),创建成功后我们可以对此进行使用if语句进行分流~~

  • 下面简单验证再来验证一下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int g_val = 100;int main()
{pid_t pid = fork();if(pid < 0) {printf("fork error!\n");exit(-1); }else if(pid == 0) {//childg_val = 200;printf("This is Child! g_val = %d, p = %p\n",g_val,&g_val);}else {//parentsleep(1);printf("This is Parent! g_val = %d, p = %p\n",g_val,&g_val);}return 0;
}
  • 很显而易见,这里的地址是虚拟的地址空间,真正的值是存储在物理内存中的。而这时通过页表的映射,本质上内存中已经是指向了不同的物理地址

在这里插入图片描述

二、进程终止

顾名思义,就是直接让进程终止,我们来了解一下:

2.1 终止是在做什么?

  • 释放内核数据结构—>task_struct:Z(僵尸状态)

2.2 终止的3种情况&&退出码的理解

  1. 代码运行完毕,结果正确
  2. 代码运行完毕,结果不正确
  3. 代码异常终止

  • 在上面的1和2中可通过退出码进行决定,这里什么是退出码呢?我们在C语言每次写的时候为什么最后写一个return 0呢?这里我们可以实验一下:
#include <stdio.h>
int main()
{return 1;
}
  • 这里我们还要了解一个命令
echo $?
  • 作用是打印出上一次进程的退出码,而我们C语言刚刚最后写的退出码是1,最后记录了刚刚的退出码,所以打印的是1

在这里插入图片描述


  • 第三个是代码执行的时候出现了异常,被提前退出了,我们可以再来验证一下:
  • 下面这个代码很明显是野指针的访问:
int main()
{int* p = NULL;*p = 10;return 0;
}

在这里插入图片描述

  • 在编译运行的时候,出现了异常,提前退出了,操作系统发现了不该做的事情,OS杀死了进程

  • 一旦出现了异常,退出码也就没有意义了,那么为什么出现了异常,是因为进程收到了OS发给进程的信号

  • 在Linux中,可以使用kill -l查看所表示的信号,可以看到0表示成功~,所以一般正常运行完成之后退出码就写成0,非0表示失败

在这里插入图片描述

2.3 进程常见退出方法

  • 正常退出
  1. 从main函数返回
  2. 调用exit函数
  3. 调用_exit函数
  • 异常退出

Ctrl+C,信号终止等

  • 在我们平时使用的kill -9就是给OS发送一个信号,对程序做出动作

在这里插入图片描述

  • 例如,使用-9信号杀死进程

在这里插入图片描述

  • 刚刚上面的段错误就可以发送11
  • 每个对应的编号都有对应的错误描述

在这里插入图片描述


exit退出函数和_exit退出函数:

  • 可以使用man手册来查看
  • stauts定义了进程的终止状态,由用户自己传递,父进程可以通过wait来获取该值

在这里插入图片描述

  • exit是库函数,_exit是系统调用函数,而库函数内部封装了系统调用。 也就是说,调用exit函数最终也会调用_exit来使进程退出,只不过在其调用_exit之前,会将缓冲区进行刷新

return退出

  • return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返 回值当做 exit的参数。

衡量一个进程退出,我们只需要两个数字:退出码和退出信号

三、进程等待

3.1 为什么要进行进程等待?

  • 如果子进程先于父进程退出,而父进程并没有关心子进程的退出状况,从而无法回收子进程的资源,就会导致子进程变成僵尸进程,这里的僵尸状态使用kill也杀不掉,会导致内存泄露
  • 如果想要解决这个僵尸状态就要进行进程等待,等待父进程回收子进程的资源,获取子进程的退出状态

在父进程中,使用wait或waitpid接口来完成进程等待。

在这里插入图片描述

  • 这里的参数是一级指针status,它其实是个输出型参数,用于获取子进程的退出状态,如果不关心则可以设置为NULL
  • 成功会返回被等待进程的pid,失败则会返回-1
#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 processint cnt=10;while(cnt){printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);sleep(1);}exit(0);// 子进程退出}sleep(15);pid_t ret=wait(NULL);if(ret>0){printf("wait success:%d!\n",ret);}sleep(5);return 0;
}

在这里插入图片描述


waitpid:

  • 等待成功正常返回则返回被等待进程的pid
  • 如果第三个参数options设置成了WNOHANG,而此时没有子进程退出(没有成功等待到子进程),就会返回0,而不是阻塞在函数内部
  • 调用出错则返回-1

参数:

  • pid,设置成-1则表示等待任意一个子进程,同wait;如果>0则表示等待一个指定的子进程,pid就是被等待子进程的进程号
  • status,获取子进程的退出状态,同wait
  • options,可以设置为0或WNOHANG。设置为0则与wait一样,如果没有等待到子进程退出会一直阻塞;而设置为WNOHANG则表示非阻塞,如果被等待的子进程未退出,则会返回0值,成功等待到子进程则会返回被等待子进程的pid

这里就不再演示了~~

3.2 取子进程退出信息status

  • 我们已经知道status是一个出参,由操作系统为其赋值,用户可以传递NULL值表示不关心,而如果传入参数,操作系统就会根据该参数,将子进程的退出信息反馈给父进程,由status最终被赋予的值来体现。

  • 如何通过status来获取子进程的退出信息呢?

  • status是一个int类型的值,意味着它应该有32个比特位,但它又不能被当初普通的整形来看待,因为其高16位的值并不被使用,而只使用其低16个比特位:

在这里插入图片描述

  • 不论是正常退出还是异常退出,status的高8个比特位(只讨论低16个比特位)都表示子进程的退出码,而这个退出码一般是return的返回值或者exit的参数;正常退出时,status的低8个比特位为全0;而异常退出时,其第8个比特位则为core dump标志位,用来标志是否会有core dump文件产生,而低7个比特位则是退出信号。

退出码:(status >> 8) & 0xFF

低7位(检测子进程是否异常退出):status & 0x7F

  • 结果为0则表示正常退出

  • 不为0则说明是异常退出,因为有终止信号

  • core dump标志位:(status >> 7) & 0x1

  • 结果为0则表示没有core dump产生

  • 等于1则说明有core dump产生

#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 processint cnt=10;while(cnt){printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);sleep(1);}exit(6);// 子进程退出}sleep(15);int status = 0;pid_t ret=waitpid(id,&status,0);if(ret>0){printf("wait success:%d!\n",ret);printf("status:%d,退出码是%d,退出信号是%d\n",status,(status>>8)&0xFF,status&0x7F);}sleep(5);return 0;
}

在这里插入图片描述

  • 执行结果

在这里插入图片描述


  • 我们还可以演示一下异常退出
  • 我们在子进程里写一个野指针访问
int* p = NULL;
*p = 100; 
  • 这里也就很显而易见了~~

在这里插入图片描述

3.3 宏WIFEXITED和WEXITSTATUS(获取进程终止情况和退出码)

  • WIFEXITED(status):若子进程是正常终止,则返回结果为真,用于查看进程是否正常退出。

  • WEXITSTATUS(status):若进程正常终止,也就是进程终止信号为0,这时候会返回子进程的退出码。

下面我们可以写一个代码来演示一下

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
int main()
{pid_t id = fork();assert(id != -1);if(id == 0){// child int cnt = 10;while(cnt){printf("child process running,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);sleep(1);}exit(10);}// 等待子进程int status=0;int ret = waitpid(id,&status,0);if(ret > 0){if(WIFEXITED(status)){printf("child process exit normally,exit code:%d\n",WEXITSTATUS(status));}else{printf("child process don't exit normally\n");}// printf("wait success,exit code:%d,signal number:%d\n",(status>>8)&0xFF,status & 0x7F);}return 0;
}
  • 正常退出

在这里插入图片描述

  • 异常退出

在这里插入图片描述

3.4 进程的阻塞和非阻塞等待

  • 当子进程还没有死的时候,也就是没有退出的时候,父进程调用的wait或waitpit需要等待子进程退出,系统调用接口也不返回,这段时间父进程什么都没做,就一直等待子进程退出,这样的等待方式,称之为阻塞式等待。

  • 非阻塞式等待就是不停的检测子进程状态,每一次检测之后,系统调用立即返回,在waitpid中的第三个参数设置为WNOHANG,即为父进程非阻塞式等待

  • 如果等待的子进程状态没有发生变化,则waitpid会返回0值。多次非阻塞等待子进程,直到子进程退出,这样的等待方式又称之为轮询。如果等待的进程不是当前父进程的子进程,则waitpid会调用失败。

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
int main()
{pid_t id = fork();assert(id!=-1);if(id==0){// child processint cnt=5;while(cnt){printf("child process running,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);sleep(3);                                                                                                                                                                                          }exit(10);}int status=0;while(1){// WNOHANG是非阻塞等待,子进程没有退出,父进程检测一次之后,立即返回pid_t ret=waitpid(id,&status,WNOHANG);if(ret == 0){// waitpid调用成功,子进程没有退出printf("Wait for success,but the child process is still running\n");}else if(ret == id){// waitpid调用成功,子进程退出printf("wait success,exit code:%d,signal number:%d\n",(status>>8)&0xFF,status & 0x7F);break;}else{// waitpid调用失败,例如等待了一个不属于该父进程的子进程printf("The waitpid call failed\n");break;}sleep(1);}return 0;
}

在这里插入图片描述

  • 非阻塞等待有一个好处就是,不会像阻塞式等待一样,父进程什么都做不了,而是在轮询期间,父进程还可以做其他的事情。

  • 下面代码中,利用了回调函数的方式,来让父进程轮询等待子进程期间,还可以处理其他任务。

#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
void task1()
{printf("Process task1\n");
}
void task2()
{printf("Process task2\n");
}
void task3()
{printf("Process task3\n");
}
typedef void (*func_t)();// 定义一个函数指针类型。func_t Process_task[10];void loadtask()
{memset(Process_task,0, sizeof(Process_task));Process_task[0]=task1;Process_task[1]=task2;Process_task[2]=task3;
}int main()
{pid_t id = fork();assert(id!=-1);if(id==0){// child processint cnt=5;while(cnt){               printf("child process running,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);sleep(1);                                                                                                                                                                                          }exit(10);                                                                               }                loadtask();// 加载任务到函数指针数组里面。int status=0;while(1){                                         pid_t ret=waitpid(id,&status,WNOHANG);// WNOHANG是非阻塞等待,子进程没有退出,父进程检测一次之后,立即返回if(ret == 0) {// waitpid调用成功,子进程没有退出printf("Wait for success,but the child process is still running\n");for(int i=0; Process_task[i]!=NULL; i++){Process_task[i]();// 回调函数的方式,让父进程在轮询期间,做其他事情}}else if(ret == id){// waitpid调用成功,子进程退出printf("wait success,exit code:%d,signal number:%d\n",(status>>8)&0xFF,status & 0x7F);break;}else {// waitpid调用失败,例如等待了一个不属于该父进程的子进程printf("The waitpid call failed\n");break;}sleep(1);}return 0;
}

在这里插入图片描述

四、进程的程序替换

4.1 创建子进程的目的?

  • 创建子进程一般两个目的:
  1. 让子进程执行父进程代码的一部分,也就是执行父进程对应的磁盘上的代码和数据的一部分。

  2. 让子进程加载磁盘上指定的程序到内存中,使其执行新的程序的代码和数据,这就是进程的程序替换。

4.2 进程的程序替换

接下来我们就介绍一些进程的程序的替换函数

4.2.1 单个进程的程序替换
  • 下面函数参数是可变参数列表,可以给C语言函数传递不同个数的参数。
int execl(const char* path,const char* arg,...);  
  • 通过man指令可以查看
man execl

在这里插入图片描述

  • 要执行一个程序,首先就是找到这个程序,然后在执行这个程序,执行程序的时候,也拥有不同的执行方式,通过执行选项的不同便可以使得程序以多种不同的方式执行。

例如:

#include <stdio.h>
#include <unistd.h>int main()
{// .c --> .exe --> load into memory --> process --> runningprintf("The process is running...\n");// 传参以NULL结尾,来表示传参结束                                                        execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);printf("The process finishes running...\n");return 0;
}
  • 可以看到只打印了一行run,紧接着是执行后面替换的程序

在这里插入图片描述


  • exec系列的函数只有在调用失败的时候才有返回值,这个返回值是-1,那为什么exec系列的函数没有调用成功时的返回值呢?

  • 答案:没有必要,因为exec系列函数调用结束之后,代码就全都被替换了,就算给你返回值你也使用不了,因为代码全都替换为指定程序的代码了,所以只要exec系列函数返回,那就一定发生调用错误了。

例如:我随便写一个命令,这个命令是不在这个目录里的

#include <stdio.h>
#include <unistd.h>int main()
{// .c --> .exe --> load into memory --> process --> runningprintf("The process is running...\n");// 传参以NULL结尾,来表示传参结束                                                        execl("/usr/bin/lsss","ls","-l","--color=auto",NULL);printf("The process finishes running...\n");return 0;
}

在这里插入图片描述

4.2.2 父进程派生子进程的程序替换
  • 子进程被替换为ls进程,不会影响父进程,因为进程具有独立性。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{printf("The process is running...\n");pid_t id = fork();assert(id != -1);if(id==0){//child processsleep(1);execl("/usr/bin/ls","ls","-l",NULL);exit(1);// 如果调用失败,直接让子进程退出}int status = 0;pid_t ret = waitpid(id,&status,0);if(ret == id){printf("wait success, exit code:%d , signal number:%d\n",(status>>8)&0xFF,status&0x7F);}return 0;
}

在这里插入图片描述

4.3 进程替换原理

  • 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并**不创建新进程,**所以调用exec前后该进程的id并未改变。

在这里插入图片描述

  • 当父进程派生的子进程发生程序替换时,防止父子进程原先共享的代码段和数据段被修改,操作系统会进行写时拷贝,将代码段和数据段重新复制一份给子进程,让子进程程序替换之后,不会影响父进程。这就是进程之间的独立性。

  • 虚拟地址空间和页表可以保证进程之间的独立性,一旦有执行流要改变代码或数据,就会发生写时拷贝。所以不是只有数据可能发生写入,代码也是有可能发生写入的,这两种情况都会发生写时拷贝。

五、替换函数

  • l代表list,指的是将参数一个一个的传入execl函数

int execl(const char *path, const char *arg, …);

int main()
{pid_t id = fork();if(id == 0){// childexecl("/usr/bin/ls", "ls", "-l", NULL);// 如果执行到这里说明替换失败,让子进程退出exit(-1);}// parentreturn 0;
}
  • p是指path,不用传程序的路径,只需要传程序的名字就够了,此函数会自动在PATH环境变量的路径下面去查找对应的程序。
  • execlp中的两个ls是不重复的,一个是告诉操作系统要执行什么程序,一个是告诉操作系统怎么执行程序。

int execlp(const char *file, const char *arg, …);

int main()
{pid_t id = fork();if(id == 0){// childexeclp("ls", "ls", "-l", "--color=auto", NULL);// 如果执行到这里说明替换失败,让子进程退出exit(-1);}// parentreturn 0;
}
  • v是指vector,指的是该函数可以将所有的执行参数放到数组里面,统一进行传参,而不是使用可变参数列表的方式,来一个一个的传执行参数。

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

int main()
{pid_t id = fork();if(id == 0){// childchar* const argv[] = {(char*)"ls", (char*)"-l", (char*)"--color=auto", NULL};execv("/usr/bin/ls", argv);// 如果执行到这里说明替换失败,让子进程退出exit(-1);}// parentreturn 0;
}

PATH和vector,指的是不用传程序路径,默认在环境变量中进行查找并且可以将执行参数放到数组里面,统一进行传参

int execvp(const char *file, char *const argv[]);

int main()
{pid_t id = fork();if(id == 0){// childchar* const argv[] = {(char*)"ls", (char*)"-l", (char*)"--color=auto", NULL};execvp("ls", argv);// 如果执行到这里说明替换失败,让子进程退出exit(-1);}// parentreturn 0;
}
  • execle中的e代表自定义环境变量。
  • 下面定义的env指针数组就是自定义环境变量,也就意味着,程序替换的时候,不用系统环境变量,用自己定义的环境变量。

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

int main()
{pid_t id = fork();if(id == 0){// childchar* const env[] = {(char*)"HELLO=123456789",NULL};execle("./mybin","mybin", NULL, env);// 如果执行到这里说明替换失败,让子进程退出exit(-1);}// parentreturn 0;
}
  • 也可以不传自定义环境变量,而用系统的环境变量传给子进程替换的程序,只不过替换的程序mybin.c没有打印出来全部的环境变量,而是只打印了PATH和PWD的值。
int main()
{pid_t id = fork();if(id == 0){// childextern char** environ;execle("./mybin","mybin", NULL, environ);// 如果执行到这里说明替换失败,让子进程退出exit(-1);}// parentreturn 0;
}
  • 上面那些函数都不在2号手册

在这里插入图片描述

int execvpe(const char *file, char *const argv[],char *const envp[]);

  • execvpe其实就是vector+PATH+env,我们需要自己传环境变量,并且不用可变参数列表的方式传执行参数,而是用指针数组的方式来一并将执行参数传递,传程序名时可以不带程序路径,系统会帮我们找。

  • 带e的函数都需要自己组装环境变量,可以选择自己的、或系统的、或系统和自己的环境变量。

在这里插入图片描述

  • 真正执行程序替换的其实只有execve这一个系统调用接口,其他的6个都是在execve的基础上封装得来的。只有execve在man2号手册,其他都在3号手册。

在这里插入图片描述

下图exec函数族 一个完整的例子:

在这里插入图片描述


  • 其中 l 和 v 的区别在于程序运行参数的赋予方式不同l是通过函数参数逐个给与,最终以NULL结尾而v是通过字符指针数组一次性给与
  • 其中有没有 p 的区别在于程序是否需要带路径,也就是是否会默认到path环境变量指定的路径下寻找程序,没有p的需要指定路径,有p的会默认到path环境变量指定路径下寻找
  • 其中有没有 e 的区别在于程序是否需要自定义环境变量没有e则默认使用父进程环境变量,有e则自定义环境变量。

  • 最后在写makefile的时候我们想让两个源文件进行编译,我们可以在makefile中添加一个:
.PHONY:all
all:mybin myprocessmybin:myexec.cgcc -o $@ $^ -std=c99myprocess:process.cgcc -o $@ $^ -std=c99.PHONY:clean
clean:rm -rf myprocess mybin 
  • exec函数族代码示例:
int main()
{char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/bin/ps", "ps", "-ef", NULL);// 带p的,可以使用环境变量PATH,无需写全路径execlp("ps", "ps", "-ef", NULL);// 带e的,需要自己组装环境变量execle("ps", "ps", "-ef", NULL, envp);execv("/bin/ps", argv);// 带p的,可以使用环境变量PATH,无需写全路径execvp("ps", argv);// 带e的,需要自己组装环境变量execve("/bin/ps", argv, envp);exit(0);
}

六、自己实现简易shell

6.1 shell代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)// 为了方便,我就直接定义了
char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;void Die()
{exit(1);
}const char *GetHome()
{const char *home = getenv("HOME");if(home == NULL) return "/";return home;
}const char *GetUserName()
{const char *name = getenv("USER");if(name == NULL) return "None";return name;
}
const char *GetHostName()
{const char *hostname = getenv("HOSTNAME");if(hostname == NULL) return "None";return hostname;
}
const char *GetCwd()
{const char *cwd = getenv("PWD");if(cwd == NULL) return "None";return cwd;
}// commandline : output
void MakeCommandLineAndPrint()
{char line[SIZE];const char *username = GetUserName();const char *hostname = GetHostName();const char *cwd = GetCwd();SkipPath(cwd);snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);printf("%s", line);fflush(stdout);
}int GetUserCommand(char command[], size_t n)
{char *s = fgets(command, n, stdin);if(s == NULL) return -1;command[strlen(command)-1] = ZERO;return strlen(command); 
}void SplitCommand(char command[], size_t n)
{(void)n;gArgv[0] = strtok(command, SEP);int index = 1;while((gArgv[index++] = strtok(NULL, SEP))); 
}void ExecuteCommand()
{pid_t id = fork();if(id < 0) Die();else if(id == 0){// childexecvp(gArgv[0], gArgv);exit(errno);}else{// fahterint status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);}}
}void Cd()
{const char *path = gArgv[1];if(path == NULL) path = GetHome();// path 一定存在chdir(path);// 刷新环境变量char temp[SIZE*2];getcwd(temp, sizeof(temp));snprintf(cwd, sizeof(cwd), "PWD=%s", temp);putenv(cwd); // OK
}int CheckBuildin()
{int yes = 0;const char *enter_cmd = gArgv[0];if(strcmp(enter_cmd, "cd") == 0){yes = 1;Cd();}else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0){yes = 1;printf("%d\n", lastcode);lastcode = 0;}return yes;
}int main()
{int quit = 0;while(!quit){// 1. 我们需要自己输出一个命令行MakeCommandLineAndPrint();// 2. 获取用户命令字符串char usercommand[SIZE];int n = GetUserCommand(usercommand, sizeof(usercommand));if(n <= 0) return 1;// 3. 命令行字符串分割. SplitCommand(usercommand, sizeof(usercommand));// 4. 检测命令是否是内建命令n = CheckBuildin();if(n) continue;// 5. 执行命令ExecuteCommand();}return 0; 
}

6.2 什么是当前路径?(当前进程的工作目录 && cd底层实现用chdir)

  • 查看进程的指令:
ls /proc/进程id

在这里插入图片描述

  • 可以看到进程有两个路径,一个是cwd一个是exe,exe路径代表当前进程执行的是磁盘上的哪个路径下的程序,可以看到执行的是myproc二进制可执行程序,cwd代表current work directory,代表当前进程的工作目录,所以实际上当前路径就是当前进程的工作目录。

  • 在模拟shell的实现代码中,cd到其他目录,pwd之后的路径实际上是没有变化的,因为pwd实际上pwd的是父进程shell的路径,而父进程的cwd路径始终是未改变的,而执行cd命令的是子进程,所以子进程的cwd路径是会改变的。

  • 系统给我们提供了一个系统调用接口叫做chdir,用于改变当前进程的工作目录cwd路径,实际上cd能够进入指定路径下的目录,底层实现上就是改变了shell(bash)进程的cwd路径,所以pwd时,随时随地打印出来的就是shell进程的工作目录。

  • 所以如果我们模拟实现的shell也想实现cd改变路径的功能,实际上是不可以创建子进程的,因为子进程程序替换执行cd,父进程的工作目录是没有改变的,所以直接将这一种情况单独拿出来进行判断,在这种情况下,直接让父进程执行cd命令,修改父进程的工作目录即可。

6.3 shell内建/内置命令(shell自己执行的命令,而不是派生子进程进行程序替换来执行)

在这里插入图片描述

  • 像上面的cd命令实际上就是shell的内建命令,因为这样的命令不需要派生子进程来进行程序替换执行,直接让父进程执行就ok,这样的指令就是shell自带的命令,我们称之为内建命令或内置命令。

  • 这也就能解释为什么echo能够打印本地变量了,我们之前将echo理解为一个可执行程序,也就是shell的子进程,但是我们说子进程只能继承父进程的环境变量,而不能继承本地变量,所以当时就陷入echo为什么能够打印出本地变量的疑问当中,因为如果echo是子进程的话,他是没有继承本地变量的。

  • 但现在我们就知道原因了,echo实际上不是shell的子进程,而是shell的内建命令,是shell自己来执行的指令,shell当然拥有本地变量了,当然也就能够打印本地变量。

好了,本文就到这里,感谢大家的收看🌹🌹🌹

相关文章:

Linux之进程控制进程终止进程等待进程的程序替换替换函数实现简易shell

文章目录 一、进程创建1.1 fork的使用 二、进程终止2.1 终止是在做什么&#xff1f;2.2 终止的3种情况&&退出码的理解2.3 进程常见退出方法 三、进程等待3.1 为什么要进行进程等待&#xff1f;3.2 取子进程退出信息status3.3 宏WIFEXITED和WEXITSTATUS&#xff08;获取…...

RegSeg 学习笔记(待完善)

论文阅读 解决的问题 引用别的论文的内容 可以用 controlf 寻找想要的内容 PPM 空间金字塔池化改进 SPP / SPPF / SimSPPF / ASPP / RFB / SPPCSPC / SPPFCSPC / SPPELAN &#xfffc; ASPP STDC&#xff1a;short-term dense concatenate module 和 DDRNet SE-ResNeXt …...

Qt中常用宏定义

Qt中常用宏定义 一、Q_DECLARE_PRIVATE(Class)二、Q_DECLARE_PRIVATE_D(Dptr, Class)三、Q_DECLARE_PUBLIC(Class)四、Q_D(Class) 和 Q_Q(Class) 一、Q_DECLARE_PRIVATE(Class) #define Q_DECLARE_PRIVATE(Class) inline Class##Private* d_func() { # 此处的 d_ptr 是属于QOb…...

【计算机网络】第 9 问:四种信道划分介质访问控制?

目录 正文什么是信道划分介质访问控制&#xff1f;什么是多路复用技术&#xff1f;四种信道划分介质访问控制1. 频分多路复用 FDM2. 时分多路复用 TDM3. 波分多路复用 WDM4. 码分多路复用 CDM 正文 什么是信道划分介质访问控制&#xff1f; 信道划分介质访问控制&#xff08;…...

Rust编程(五)终章:查漏补缺

闭包 & 迭代器 闭包&#xff08;Closure&#xff09;通常是指词法闭包&#xff0c;是一个持有外部环境变量的函数。外部环境是指闭包定义时所在的词法作用域。外部环境变量&#xff0c;在函数式编程范式中也被称为自由变量&#xff0c;是指并不是在闭包内定义的变量。将自…...

LLM漫谈(五)| 从q star视角解密OpenAI 2027年实现AGI计划

最近&#xff0c;网上疯传OpenAI2027年关于AGI的计划。在本文&#xff0c;我们将针对部分细节以第一人称进行分享。​ 摘要&#xff1a;OpenAI于2022年8月开始训练一个125万亿参数的多模态模型。第一个阶段是Arrakis&#xff0c;也叫Q*&#xff0c;该模型于2023年12月完成训练&…...

【echart】数据可视化+vue+vite遇到问题

1、vue3使用echars图表报错&#xff1a;"Initialize failed:invalid dom" 原因是因为&#xff1a;Dom没有完成加载时&#xff0c;echarts.init() 就已经开始执行了&#xff0c;获取不到Dom&#xff0c;无法进行操作 解决&#xff1a;加个延时 onMounted(async () …...

mac m1安装和使用nvm的问题

mac m1安装和使用nvm的问题 使用nvm管理多版本node 每个项目可能用的node版本不同&#xff0c;所以需要多个版本node来回切换 但是最近遇到安装v14.19.0时一直安装失败的问题。 首先说明一下&#xff0c;用的电脑是mac M1芯片 Downloading and installing node v14.19.0... …...

git泄露

git泄露 CTFHub技能树-Web-信息泄露-备份文件下载 当前大量开发人员使用git进行版本控制&#xff0c;对站点自动部署。如果配置不当,可能会将.git文件夹直接部署到线上环境。这就引起了git泄露漏洞。 工具GitHack使用&#xff1a;python2 GitHack.py URL地址/.git/ git命令…...

Java项目:78 springboot学生宿舍管理系统的设计与开发

作者主页&#xff1a;源码空间codegym 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 系统的角色&#xff1a;管理员、宿管、学生 管理员管理宿管员&#xff0c;管理学生&#xff0c;修改密码&#xff0c;维护个人信息。 宿管员…...

ArcGis Pro Python工具箱教程 03 工具箱中工具自定义

ArcGis Pro Python工具箱教程 03 工具箱中工具自定义 用于定义工作工具类的方法 工具方法必选或可选描述__ init __必需项right-aligned 初始化工具类。getParameterInfo可选定义工具的参数。isLicensed可选返回工具是否获得执行许可。updateParameters可选在用户每次在工具对…...

【C++初阶】之类和对象(中)

【C初阶】之类和对象&#xff08;中&#xff09; ✍ 类的六个默认成员函数✍ 构造函数&#x1f3c4; 为什么需要构造函数&#x1f3c4; 默认构造函数&#x1f3c4; 为什么编译器能自动调用默认构造函数&#x1f3c4; 自己写的构造函数&#x1f3c4; 构造函数的特性 ✍ 拷贝构造…...

Vue2(十一):脚手架配置代理、github案例、插槽

一、脚手架配置代理 1.回顾常用的ajax发送方式&#xff1a; &#xff08;1&#xff09;xhr 比较麻烦&#xff0c;不常用 &#xff08;2&#xff09;jQuery 核心是封装dom操作&#xff0c;所以也不常用 &#xff08;3&#xff09;axios 优势&#xff1a;体积小、是promis…...

在宝塔面板中,为自己的云服务器安装SSL证书,为所搭建的网站启用https(主要部分攻略)

前提条件 My HTTP website is running Nginx on Debian 10&#xff08;或者11&#xff09; 时间&#xff1a;2024-3-28 16:25:52 你的网站部署在Debain 10&#xff08;或者11&#xff09;的 Nginx上 安装单域名证书&#xff08;默认&#xff09;&#xff08;非泛域名&#xf…...

学习JavaEE的日子 Day32 线程池

Day32 线程池 1.引入 一个线程完成一项任务所需时间为&#xff1a; 创建线程时间 - Time1线程中执行任务的时间 - Time2销毁线程时间 - Time3 2.为什么需要线程池(重要) 线程池技术正是关注如何缩短或调整Time1和Time3的时间&#xff0c;从而提高程序的性能。项目中可以把Time…...

@Transactional 注解使用的注意事项

事务管理 事务管理在系统开发中是不可缺少的一部分&#xff0c;Spring提供了很好的事务管理机制&#xff0c;主要分为编程式事务和声明式事务两种。 编程式事务&#xff1a; 是指在代码中手动的管理事务的提交、回滚等操作&#xff0c;代码侵入比较强。 声明式事务&#xff…...

电商系列之库存

> 插&#xff1a;AI时代&#xff0c;程序员或多或少要了解些人工智能&#xff0c;前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 坚持不懈&#xff0c;越努力越幸运&#xff0c;大家…...

Apache HBase(二)

目录 一、Apache HBase 1、HBase Shell操作 1.1、DDL创建修改表格 1、创建命名空间和表格 2、查看表格 3、修改表 4、删除表 1.2、DML写入读取数据 1、写入数据 2、读取数据 3、删除数据 2、大数据软件启动 一、Apache HBase 1、HBase Shell操作 先启动HBase。再…...

【设计模式】原型模式详解

概述 用一个已经创建的实例作为原型&#xff0c;通过复制该原型对象来创建一个和原型对象相同的新对象 结构 抽象原型类&#xff1a;规定了具体原型对象必须实现的clone()方法具体原型类&#xff1a;实现抽象原型类的clone()方法&#xff0c;它是可以被复制的对象。访问类&…...

企微侧边栏开发(内部应用内嵌H5)

一、背景 公司的业务需要用企业微信和客户进行沟通&#xff0c;而客户的个人信息基本都存储在内部CRM系统中&#xff0c;对于销售来说需要一边看企微&#xff0c;一边去内部CRM系统查询&#xff0c;比较麻烦&#xff0c;希望能在企微增加一个侧边栏展示客户的详细信息&#xf…...

如何确定最优的石油管道位置

如何确定最优的石油管道位置 一、前言二、问题概述三、理解问题的几何性质四、转化为数学问题五、寻找最优解六、算法设计6.1伪代码6.2 C代码七算法效率和实际应用7.1时间效率分析7.2 空间效率分析结论一、前言 当我们面对建设大型输油管道的复杂任务时,确保效率和成本效益是…...

FPGA 图像边缘检测(Canny算子)

1 顶层代码 timescale 1ns / 1ps //边缘检测二阶微分算子&#xff1a;canny算子module image_canny_edge_detect (input clk,input reset, //复位高电平有效input [10:0] img_width,input [ 9:0] img_height,input [ 7:0] low_threshold,input [ 7:0] high_threshold,input va…...

2024.3.28学习笔记

今日学习韩顺平java0200_韩顺平Java_对象机制练习_哔哩哔哩_bilibili 今日学习p286-p294 继承 继承可以解决代码复用&#xff0c;让我们的编程更加靠近人类思维&#xff0c;当多个类存在相同的属性和方法时&#xff0c;可以从这些类中抽象出父类&#xff0c;在父类中定义这些…...

33.HarmonyOS App(JAVA)鸿蒙系统app数据库增删改查

33.HarmonyOS App(JAVA)鸿蒙系统app数据库增删改查 关系数据库 关系对象数据库&#xff08;ORM&#xff09; 应用偏好数据库 分布式数据库 关系型数据库&#xff08;Relational Database&#xff0c;RDB&#xff09;是一种基于关系模型来管理数据的数据库。HarmonyOS关系型…...

寄主机显示器被快递搞坏了怎么办?怎么破?

大家好&#xff0c;我是平泽裕也。 最近&#xff0c;我在社区里看到很多关于开学后弟弟寄来的电脑显示器被快递损坏的帖子。 看到它真的让我感到难过。 如果有人的数码产品被快递损坏了&#xff0c;我会伤心很久。 那么今天就跟大家聊聊寄快递的一些小技巧。 作为一名曾经的…...

python爬虫-bs4

python爬虫-bs4 目录 python爬虫-bs4说明安装导入 基础用法解析对象获取文本Tag对象获取HTML中的标签内容find参数获取标签属性获取所有标签获取标签名嵌套获取子节点和父节点 说明 BeautifulSoup 是一个HTML/XML的解析器&#xff0c;主要的功能也是如何解析和提取 HTML/XML 数…...

SpringBoot学习之ElasticSearch下载安装和启动(Mac版)(三十一)

本篇是接上一篇Windows版本,需要Windows版本的请看上一篇,这里我们继续把Elasticsearch简称为ES,以下都是这样。 一、下载 登录Elasticsearch官网,地址是:Download Elasticsearch | Elastic 进入以后,网页会自动识别系统给你提示Mac版本的下载链接按钮 二、安装 下载…...

OC对象 - Block解决循环引用

文章目录 OC对象 - Block解决循环引用前言1. 循环引用示例1.1 分析 2. 解决思路3. ARC下3.1 __weak3.2 __unsafe_unretained3.3 __block 4. MRC下4.1 __unsafe_unretain....4.1 __block 5. 总结5.1 ARC下5.2 MRC下 OC对象 - Block解决循环引用 前言 本章将会通过一个循环引用…...

Java设计模式之装饰器模式

装饰器模式是一种结构型设计模式&#xff0c;它允许动态地将责任附加到对象上。装饰器模式是通过创建一个包装对象&#xff0c;也就是装饰器&#xff0c;来包裹真实对象&#xff0c;从而实现对真实对象的功能增强。装饰器模式可以在不修改原有对象的情况下&#xff0c;动态地添…...

Java基础知识总结(25)

代理模式 什么是代理模式&#xff1f; 代理模式是指&#xff0c;为其他对象提供一种代理以控制这个对象的访问。一个对象不适合或者不能直接引用另一个对象&#xff0c;而代理对象可以在客户和目标对象之间起到中介的作用。换句话说&#xff0c;代理模式&#xff0c;是在不修…...

营销型网站制作多少钱/google推广seo

&#xff0c;神经网络是一种模拟人脑的神经网络&#xff0c;以期能够实现类人工智能的机器学习技术。 一.预测器和分类器 1.什么叫预测器:一个简单的机器接受了一个输入&#xff0c;并做出应有的预测&#xff0c;输出结果&#xff0c;所以我们将其称为预测器。 2.什么叫分类器:…...

wordpress增加搜索框/邮件营销

★点开后电脑将在某一时间段后自动关机&#xff0c;输入如同“我是猪”一类的话语&#xff0c;即可结束&#xff0c;&#xff0c;&#xff0c;否则关机任务则会一直处于倒计时状态。。#include<stdio.h> //标准输入输出库函数 #include<stdlib.h> //可以输入…...

谷歌seo和sem/温州网站优化推广方案

C#基础知识梳理系列索引 摘 要这个系列&#xff0c;将从山脚写到山腰。由于鄙人知识匮乏&#xff0c;不敢奢望攀登山顶。 C# Target Runtime: v4.0.30319 系列索引C#基础知识梳理系列一&#xff1a;CLR及程序集部署 C#基础知识梳理系列二&#xff1a;C#的演绎大师&#xff1a;类…...

phpstudy 搭建wordpress/seo综合查询平台

转自&#xff1a;http://blog.sina.com.cn/s/blog_6a656bb40102dtv2.html 一个小和尚问方丈&#xff1a;“师父&#xff0c;我念经的时候可以吸烟吗&#xff1f;”方丈怒道&#xff1a;“不行&#xff01;” 另一小和尚问&#xff1a;“师父&#xff0c;我吸烟的时候可以念经吗…...

上海网页设计公司怎么样/网络建站优化科技

1.刘未鹏 | Mind Hacks 文章质量很高&#xff0c;涉及算法、数学和计算机&#xff0c;很值得一看转载于:https://www.cnblogs.com/cpointer/p/4644833.html...

网站建设 博采/免费建站哪个最好

mongodb备份数据 /mongodb/bin/mongodump -h localhost -d 数据库名 -o /tmp/&#xff08;输出目录&#xff09; -u 用户名-p 密码 mongdb恢复数据 /mongodb/bin/mongorestore -h localhost -d 数据库名--dir /tmp/authService&#xff08;备份目录&#xff09; -u 用户名-p 密…...