当前位置: 首页 > 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;基本不需要自己修…...

项目经理在项目中是什么角色?

有人说&#xff0c;项目经理就是一个求人的差事&#xff0c;你是在求人帮你做事。 有人说&#xff0c;项目经理就是一个与人扯皮的差事&#xff0c;你要不断的与开发、产品、测试等之间沟通、协调。 确实&#xff0c;在做项目的时候&#xff0c;有的人是为了完成功能&#x…...

【技术分享】防止根据IP查域名,防止源站IP泄露

有的人设置了禁止 IP 访问网站&#xff0c;但是别人用 https://ip 的形式&#xff0c;会跳到你服务器所绑定的一个域名网站上 直接通过 https://IP, 访问网站&#xff0c;会出现“您的连接不是私密连接”&#xff0c;然后点高级&#xff0c;会出现“继续前往 IP”&#xff0c;…...

Baumer工业相机堡盟相机如何使用偏振功能(偏振相机优点和行业应用)(C#)

项目场景&#xff1a; Baumer工业相机堡盟相机是一种高性能、高质量的工业相机&#xff0c;可用于各种应用场景&#xff0c;如物体检测、计数和识别、运动分析和图像处理。 Baumer的万兆网相机拥有出色的图像处理性能&#xff0c;可以实时传输高分辨率图像。此外&#xff0…...

无损以太网与网络拥塞管理(PFC、ECN)

无损以太网 无损以太网&#xff08;Lossless Ethernet&#xff09;是一种专门用于数据中心网络的网络技术&#xff0c;旨在提供低延迟、高吞吐量和可靠性的传输服务。它是在传统以太网的基础上进行了扩展&#xff0c;引入了新的拥塞管理机制&#xff0c;以避免数据包丢失和网络…...

爬虫大全:从零开始学习爬虫的基础知识

爬虫是一种自动获取网站信息的技术&#xff0c;它可以帮助我们快速地抓取海量网站数据&#xff0c;进行统计分析、挖掘和展示。本文旨在为初学者详细介绍爬虫的基础知识&#xff0c;包括&#xff1a;爬虫原理、爬虫分类、网页结构分析、爬虫工具和技能、爬虫实践示范&#xff0…...

【Python】【进阶篇】21、Django Admin数据表可视化

目录 21、Django Admin数据表可视化1. 创建超级用户2. 将Model注册到管理后台1)在admin.py文件中声明 3. django_admin_log数据表 21、Django Admin数据表可视化 在《Django Admin后台管理系统》介绍过 Django 的后台管理系统是为了方便站点管理人员对数据表进行操作。Django …...

【MySQL约束】数据管理实用指南

1、数据库约束的认识 数据库约束的概念&#xff1a;数据库的约束是关系型数据库的一个重要的功能&#xff0c;它提供了一种“校验数据”合法性的机制&#xff0c;能够保证数据的“完整性”、“准确性”和“正确性” 数据库的约束&#xff1a; not null&#xff1a;不能存储 nul…...

2023年第二十届五一数学建模竞赛C题:“双碳”目标下低碳建筑研究-思路详解与代码答案

该题对于模型的考察难度较低&#xff0c;难度在于数据的搜集以及选取与处理。 这里推荐数据查询的网站&#xff1a;中国碳核算数据库&#xff08;CEADs&#xff09; https://www.ceads.net.cn/ 国家数据 国家数据​data.stats.gov.cn/easyquery.htm?cnC01 以及各省市《统…...

Vue父组件生命周期和子组件生命周期触发顺序

加载渲染过程 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted子组件更新过程 父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated父组件更新…...

DevOps工程师 - 面试手册

DevOps工程师 - 面试手册 岗位概述 DevOps工程师是一种专注于提高软件开发和运维团队协作、提高软件产品交付速度和质量的职位。这种角色要求具备跨领域的知识&#xff0c;以便在开发和运维过程中建立起稳定、可靠的基础设施和自动化流程。 常见的职位招聘描述 负责设计、实…...

建设官方网站的请示/湖南网站排名

1. DOM复习 DOM&#xff1a;document object model 文档对象模型或文档树模型。 1.1 节点分类 DOM中一共有5个节点类型&#xff0c;而现在我们常用的有三种&#xff1a;标签&#xff08;元素&#xff09;节点&#xff0c;属性节点&#xff0c;文本节点。 我们可以通过nodeType…...

中国建材建设网站/谷歌浏览器 安卓下载2023版

FTP服务器是在互联网上提供文件存储和访问服务的计算机&#xff0c;它们依照FTP协议提供服务。 IIS7服务器管理工具可以批量管理、定时上传下载、同步操作、数据备份、到期提醒、自动更新。IIS7服务器管理工具适用于Windows操作系统和liunx操作系统&#xff1b;支持Ftp客户端批…...

淘宝做seo要建网站吗/大连中小企业网络营销

1.1 &#xff08;无效&#xff09;// 验证本地缓存的链接是否可以访问 // pathImg为链接 target为data中变量名的字符串getValidateImage (pathImg, target) {console.log(pathImg)var imgObj new Image()imgObj.src pathImgif ((imgObj.width > 0 && imgObj.hei…...

住房和城乡建设部网站事故快报/百度搜索引擎属于什么引擎

http://videolectures.net/Top/Computer_Science/Machine_Learning/##tvtt转载于:https://www.cnblogs.com/stoneresearch/archive/2010/08/22/4336500.html...

网站怎么做微信扫描登录网站/whois查询 站长工具

一、修改Flex builder 1.用无格式编辑器打开FlashBuilder.ini 2.把zh_CN替换成"en_US" 二、修改MyEclipse插件 1.用无格式编辑器打开MyEclipse8.5\configuration\config.ini 2.最后添加osgi.nlen_US 三、修改Eclipse插件 1.用无格式编辑器打开eclipse.ini 2.顶部加入…...

专门的设计师服务平台/网站seo方案撰写

概述 Spring MVC框架的文件上传是基于commons-fileupload组件的文件上传。 因此所需要的包是commons-fileupload.jar和commons-io.jar包。 基于表单的文件上传 <form action"upload" method"post" enctype"multipart/form-data"><i…...