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

【Liunx】进程的程序替换——自定义编写极简版shell

目录

  • 进程程序替换[1~5]
    • 1.程序替换的接口(加载器)
    • 2.什么是程序替换?
    • 3.进程替换的原理
    • 4.引入多进程
    • 5.系列程序替换接口的详细解析(重点!)
  • 自定义编写一个极简版shell[6~8]
    • 6.完成命令行提示符
    • 7.获取输入的命令行字符串
    • 8.完整的代码与测试效果
    • 9.补充——内建命令

进程程序替换[1~5]

创建子进程的目的是:
1.让子进程执行父进程的一部分代码;
2.让子进程执行一个全新的程序代码——进程的程序替换

1.程序替换的接口(加载器)

程序替换的接口有:
在这里插入图片描述
在这里插入图片描述

(environ是环境变量表的指针,在环境变量讲过)

头文件:
#include <unistd.h>
函数:
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 execve(const char *path, char *const argv[], char *const envp[]);


返回值:
调用成功,没有返回值;
调用出错,返回-1;(比如替换不存在的程序就会失败)
所以execl系列函数只有出错的返回值而没有成功的返回值
即:只要有返回值,就失败了。


关于execl系列函数的使用:
因为这些程序替换函数如果替换成功,后面我们自己的程序就会被替换不再执行,会执行替换的程序。
所以我们一般不用对这些函数的返回值做判断,在使用完这些函数后,直接使用exit(1)退出就可以了,因为替换成功了不会执行退出函数,替换失败了就会执行exit(1)异常退出!


补充:
参数中的“…”表示的是可变参数列表,可变参数列表的作用是可以给函数传递任意个数个参数。
比如这个函数,前面两个指明的参数时必须传的,但是后面的参数可以不传,也可以任意个数去传。(其实与平时调用printf一样,逗号后面的参数需要多少个就传多少个)

2.什么是程序替换?

——用一段简单的代码来解释:

//测试代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{printf("begin...\n");printf("我是一个进程了,我的PID:%d\n", getpid());execl("/bin/ls", "ls", "-a", "-l", NULL);printf("end...\n");
}

结果:
在这里插入图片描述
可以观察到,上面执行的是自己写的程序,后面执行的是ls命令。
ls是一个磁盘中的可执行程序,也就是一个文件,当我们自己的进程运行时,在内存中有自己的pcb和代码数据,刚开始执行的时候,执行的使我们自己的代码和数据,当执行execl时,就将磁盘中的ls可执行程序替换到当前进程的代码和数据中(main函数中老的代码和数据全都被替换了),所以后面执行的就不是我们自己的代码和数据了,执行的是替换后的ls的代码和数据,所以后面我们自己程序中的"end…"也没有打印出来。

3.进程替换的原理

在程序替换的时候,进程的数据和代码直接被新的程序所替换,如下图所示,并且直接换物理内存,左边的映射不变。
并且在进程替换的时候,并没有创建新的子进程,只是将当前的进程进行了替换,让CPU去调度当前进程就可以运行了,进程的内核pcb和虚拟地址空间都没有发生变化。
在这里插入图片描述

4.引入多进程

1.程序替换是整体替换,不能局部替换
意思就是当前进程调用了程序替换接口,则当前进程全部的代码和数据都会被替换成新的。

2.程序替换只会影响调用的进程,因为进程具有独立性。
虽然父子进程通过页表指向同样的代码和数据,但是当子进程发生进程替换的时候,会发生写时拷贝,将父子进程进行区分,就不会影响父进程了。(代码区和数据区全都发生写时拷贝)

例如下面的代码:只替换子进程的程序:

#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 == 0){printf("我是子进程:%d\n", getpid());execl("/bin/ls", "ls", "-a", "-l", NULL);//1.这里注释的是下面会替换失败的情况,这种情况父进程获取退出码为1//execl("/bin/lssssss", "ls", "-a", "-l", NULL);//2.这里注释的是下面会替换成功但是替换的程序内部出错的情况,这种情况父进程获取退出码为2//execl("/bin/ls", "ls", "-abcdefg", "-l", NULL);exit(1);}sleep(5);int status = 0;printf("我是父进程:%d\n", getpid());waitpid(id, &status, 0);printf("child exit code:%d\n", WEXITSTATUS(status));return 0;
}

运行结果:
成功父子进程正常运行,子进程执行替换程序;
失败父子进程正常运行,子进程执行原有代码;
在这里插入图片描述

