文章目录
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),但是数据(代码数据)不会先进行拷贝。父子代码与数据在开始时共享就很好理解了,因为子进程把父进程包括页表的映射关系给你全拷贝了,那么父子进程的代码和数据映射到同一片物理内存也就很好理解了。
(父或子任何一方修改了一段数据)修改内容之后
:
——》结果:发生写实拷贝,子进程申请物理内存,数据会被先拷贝一份进去,在子进程的页表内重新构建映射关系,再进行写入。
🤔疑问1
:OS怎么知道要触发写实拷贝的?
——》💡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之前,还做了其他⼯作:
-
👉执⾏用户通过atexit或on_exit定义的清理函数。
-
👉关闭所有打开的流,所有的缓存数据均被写入(刷新语言级缓冲区)
-
👉调⽤_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也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息,子进程就不会进入僵尸状态。
进程等待作用
:
- 回收子进程的僵尸状态
- 一直等待一个子进程,一直阻塞,直到该子进程退出,然后回收子进程
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会被设置成相应的值以指示错误所在;
——》参数:
- pid:Pid= -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 重谈进程退出
我们进程退出通常由三种情况:
- 代码跑完了,结果是对的,此时是return 0 ;
- 代码跑完了,结果是不对的,此时是return !0;
——》前两种结果的正确与否,通过退出码判定。 - 第三种就是,进程异常了,比如出现栈溢出,除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");
}
}
}
-
execl
-
execv
-
execlp
-
execvpe
4.2.3 子进程环境变量
- 子进程环境变量的获取,一般是由父进程继承下来。程序替换后,环境变量表默认不变,命令行参数由传入的参数提供。
- 如果子进程需要传入全新的命令行参数表,则需要在带有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");
}
}
}
- 如果需要在原有的环境变量表中添加新的环境变量呢?
#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;
}
运行结果实例:
本文就到这里,感谢你看到这里❤️❤️! 我知道一些人看文章喜欢静静看,不评论🤔,但是他会点赞😍,这样的人,帅气低调有内涵😎,美丽大方很优雅😊,明人不说暗话,要你手上的一个点赞😘!如果你不点赞的话,瓦达西😭😭!
十分十分感谢你能看完我这么长的“流水账”!希望你能从我的文章学到一点点的东西❤️❤️