解析:
如果程序替换成功,则父进程waitpid获取到替换进程的退出码“0”;
如果程序替换失败,则父进程waitpid获取到原来进程的退出码“1”;
如果程序替换成功,但是替换成功的程序选项错误,这时的错误退出码也是由替换后的程序返回的,就比如前面代码中的成功但是退出码是2的情况。

5.系列程序替换接口的详细解析(重点!)

这些所有接口都符合前面所讲的性质,只是用法略有差异。

头文件:
#include <unistd.h>
函数:
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 execve(const char *path, char *const argv[], char *const envp[]);

  1. int execl(const char *path, const char *arg, …) ——“l”表示list,以列表方式一个个传参。
    path——想要执行程序的路径(比如ls,就传"/bin/ls");
    arg——执行该程序的方式,(在命令行怎么执行就怎么传参,比如ls -a -l就传"ls", "-a", "-l")参数最后必须以NULL结尾;
    例:execl("/bin/ls", "ls", "-a", "-l", NULL);

  1. int execv(const char *path, char *const argv[]) ——“v”表示vector,以数组方式一次性传参。
    path——想要执行程序的路径(比如ls,就传"/bin/ls");
    argv[]——传参以指针数组方式传(在命令行怎么执行数组中就怎么写,比如可以定义一个指针数组:char *myargv[] = {"ls", "-a", "-l", NULL};),传入的数组最后以NULL结尾;
    例:
char *myargv[] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", myargv);

  1. int execlp(const char *file, const char *arg, …) ——“p”表示环境变量PATH,会自动在PATH中查找。
    file——想要执行的程序,不用带路径(比如ls,就传"ls");
    arg——与execl的arg传入参数方式完全一样。(比如ls -a -l就传"ls", "-a", "-l");
    例:execlp("ls", "ls", "-a", "-l", NULL);

4.int execvp(const char *file, char *const argv[]) ——“v”与“p”的意思和上面的一样。
file——想要执行的程序,不用带路径(比如ls,就传"ls");
argv[]——传参以指针数组方式传。(与execv一样);
例:execvp("ls", myargv);


  1. int execle(const char *path, const char *arg, …,char *const envp[]) ——父进程给子进程手动传环境变量。
    path——想要执行程序的路径。
    arg——执行该程序的方式,一个一个传入。
    envp[]——自定义环境变量,以NULL结尾,比如:char *const myenv[] = {"MYENV=YouCanSeeMe", NULL};
    解析:可以通过这些接口调用自己写好的其他可执行程序。(这些程序可以不是C程序,其他语言都可以调用

例如:目录如下如所示,我们要通过myproc调用otherproc,并在otherproc中获取myproc传给它的环境变量MYENV。
在这里插入图片描述

//myproc.c
#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 == 0){printf("我是子进程:%d\n", getpid());char *const myenv[] = {"MYENV=YouCanSeeMe", NULL};//自定义环境变量execle("./Otherproc/otherproc", "otherproc", NULL, myenv);//传入自定义环境变量exit(1);}sleep(5);int status = 0;printf("我是父进程:%d\n", getpid());waitpid(id, &status, 0);printf("child exit code:%d\n", WEXITSTATUS(status));return 0;
}
//otherproc.cc
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
using namespace std;int main()
{for(int i = 0; i < 5; i++){cout << "这是子进程, PID : " << getpid() << " MYENV : " << (getenv("MYENV")==NULL? "NULL" : getenv("MYENV")) << " PATH : " << (getenv("PATH")==NULL? "NULL" : getenv("PATH")) << endl;sleep(1);}return 0;
}

运行结果:可以看到子进程获取了父进程传入的自定义环境变量。
在这里插入图片描述

注意:自定义环境变量envp是覆盖式传入,之前的老的会被覆盖!

如果我们想把父进程的环境变量原封不动的传给子进程,那么就用environ(之前讲过的获取环境变量的方法之一)传入即可。
用法:在父进程创建extern char **environ;后,直接传入。
比如:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{extern char **environ;//获取父进程环境变量,下面通过execle传入pid_t id = fork();if(id == 0){printf("我是子进程:%d\n", getpid());execle("./Otherproc/otherproc", "otherproc", NULL, environ);exit(1);}sleep(5);int status = 0;printf("我是父进程:%d\n", getpid());waitpid(id, &status, 0);printf("child exit code:%d\n", WEXITSTATUS(status));return 0;
}

运行结果:发现子进程获取了父进程的默认环境变量PATH。
在这里插入图片描述

如果我们想保留原来父进程的环境变量,并且在此基础上增加自定义环境变量呢?
——int putenv(char *string);那个进程调用这个函数,就在当前的进程中新增一个环境变量。
头文件:#include <stdlib.h>。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{extern char **environ;pid_t id = fork();if(id == 0){printf("我是子进程:%d\n", getpid());putenv("MYENV=YouCanSeeMe");execle("./Otherproc/otherproc", "otherproc", NULL, environ);exit(1);}sleep(5);int status = 0;printf("我是父进程:%d\n", getpid());waitpid(id, &status, 0);printf("child exit code:%d\n", WEXITSTATUS(status));return 0;
}

我们的子进程otherproc默认继承的是父进程proc的环境变量,那么父进程的环境变量从哪里来?
——bash,所以我们也可以不用putenv来追加环境变量,直接在命令行输入export MYENV=YouCanSeeMe,来给bash添加一个环境变量,这样默认传入environ的时候子进程也会接收到MYENV这个环境变量了。


  1. int execve(const char *path, char *const argv[], char *const envp[]);
    发现这个接口在man手册中被单独放出来了,和前面的有什么区别?
    ——接口用法的规则和前面的几个一模一样,类推即可不再详细解析。
    那么区别是:这个是真正提供的的系统调用接口,上面的所有都是对这个的封装!

自定义编写一个极简版shell[6~8]

bash就是一个进程:
在这里插入图片描述

6.完成命令行提示符

bash会接收我们输入的命令行字符串,并且不会退出,一直在为我们打印命令行提示符。

一个命令行提示符包括:
[用户名 + @ + 主机名 + 当前路径的名称]$
在这里插入图片描述
每个字段都可以通过对应的系统调用获取,但是现在对我来说意义不大,这里直接打印,就不使用了。

//实现命令行提示符:
int main()
{while(1){printf("[YGH@MyMachina CurrentPath]#");//命令行的输入是在当前行输入,不能用\nfflush(stdout);//不用\n不会行刷新出来,所以要用fflush刷新缓冲区sleep(100);//测试观察,这句之后不要}
}

7.获取输入的命令行字符串

使用接口fget——从特定的标准输入stream当中获取命令行输入。

接口函数:char *fgets(char *s, int size, FILE *stream);
头文件:stdio.h
手册查询:
在这里插入图片描述

1.首先我们自己定一个一个命令行commandstr,然后通过fgets获取输入的命令行字符串:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>#define MAX 1024int main()
{char commandstr[MAX] = {0};while(1){printf("[YGH@MyMachina CurrentPath]# ");//命令行的输入是在当前行输入,不能用\nfflush(stdout);//不用\n不会行刷新出来,所以要用fflush刷新缓冲区char *s = fgets(commandstr, sizeof(commandstr), stdin);//输入的时候最后缓冲区会输入一个\nassert(s);//断言在debug方式的时候存在,release方式会被裁掉(void)s;//这句的用处是让断言在release方式下也不会失效(因为assert被去掉了,s没有被使用会报警,这里用一下就不会报警了)commandstr[strlen(commandstr) - 1] = '\0';//去掉我们缓冲区中输入的\nprintf("%s\n", commandstr);//测试观察一下,这句之后不要}
}

2.然后我们也不能自己来执行命令行输入要执行的程序,因为如果进程替换了我们作为命令行自己就会被替换掉了,这样是不合理的,所以肯定需要子进程来处理。
即父进程把命令给子进程,然后父进程等待结果就行了。
同时我们的自定义缓冲区中输入的是一整个连续的字符串,需要切割成单个的命令传入,比如:“ls -a -l” 要切割成: “ls”, “-a”, “-l”。
——可以自己切割,也可以使用字符串分割函数:char* strtok(char* str, const char* sep);
进行字符串的切割:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAX 1024
#define ARGC 64
#define SEP " "//分隔符空格//字符串切割函数
int split(char* commandstr, char* argv[])
{assert(commandstr);assert(argv);argv[0] = strtok(commandstr, SEP);//函数切割后返回第一个地址if(argv[0] == NULL){return 1;}int i = 1;while(argv[i++] = strtok(NULL, SEP));//这一句等价于下面的一段代码//while(1)//{//        argv[i] = strtok(NULL, SEP);//NULL表示还是继续切割commandstr//        if(argv[i] == NULL)//        {//            break;//        }//        i++;//}return 0;
}int main()
{while(1){char commandstr[MAX] = {0};//放在循环内部每次输入后重新创建缓冲区判断char* argv[ARGC] = {NULL};//存放切割后的字符串printf("[YGH@MyMachina CurrentPath]# ");//命令行的输入是在当前行输入,不能用\nfflush(stdout);//不用\n不会行刷新出来,所以要用fflush刷新缓冲区char *s = fgets(commandstr, sizeof(commandstr), stdin);//输入的时候最后缓冲区会输入一个\nassert(s);//断言在debug方式的时候存在,release方式会被裁掉(void)s;//这句的用处是让断言在release方式下也不会失效(因为assert被去掉了,s没有被使用会报警,这里用一下就不会报警了)commandstr[strlen(commandstr) - 1] = '\0';//去掉我们缓冲区中输入的\nint n = split(commandstr, argv);//字符串切割if(n != 0)//判断字符串是否切割成功{continue;}pid_t id = fork();assert(id >= 0);(void)id;if(id == 0){//子进程exit(0);}//父进程int status = 0;//退出码waitpid(id, &status, 0);//阻塞等待子进程}
}

完成切割后进行进程的替换:要使用execvp,因为输入的命令没有地址,要在环境变量中直接寻找,需要“p”,而且输入的命令行字符串时切割开放在一个数组中的,所以需要“v”。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAX 1024
#define ARGC 64
#define SEP " "//分隔符空格//字符串切割函数
int split(char* commandstr, char* argv[])
{assert(commandstr);assert(argv);argv[0] = strtok(commandstr, SEP);//函数切割后返回第一个地址if(argv[0] == NULL){return 1;}int i = 1;while(argv[i++] = strtok(NULL, SEP));//这一句等价于下面的一段代码//while(1)//{//        argv[i] = strtok(NULL, SEP);//NULL表示还是继续切割commandstr//        if(argv[i] == NULL)//        {//            break;//        }//        i++;//}return 0;
}//用来输出切割后的字符串的函数
void debugPrintf(char* argv[])
{for(int i = 0; argv[i]; i++){printf("%d: %s\n", i, argv[i]);}
}int main()
{while(1){char commandstr[MAX] = {0};//放在循环内部每次输入后重新创建缓冲区判断char* argv[ARGC] = {NULL};//存放切割后的字符串printf("[YGH@MyMachina CurrentPath]# ");//命令行的输入是在当前行输入,不能用\nfflush(stdout);//不用\n不会行刷新出来,所以要用fflush刷新缓冲区char *s = fgets(commandstr, sizeof(commandstr), stdin);//输入的时候最后缓冲区会输入一个\nassert(s);//断言在debug方式的时候存在,release方式会被裁掉(void)s;//这句的用处是让断言在release方式下也不会失效(因为assert被去掉了,s没有被使用会报警,这里用一下就不会报警了)commandstr[strlen(commandstr) - 1] = '\0';//去掉我们缓冲区中输入的\nint n = split(commandstr, argv);//字符串切割if(n != 0)//判断字符串是否切割成功{continue;}//debugPrintf(argv);//测试输出切割后的字符串pid_t id = fork();assert(id >= 0);(void)id;if(id == 0){//子进程execvp(argv[0], argv);exit(1);//替换失败直接退出}//父进程int status = 0;//退出码waitpid(id, &status, 0);//阻塞等待子进程}
}

8.完整的代码与测试效果

以上就实现了一个最基础的极简版的shell命令行,以下是删除不必要的完整代码:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAX 1024
#define ARGC 64
#define SEP " "//分隔符空格//字符串切割函数
int split(char* commandstr, char* argv[])
{assert(commandstr);assert(argv);argv[0] = strtok(commandstr, SEP);//函数切割后返回第一个地址if(argv[0] == NULL){return 1;}int i = 1;while(argv[i++] = strtok(NULL, SEP));//这一句等价于下面的一段代码return 0;
}int main()
{while(1){char commandstr[MAX] = {0};//放在循环内部每次输入后重新创建缓冲区判断char* argv[ARGC] = {NULL};//存放切割后的字符串printf("[YGH@MyMachina CurrentPath]# ");//命令行的输入是在当前行输入,不能用\nfflush(stdout);//不用\n不会行刷新出来,所以要用fflush刷新缓冲区char *s = fgets(commandstr, sizeof(commandstr), stdin);//输入的时候最后缓冲区会输入一个\nassert(s);//断言在debug方式的时候存在,release方式会被裁掉(void)s;//这句的用处是让断言在release方式下也不会失效(因为assert被去掉了,s没有被使用会报警,这里用一下就不会报警了)commandstr[strlen(commandstr) - 1] = '\0';//去掉我们缓冲区中输入的\nint n = split(commandstr, argv);//字符串切割if(n != 0)//判断字符串是否切割成功{continue;}pid_t id = fork();assert(id >= 0);(void)id;if(id == 0){//子进程execvp(argv[0], argv);exit(1);//替换失败直接退出}//父进程int status = 0;//退出码waitpid(id, &status, 0);//阻塞等待子进程}
}

测试效果:
在这里插入图片描述

9.补充——内建命令

对于我们前面自己完成的shell,因为是在子进程中执行的,所以只能完成ls这样的命令,cd命令就无法移动到指定目录,因为需要由父进程bash来运行才能做到移动,像cd这样的命令我们称之为内建命令,要特殊处理。

以cd为例:要调用chdir(修改工作路径)来直接让bash修改路径即可:

头文件:
#include <unistd.h>
函数:
int chdir(const char *path);
参数:
要改变的路径(绝对/相对)
返回值:
成功返回0,失败返回-1;

所以加上这样一段代码就可以让bash来执行cd命令了:

//cd是内建命令,让bash来执行
if(strcmp(argv[0], "cd") == 0)
{if(argv[1] != NULL){chdir(argv[1]);}continue;//直接通过chdir让bash修改,就不用往后传给子进程了
}

还有像export导入环境变量也是内建命令,也可以用同样的方式来实现:
直接使用putenv,由bash来执行,将环境变量导入。
注意:
用户自己定义的环境变量,在bash中要用户自己来维护!不能用一个经常被覆盖的缓冲区来保存环境变量。
(因为环境变量导入后,是直接指向缓冲区中保存的字符串的,如果经常被刷新的话就找不到了)

//因为维护了缓冲区,所以贴完整的代码
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAX 1024
#define ARGC 64
#define SEP " "//分隔符空格//字符串切割函数
int split(char* commandstr, char* argv[])
{assert(commandstr);assert(argv);argv[0] = strtok(commandstr, SEP);//函数切割后返回第一个地址if(argv[0] == NULL){return 1;}int i = 1;while(argv[i++] = strtok(NULL, SEP));//这一句等价于下面的一段代码return 0;
}//用来输出切割后的字符串的函数
void debugPrintf(char* argv[])
{for(int i = 0; argv[i]; i++){printf("%d: %s\n", i, argv[i]);}
}int main()
{char myenv[32][256] = {0};//自定义维护的环境变量缓冲区int env_index = 0;while(1){char commandstr[MAX] = {0};//放在循环内部每次输入后重新创建缓冲区判断char* argv[ARGC] = {NULL};//存放切割后的字符串printf("[YGH@MyMachina CurrentPath]# ");//命令行的输入是在当前行输入,不能用\nfflush(stdout);//不用\n不会行刷新出来,所以要用fflush刷新缓冲区char *s = fgets(commandstr, sizeof(commandstr), stdin);//输入的时候最后缓冲区会输入一个\nassert(s);//断言在debug方式的时候存在,release方式会被裁掉(void)s;//这句的用处是让断言在release方式下也不会失效(因为assert被去掉了,s没有被使用会报警,这里用一下就不会报警了)commandstr[strlen(commandstr) - 1] = '\0';//去掉我们缓冲区中输入的\nint n = split(commandstr, argv);//字符串切割if(n != 0)//判断字符串是否切割成功{continue;}//cd是内建命令,让bash来执行if(strcmp(argv[0], "cd") == 0){if(argv[1] != NULL){chdir(argv[1]);}continue;}else if(strcmp(argv[0], "export") == 0)//export是内建命令,让bash执行{if(argv[1] != NULL){strcpy(myenv[env_index], argv[1]);//拷贝到自定义的环境变量缓冲区putenv(myenv[env_index++]);//导入,因为commandstr会被覆盖}continue;}//配置ls命令的配色方案if(strcmp(argv[0], "ls") == 0){int pos = 0;while(argv[pos]){++pos;}argv[pos++] = (char*)"--color=auto";argv[pos] = NULL;}//debugPrintf(argv);//输出切割后的字符串pid_t id = fork();assert(id >= 0);(void)id;if(id == 0){//子进程execvp(argv[0], argv);exit(1);//替换失败直接退出}//父进程int status = 0;//退出码waitpid(id, &status, 0);//阻塞等待子进程}
}

而此时获取的env还是子进程的环境变量,我们要查的是bash的环境变量,所也要对env命令做特殊处理,echo命令同理也需要处理。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAX 1024
#define ARGC 64
#define SEP " "//分隔符空格//字符串切割函数
int split(char* commandstr, char* argv[])
{assert(commandstr);assert(argv);argv[0] = strtok(commandstr, SEP);//函数切割后返回第一个地址if(argv[0] == NULL){return 1;}int i = 1;while(argv[i++] = strtok(NULL, SEP));//这一句等价于下面的一段代码return 0;
}//用来输出切割后的字符串的函数
void debugPrintf(char* argv[])
{for(int i = 0; argv[i]; i++){printf("%d: %s\n", i, argv[i]);}
}void showEnv()
{extern char** environ;for(int i = 0; environ[i]; i++){printf("%d:%s\n", i, environ[i]);}
} int main()
{char myenv[32][256] = {0};//自定义维护的环境变量缓冲区int env_index = 0;while(1){int last_exit = 0;//保存最后获取的退出码char commandstr[MAX] = {0};//放在循环内部每次输入后重新创建缓冲区判断char* argv[ARGC] = {NULL};//存放切割后的字符串printf("[YGH@MyMachina CurrentPath]# ");//命令行的输入是在当前行输入,不能用\nfflush(stdout);//不用\n不会行刷新出来,所以要用fflush刷新缓冲区char *s = fgets(commandstr, sizeof(commandstr), stdin);//输入的时候最后缓冲区会输入一个\nassert(s);//断言在debug方式的时候存在,release方式会被裁掉(void)s;//这句的用处是让断言在release方式下也不会失效(因为assert被去掉了,s没有被使用会报警,这里用一下就不会报警了)commandstr[strlen(commandstr) - 1] = '\0';//去掉我们缓冲区中输入的\nint n = split(commandstr, argv);//字符串切割if(n != 0)//判断字符串是否切割成功{continue;}//cd是内建命令,让bash来执行if(strcmp(argv[0], "cd") == 0){if(argv[1] != NULL){chdir(argv[1]);}continue;}else if(strcmp(argv[0], "export") == 0)//export是内建命令,让bash执行{if(argv[1] != NULL){strcpy(myenv[env_index], argv[1]);//拷贝到自定义的环境变量缓冲区putenv(myenv[env_index++]);//导入,因为commandstr会被覆盖}continue;}else if(strcmp(argv[0], "env") == 0)//env如果交给子进程打印的是子进程的,特殊处理打印bash的{showEnv();continue;}else if(strcmp(argv[0], "echo") == 0){const char* target_env = NULL;if(*argv[1] == '$'){if(*argv[1]+1 == '?'){printf("%d\n", last_exit);continue;}else{target_env = getenv(argv[1]+1);}if(target_env != NULL){printf("%s=%s\n", argv[1] + 1, target_env);}}}//配置ls命令的配色方案if(strcmp(argv[0], "ls") == 0){int pos = 0;while(argv[pos]){++pos;}argv[pos++] = (char*)"--color=auto";argv[pos] = NULL;}//debugPrintf(argv);//输出切割后的字符串pid_t id = fork();assert(id >= 0);(void)id;if(id == 0){//子进程execvp(argv[0], argv);exit(1);//替换失败直接退出}//父进程int status = 0;//退出码pid_t ret = waitpid(id, &status, 0);//阻塞等待子进程if(ret > 0)//获取退出码{last_exit = WEXITSTATUS(status);}}
}

相关文章:

【Liunx】进程的程序替换——自定义编写极简版shell

目录 进程程序替换[1~5]1.程序替换的接口&#xff08;加载器&#xff09;2.什么是程序替换&#xff1f;3.进程替换的原理4.引入多进程5.系列程序替换接口的详细解析&#xff08;重点&#xff01;&#xff09; 自定义编写一个极简版shell[6~8]6.完成命令行提示符7.获取输入的命令…...

c++标准模板(STL)(std::array)(三)

定义于头文件 <array> template< class T, std::size_t N > struct array;(C11 起 std::array 是封装固定大小数组的容器。 此容器是一个聚合类型&#xff0c;其语义等同于保有一个 C 风格数组 T[N] 作为其唯一非静态数据成员的结构体。不同于 C 风格数组…...

c#笔记-创建一个项目

创建一个项目 创建控制台程序 在你安装完成Visual Studio后打开它&#xff0c;你会的到一个启动窗口 点击创建新项目&#xff0c;选择右上角c#的没有Framework的控制台应用。 项目名称&#xff0c;位置自己随意。 目标框架选择NET7.0。 项目创建完成后应该你的界面应该类似…...

Photoshop如何使用图像调色之实例演示?

文章目录 0.引言1.将一张偏冷调的图像调整成暖调2.将图像调整成不同季节色彩倾向3.变换花朵的颜色4.创建人像轮廓风景5.修饰蓝天白云6.调换花草颜色 0.引言 因科研等多场景需要进行绘图处理&#xff0c;笔者对PS进行了学习&#xff0c;本文通过《Photoshop2021入门教程》及其配…...

IDEA中使用Git提交代码提示:您即将把CRLF行分隔符提交到Gt仓库。 建议将core.autocrlf Git特性设置为trUe,以免发生行分隔符问题。

IDEA中使用Git提交代码提示&#xff1a;您即将把CRLF行分隔符提交到Gt仓库。 建议将core.autocrlf Git特性设置为trUe,以免发生行分隔符问题。 问题背景&#xff1a; 在IDEA中&#xff0c;使用Git提交代码到远程仓库时&#xff0c;结果弹出一个警告窗口 问题原因&#xff1a; …...

ArduPilot之开源代码LibrarySketches设计

ArduPilot之开源代码Library&Sketches设计 1. 简介1.1 Core libraries1.2 Sensor libraries1.3 Other libraries 2. 源由3. Library Sketches设计3.1 设计框架3.2 Example Sketches3.3 AP_Common Sketches3.3.1 配置sitl环境3.3.2 编译AP_Common3.3.3 运行AP_Common3.3.4 代…...

第一章:概述

1&#xff0c;因特网概述 1.网络、互联网和英特网 网络(Network)由若干结点(Node)和连接这些结点的链路(Link)组成。 多个网络还可以通过路由器互连起来&#xff0c;这样就构成了一个覆盖范围更大的网络&#xff0c;即互联网(或互连网)。因此&#xff0c;互联网是“网络的网络…...

MySQL --- DDL图形化工具表结构操作

一. 图形化工具 1. 介绍 前面我们讲解了DDL中关于数据库操作的SQL语句&#xff0c;在我们编写这些SQL时&#xff0c;都是在命令行当中完成的。大家在练习的时候应该也感受到了&#xff0c;在命令行当中来敲这些SQL语句很不方便&#xff0c;主要的原因有以下 3 点&#xff1a;…...

归一化处理(2023寒假每日一题 14)

在机器学习中&#xff0c;对数据进行归一化处理是一种常用的技术。 将数据从各种各样分布调整为平均值为 0 0 0、方差为 1 1 1 的标准分布&#xff0c;在很多情况下都可以有效地加速模型的训练。 这里假定需要处理的数据为 n n n 个整数 a 1 , a 2 , ⋯ , a n a_1,a_2,⋯…...

无公网IP,外网远程连接MySQL数据库

哈喽~大家好&#xff0c;这篇来看看无公网IP&#xff0c;外网远程连接MySQL数据库。 文章目录 前言1. 检查mysql安装状态2. 安装配置cpolar内网穿透3. 创建tcp隧道&#xff0c;映射3306端口4. 公网远程连接4.1 图形化界面4.2 使用命令行远程连接 5. 配置固定tcp端口地址5.1 保留…...

OJ刷题 第十四篇(递归较多)

23204 - 进制转换 时间限制 : 1 秒 内存限制 : 128 MB 将一个10进制数x(1 < x < 100,000,000)转换成m进制数(2< m < 16) 。分别用 ABCDEF表示10以上的数字。 输入 x m (1 < x < 100,000,000, 2< m < 16) 输出 m进制数 样例 输入 31 16 输出 1F 答…...

FileZilla读取目录列表失败(vsftpd被动模式passive mode部署不正确)

文章目录 现象问题原因解决方法临时解决&#xff08;将默认连接方式改成主动模式&#xff09;从根本解决&#xff08;正确部署vsftpd的被动模式&#xff09; 现象 用FileZilla快速连接vsftpd服务器时&#xff0c;提示读取目录列表失败 问题原因 是我vsftpd服务端的被动模式没…...

【Java面试八股文】数据库篇

导航&#xff1a; 【黑马Java笔记踩坑汇总】JavaSEJavaWebSSMSpringBoot瑞吉外卖SpringCloud黑马旅游谷粒商城学成在线MySQL高级篇设计模式牛客面试题 目录 请你说说MySQL索引,以及它们的好处和坏处 请你说说MySQL的索引是什么结构,为什么不用哈希表 请你说说数据库索引的底…...

Android Glide加载图片、网络监听、设置资源监听

再搞事情之前首先创建一个项目&#xff0c;就命名为GlideDemo吧。    一、项目配置 创建好之后&#xff0c;在app模块下build.gradle的dependencies闭包中添加如下依赖&#xff1a; //glide//glideimplementation com.github.bumptech.glide:glide:4.11.0annotationProcess…...

等保定级报告模版

等保定级怎么做_luozhonghua2000的博客-CSDN博客 上篇给大家说清楚了,等保定级怎么做,但在日常工作中,需要向上级或甲方输出定级报告,这篇我降弄个模版供大家参考。 信息系统安全等级保护定级报告 XX 平台系统描述 (一) 2023年5月,XX 正式上线,XX 隶属于深圳 XX 科技…...

计算机组成原理4.2.2汉明码

编码的最小距离 奇校验和偶校验 看1的个数是奇数 还是偶数 汉明码 汉明码的配置 根据不等式&#xff0c;确定增添几位&#xff0c;根据指数放置增添位 汉明码的检错 分不同检测小组 分组规则&#xff1a;哪位为’1‘就是哪组元素。 1号位为‘1’的都是第一组元素&#…...

JavaScript全解析——本地存储的概念、用法详解

本地存储概念&#xff1a; 就是浏览器给我们提供的可以让我们在浏览器上保存一些数据 常用的本地存储 localStorage sessionStorage localStorage 特点: 1.长期存储,除非手动删除否则会一直保存在浏览器中&#xff0c;清除缓存或者卸载浏览器也就没有了 2.可以跨页面通讯,…...

对象浅拷贝的5种方式

参考原文:浅拷贝的五种实现方式 - 掘金 (juejin.cn) 哈喽 大家好啊 最近发现自己对对象都不是很熟练&#xff0c;特别是涉及到一些复制&#xff0c;深浅拷贝的东西 1.Object.assign 首先 我们创建一个空对象obj1 然后创建一个对象obj2 用object.assign(目标对象&#xff0c…...

Java每日一练(20230504)

目录 1. 位1的个数 &#x1f31f; 2. 移除元素 &#x1f31f; 3. 验证二叉搜索树 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 1. 位1的个数 编写一个…...

【深度学习】计算机视觉(13)——模型评价及结果记录

1 Tensorboard怎么解读&#xff1f; 因为意识到tensorboard的使用远不止画个图放个图片那么简单&#xff0c;所以这里总结一些关键知识的笔记。由于时间问题&#xff0c;我先学习目前使用最多的功能&#xff0c;大部分源码都包含summary的具体使用&#xff0c;基本不需要自己修…...

Java - Mysql数据类型对应

Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...

在Ubuntu中设置开机自动运行(sudo)指令的指南

在Ubuntu系统中&#xff0c;有时需要在系统启动时自动执行某些命令&#xff0c;特别是需要 sudo权限的指令。为了实现这一功能&#xff0c;可以使用多种方法&#xff0c;包括编写Systemd服务、配置 rc.local文件或使用 cron任务计划。本文将详细介绍这些方法&#xff0c;并提供…...

土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等

&#x1f50d; 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术&#xff0c;可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势&#xff0c;还能有效评价重大生态工程…...

【Java_EE】Spring MVC

目录 Spring Web MVC ​编辑注解 RestController RequestMapping RequestParam RequestParam RequestBody PathVariable RequestPart 参数传递 注意事项 ​编辑参数重命名 RequestParam ​编辑​编辑传递集合 RequestParam 传递JSON数据 ​编辑RequestBody ​…...

AspectJ 在 Android 中的完整使用指南

一、环境配置&#xff08;Gradle 7.0 适配&#xff09; 1. 项目级 build.gradle // 注意&#xff1a;沪江插件已停更&#xff0c;推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

python执行测试用例,allure报乱码且未成功生成报告

allure执行测试用例时显示乱码&#xff1a;‘allure’ &#xfffd;&#xfffd;&#xfffd;&#xfffd;&#xfffd;ڲ&#xfffd;&#xfffd;&#xfffd;&#xfffd;ⲿ&#xfffd;&#xfffd;&#xfffd;Ҳ&#xfffd;&#xfffd;&#xfffd;ǿ&#xfffd;&am…...

Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?

在大数据处理领域&#xff0c;Hive 作为 Hadoop 生态中重要的数据仓库工具&#xff0c;其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式&#xff0c;很多开发者常常陷入选择困境。本文将从底…...

python报错No module named ‘tensorflow.keras‘

是由于不同版本的tensorflow下的keras所在的路径不同&#xff0c;结合所安装的tensorflow的目录结构修改from语句即可。 原语句&#xff1a; from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后&#xff1a; from tensorflow.python.keras.lay…...

让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比

在机器学习的回归分析中&#xff0c;损失函数的选择对模型性能具有决定性影响。均方误差&#xff08;MSE&#xff09;作为经典的损失函数&#xff0c;在处理干净数据时表现优异&#xff0c;但在面对包含异常值的噪声数据时&#xff0c;其对大误差的二次惩罚机制往往导致模型参数…...