目录
前言:学完进程概念,进程控制就相对比较轻松了。
一.进程创建
1.fork函数
再次来看fork函数。
fork之前父进程独立执行,fork之后,父进程和子进程两个执行流分别执行。
进程具有独立性,代码和数据必须是独立的,因此有了写时拷贝。
fork之后有两个进程,并且fork之后父子代码共享。一般情况下,父子进程共享所有的代码。
虽然子进程共享了父进程所有的代码,但是因为eip的存在,子进程只能从fork处开始执行。
CPU中有一个eip:程序计数器
eip(pc指针):保存当前正在执行指令的下一条指令。
eip程序计数器会拷贝给子进程,子进程便从eip所指向的代码处开始指向。
fork之后,OS做了什么呢?
进程 = 内核的进程数据结构 + 进程的代码和数据
OS会创建子进程的内核数据结构(struct take_struct + struct mm_struct + 页表) + 代码继承父进程,数据以写时拷贝的方式,来进行共享或者独立。
2.写时拷贝
在进行写时拷贝之前,父子进程因为代码和数据共享,所以空间也是共享的。
如果父子进程有进程要修改,这里展示的是子进程修改,那么要修改的这个变量,父子进程原来指向的空间相同,但是确定要修改这个变量时,子进程就会重新开一个空间,并把修改后的值存入该空间中,完成写时拷贝。
为什么要有写时拷贝呢?在创建子进程的时候,就把数据分开不行吗?
① 父进程的数据,子进程不一定全用,即使使用,也不一定全部会写入。
如果直接分开,就可能会浪费空间。
② 最理想的情况是,只有会被父子进程修改的数据,进行分离拷贝,不需要修改的共享。
这个从技术角度上很难实现。
③ 如果fork的时候,就一直拷贝数据给子进程,那么就会增加fork的成本(内存和时间)。
根据以上三点所述,最终采用写时拷贝(提高了内存的使用率)。
写时拷贝只会拷贝父子进程修改的,这就是拷贝数据的最小成本,但是拷贝的成本是依旧存在的。
写时拷贝是一种延时拷贝策略,只有真正要使用空间的时候,才会进行拷贝。(如果你想要空间,但是不立刻使用,就先不给你空间,那么这个空间就可以先给别个需要使用空间的进程)
3.fork返回值
① 子进程返回0
② 父进程返回子进程的pid
4.fork用法
① 一个父进程希望复制自己,使父子进程同时执行不同的代码段。
② 一个进程要执行一个不同的程序。
5.fork调用失败的原因
① 系统中的进程过多。
② 实际用户的进程数超过限制。
二.进程终止
1.进程退出码
进程退出有三种情况:
① 代码跑完,结果正确。
② 代码跑完,结果不正确。
③ 代码没跑完,程序异常。
我们日常写代码的时候,在main函数中,都是return 0,那么为什么是0呢?
这个return的数字就是与进程退出的正确与否有关:0:success 非0:fail
如果正确时,就是直接返回0,我们也不需要知道为什么正确,而如果错误,那么我们就会想要知道错误的原因,因此,根据错误的不同,会返回不同的非0的数字。
这个数字就是进程退出码。
这里我们可以大致看一下0-100进程退出码对应数字所对应的信息,当然并不需要记住,只是看一看。
#include <stdio.h>
int main()
{
return 10;
}
这里我们return 10。
echo $? :这个指令就是用来看上一个进程的退出码的
这里我们可以看到进程退出码是10。与return的数字相同。
这里因为ls -al代码成功执行,所以进程退出码是0。
2.进程退出方法
进程退出有三种方法:
① return:只能在main函数中用
② exit:终止进程,刷新缓冲区
③ _exit:直接终止进程,不会有任何刷新操作
exit和_exit的差别不大,但是一般exit用的是最多的。
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello proc");
sleep(1);
exit(111);
}
这里我们首先可以看到,exit终止进程时,可以得到该退出码,与return一样。
再就是注意这里打印出了hello proc,因为exit刷新了缓冲区之后才终止进程。
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("hello proc");
sleep(1);
_exit(111);
}
使用_exit后就不会打印出来了,因为_exit不会刷新缓冲区。
3.终止时,内核的做法
进程 = 内核结构 + 进程代码和数据
内核结构是take_struct和mm_struct,进程终止时,OS可能并不会释放该进程的内核数据结构(是否释放取决于当时空闲的空间大小)。
创建进程时要进行开辟空间和初始化,而终止进程一般并不会去释放这个内核的空间,而是让这个空间内的代码和数据无效,等到下次创建进程时,直接进行初始化,而不再需要开辟空间,进而提高了CPU的效率(进程的创建和终止是非常频繁的)。
因为内核具有数据结构缓冲池:slab分派器。
三.进程等待
1.为什么进行进程等待
① 子进程退出时,父进程如果不管它,就可能导致子进程进入僵尸进程,进而造成内存泄漏。并且,进程一旦变成僵尸进程,即使是kill也无法杀死一个已经死去的进程。
② 我们需要知道父进程派给子进程的任务完成的如何,因此子进程运行完成,结果的对错,或者是否正常退出,都应该告诉父进程。
总结:父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
2.如何进行进程等待
(1)wait函数
pid_t wait(int* status)
这里我们先不去了解status,先使用NULL代替。
我们先来验证上面的解决僵尸进程。wait可以解决子进程Z状态,让子进程进入X状态退出。
#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
while(1)
{
printf("我是子进程,我正在运行...pid: %d\n", getpid());
sleep(1);
}
}
else
{
printf("我是父进程: pid: %d, 我准备等待子进程\n", getpid());
sleep(20);
pid_t ret = wait(NULL);
if(ret < 0)
{
printf("等待失败!\n");
}
else
{
printf("等待成功: result: %d\n", ret);
}
sleep(20);
}
return 0;
}
这里我们让子进程变成死循环,然后我们在运行的过程中kill掉子进程,看进程状态的变化。这里父进程最开始是等待20秒的。
kill子进程之前进程状态:
kill子进程之后进程状态:
父进程等待20s结束之后:
通过以上3张图,我们可以很明显的看出,在子进程被kill之后,进入了Z(僵尸)状态,然后等到父进程结束sleep之后,因为调用了wait()函数,子进程消失了。因为子进程不再是Z状态了,而是变成了X直接退出了(这里X的时间非常短,所以没有看到)。
而这张图,就显示了wait的返回值是子进程的pid。
(2)waitpid函数
上面的wait函数并不是重点,这个waitpid函数才是,waitpid不仅包括了wait的功能,还有额外的用处。
pid_t waitpid(pid_t pid, int* status, int options)
使用之前要写上头文件:
#include <sys/types.h>
#include <sys/wait.h>
这个函数有3个参数。
① 参数pid
首先第一个参数pid,如果传的pid > 0,该传的值为多少,就代表等待哪一个子进程(指定等待),如果传 -1,就等待任意进程,,类似上面的wait。
② 参数status
第二个参数status:这个参数是一个输出型参数,通过调用该函数,可以从函数内部拿出来特定的函数。具体作用是从子进程的take_struct中拿出子进程退出的退出码,或是拿到特定的信号。
status的内部构造如上图所示,我们只需要注意该整数的低16个比特位。
这是一个位图,最低7位是进程异常退出,接收到的特定的信号;次低8位用来接收子进程的退出码。
#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)
{
int cnt = 5;
// child
while(1)
{
printf("我是子进程,我正在运行...pid: %d\n", getpid());
sleep(1);
cnt--;
if(!cnt)
{
break;
}
}
exit(13);
}
else
{
int status = 0;
printf("我是父进程: pid: %d, 我准备等待子进程\n", getpid());
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("wait success, ret: % d, 我所等待的子进程的退出码: %d, 退出信号是: %d\n", ret, (status >> 8) & 0xFF, status & 0X7F);
}
return 0;
}
我们要想得到这个status的最低7位和次低8位就要通过位操作,(status >> 8) & 0xFF就可以得到次低8位,status & 0x7F就可以得到最低7位。
运行该进程,因为该进程正确运行了,所以退出信号为0,而退出码就是我们所写在子进程中的exit()中的值。
如果我们让子进程出错:
#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
while(1)
{
printf("我是子进程,我正在运行...pid: %d\n", getpid());
sleep(1);
int a = 10 / 0;
}
exit(13);
}
else
{
int status = 0;
printf("我是父进程: pid: %d, 我准备等待子进程\n", getpid());
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("wait success, ret: % d, 我所等待的子进程的退出码: %d, 退出信号是: %d\n", ret, (status >> 8) & 0xFF, status & 0X7F);
}
return 0;
}
这里我们写入一个int a = 10 / 0,因为这个,子进程一定会出错。
这里运行后,就可以看到退出信号是:8
如果退出信号不为0,就说明代码没跑完,那么无论退出码是几都没有用了。
实际上,信号是没有0的:
在实际使用中,我们并不需要通过位操作来获得退出码和退出信号,可以通过系统中的宏来得到。
① WIFEXITED(status):若为正常终止子进程返回的状态,则为真(查看进程是否正常退出)【即退出码】,这里WIFEXITED为非零时,才是正常退出的。
② WEXITSTATUS(status):若WIFEXITED非零,则提取子进程退出码。(查看进程的退出码)
wifexited
使用案例:
#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
while(1)
{
printf("我是子进程,我正在运行...pid: %d\n", getpid());
sleep(1);
}
exit(10);
}
else
{
int status = 0;
printf("我是父进程: pid: %d, 我准备等待子进程\n", getpid());
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
if(WIFEXITED(status))
{
printf("子进程是正常退出的,退出码: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
中间还有一个core dump,下面来介绍一下这个core dump:
#include <iostream>
#include <cstdlib>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
int *p = nullptr;
*p = 1000; // 野指针问题
exit(1);
}
// 父进程
int status = 0;
waitpid(id, &status, 0);
printf("exitcode: %d, signo: %d, core dump flag: %d\n", (status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);
return 0;
}
这里因为野指针的问题,会发送11号信号,这里的core dump是0。
这里我们在man手册中看一下信号码。
这里可以发现,一般是Core的都是因为我们自己的代码问题导致的异常,出现这种问题的,可能是要我们调试的。
这个core dump在服务器中默认是0,是关闭的。
可以通过ulimit -c来增大空间。
然后我们再次调用:
现在core dump就变成1了。
这时我们ll,会发现多了一个core.的文件,后面的.28339是引起异常的进程。
这种机制就叫做核心转储,会把进程在运行中,对应的异常上下文数据,core dump到磁盘上,方便调试。
这里我们创建时使用了-g选项后,进行gbd调试时,直接输入core-file [对应的core.文件],就可以看到异常的位置。
那么为什么服务器中的core dump一般是关闭的呢?
(1) core dump需要配合gdb和-g选项,能用-g的一定是调试Debug版本,能上服务器的都是Release版本的,没法调试。
(2) 各种大厂服务因错误挂掉了后,是没有时间去调试的,一般都是去重启该服务,并且很多都是自动重启的,那么自动重启了很多次之后,每次都有一个core dump,那么过多之后,就可能把空间占满,出现问题。
③ 参数options
再就是第三个参数options:传值0,为阻塞等待,使用宏WNOHANG为非阻塞等待。
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若大于0,说明pid的子进程正常结束,则返回该子进程的ID。
阻塞等待:当我们调用某些函数的时候,因为条件不就绪,需要我们阻塞等待。
本质:就是当前进程自己变成阻塞状态,等条件就绪(任意的软硬件条件)的时候,再被唤醒。
非阻塞等待:当我们调用某些函数的时候,不需要我们一直等待,可以先去做别的事情,也会进行轮调检测查看处理状态。
之前写的都是阻塞等待,就不演示阻塞等待了,这里来演示一下非阻塞等待的。
#include <iostream>
#include <vector>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
typedef void (*handler_t)();
//方法集
std::vector<handler_t> handlers;
void fun1()
{
printf("hello, 我是方法1\n");
}
void fun2()
{
printf("hello, 我是方法2\n");
}
void Load()
{
//加载方法
handlers.push_back(fun1);
handlers.push_back(fun2);
}
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程
while(1)
{
printf("我是子进程, 我的PID: %d, 我的PPID:%d\n", getpid(), getppid());
sleep(3);
}
exit(104);
}
else if(id >0)
{
//父进程
// 基于非阻塞的轮询等待方案
int status = 0;
while(1)
{
pid_t ret = waitpid(-1, &status, WNOHANG);
if(ret > 0)
{
printf("等待成功, %d, exit sig: %d, exit code: %d\n", ret, status&0x7F, (status>>8)&0xFF);
break;
}
else if(ret == 0)
{
//等待成功了,但是子进程没有退出
printf("子进程没好,那么我父进程就做其他事情了...\n");
if(handlers.empty())
Load();
for(auto f : handlers)
{
f(); //回调处理对应的任务
}
sleep(1);
}
else{
//出错了
}
}
}
else
{
//do nothing
}
return 0;
}
这里子进程和父进程都在运行,父进程并没听有因为要等待子进程而耽误自己运行。
四.进程程序替换
1.进程程序替换是什么
子进程执行的是父进程的代码片段。
如果我们想让创建出来的子进程,去执行全新的程序怎么办?
就使用进程程序替换。
程序进程替换的原理:
① 将磁盘中的程序,加载到内存结构
② 重新建立页表映射,谁执行程序替换,就重新建立谁的映射(子进程)。
效果:让父进程和子进程彻底分离,并让子进程执行一个全新的程序。
2.为什么要有进程程序替换
在linux编程的时候,往往需要子进程做两件种类事情:
① 让子进程执行父进程的代码片段(服务器代码)
② 让子进程执行磁盘中的一个权限的程序(shell),想让客户端执行对应的程序,通过我们的进程,执行其他人写的进程代码等待。(需要C/C++变为Python/Java/Shell等等)
3.如何进行程序替换
① 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 execvpe(const char *file, char *const argv[], char *const envp[]);
⑦ int execve(const char *path, char *const argv[], char *const envp[]);
以上7个函数是专门用来进行程序替换的函数。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
我们如果想执行一个全新的程序,需要做这两个事情:
① 先找到这个程序在哪
② 对程序可能携带的选项进行执行
(1)execl
① int execl(const char *path, const char *arg, ...);
第一个参数是寻找环境变量PATH的路径,第二个参数是参数包,需要我们写参数指令,最后必须是NULL,表示参数传递完毕。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
printf("我是父进程,我的pid是: %d\n", getpid());
execl("/usr/bin/ls", "ls", "-l", "-a", NULL); //带选项
//execl("/usr/bin/top", "top", NULL); //不带选项
//execl("/usr/bin/pwd", "pwd", NULL); //不带选项
printf("我执行完毕了,我的pid是: %d, ret: %d\n", getpid(), ret);
return 0;
}
一旦替换成功,就将当前进程的代码和数据全部替换了。
这里我们可以看到,后面的printf是代码,但是并没有输出,因为它被替换了,替换后该代码就不存在了。
因此这个程序替换函数并不需要判断返回值,只要替换成功,就不会有返回值;而失败的时候,必然会继续向后执行,最多通过返回值得到什么原因导致的替换失败。
这里调用下面的execl不带选项的结果如下:
(2)execlp
② int execlp(const char *file, const char *arg, ...);
这个与上面多了个p,就可以自动搜索环境变量PATH了
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
printf("我是父进程,我的pid是: %d\n", getpid());
pid_t id = fork();
if(id == 0)
{
//child
// 让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是: %d\n", getpid());
execlp("ls", "ls", "-a", "-l", NULL);// 这里出现了两个ls, 含义不一样
exit(1); //只要执行了exit,意味着,execl系列的函数失败了
}
// 一定是父进程
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret == id)
{
sleep(2);
printf("父进程等待成功!\n");
}
return 0;
}
带p的exec可以在执行命令的时候,去默认的搜索路径去搜索,而不需要我们手动的写路径,并且结果是相同的。
(3)execle
③ int execle(const char *path, const char *arg, ...,char *const envp[]);
有了e可以自己维护环境变量
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
//环境变量的指针声明
extern char**environ;
printf("我是父进程,我的pid是: %d\n", getpid());
pid_t id = fork();
if(id == 0)
{
//child
// 让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是: %d\n", getpid());
char *const env_[] = {
(char*)"MYPATH=YouCanSeeMe!!",
NULL
};
//e: 添加环境变量给目标进程,是覆盖式的!
execle("./mycmd", "mycmd", NULL, env_);
exit(1); //只要执行了exit,意味着,execl系列的函数失败了
}
// 一定是父进程
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret == id)
{
sleep(2);
printf("父进程等待成功!\n");
}
return 0;
}
mycmd.cpp:
#include <iostream>
#include <stdlib.h>
int main()
{
std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;
std::cout << "-------------------------------------------\n";
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
return 0;
}
带e的exec可以维护自己的环境变量。
这里通过程序替换,去替换到mycmd中,并且维护的环境变量为MYPATH。
如果我们不想修改环境变量,可以这样写:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
//环境变量的指针声明
extern char**environ;
printf("我是父进程,我的pid是: %d\n", getpid());
pid_t id = fork();
if(id == 0)
{
//child
// 让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是: %d\n", getpid());
char *const env_[] = {
(char*)"MYPATH=YouCanSeeMe!!",
NULL
};
//e: 添加环境变量给目标进程,是覆盖式的
execle("./mycmd", "mycmd", NULL, environ);
exit(1); //只要执行了exit,意味着,execl系列的函数失败了
}
// 一定是父进程
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret == id)
{
sleep(2);
printf("父进程等待成功!\n");
}
return 0;
}
再修改一下mycmd:
#include <stdlib.h>
int main()
{
std::cout << "PATH:" << getenv("PATH") << std::endl;
std::cout << "-------------------------------------------\n";
std::cout << "MYPATH:" << getenv("MYPATH") << std::endl;
std::cout << "-------------------------------------------\n";
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
std::cout << "hello c++" << std::endl;
return 0;
}
这里会发现PATH就是系统的环境变量,而MYPATH没了我们的维护就为空了。
当然,如果我们去添加后,就可以输出出来了。
(4)execv
④ int execv(const char *path, char *const argv[]);
这里有了v,就可以用让参数数组实现。而l是列表。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
printf("我是父进程,我的pid是: %d\n", getpid());
pid_t id = fork();
if(id == 0)
{
//child
// 让子进程执行全新的程序,以前是执行父进程的代码片段
printf("我是子进程,我的pid是: %d\n", getpid());
char *const argv_[] =
{
(char*)"ls",
(char*)"-a",
(char*)"-l",
(char*)"-i",
NULL
};
execv("/usr/bin/top", argv_);
exit(1); //只要执行了exit,意味着,execl系列的函数失败了
}
// 一定是父进程
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret == id)
{
sleep(2);
printf("父进程等待成功!\n");
}
return 0;
}
(5)execvp
(6)execvpe
这俩也是如此,根据l、v、p、e的不同有不同的输入。
上面6个是一起的,只有(7)是单独的,因为(7)execve才是真正的系统接口,上面的6个都是对系统接口execve的封装。
那么为什么会有这么多封装的接口呢? 这个就像函数重载一样,是为了适配更多的应用场景。
最后,再记一下l、v、p、e的不同作用。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
五.简易shell实现
shell中有很多命令是通过创建子进程来执行的,但是有一些命令是不能通过创建子进程执行的,这就是内建命令。例如cd等,如果依旧创建子进程执行,就会使命令失效,因为仅仅子进程的位置变了,而shell本身没有变化。
那么内建命令如何做呢,让父进程shell自己执行,不让子进程执行。
环境变量的数据是在进程的上下文中。
环境变量会被子进程继承下去,所以环境变量有全局属性。
当进行程序替换的时候,当前进程的环境变量非但不会被替换,而且是继承父进程的。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SEP " "
#define NUM 1024
#define SIZE 128
char command_line[NUM];
char *command_args[SIZE];
char env_buffer[NUM];
extern char**environ;
//对应上层的内建命令
int ChangeDir(const char * new_path)
{
chdir(new_path);
return 0; // 调用成功
}
void PutEnvInMyShell(char * new_env)
{
putenv(new_env);
}
int main()
{
//shell 本质上就是一个死循环
while(1)
{
// 1. 显示提示符
printf("[李四@我的主机名 当前目录]# ");
fflush(stdout);
// 2. 获取用户输入
memset(command_line, '\0', sizeof(command_line)*sizeof(char));
fgets(command_line, NUM, stdin); //键盘,标准输入,stdin, 获取到的是c风格的字符串, '\0'
command_line[strlen(command_line) - 1] = '\0';// 清空\n
// 3. 字符串切分"ls -a -l -i" -> "ls" "-a" "-l" "-i"
command_args[0] = strtok(command_line, SEP);
int index = 1;
// 给ls命令添加颜色
if(strcmp(command_args[0]/*程序名*/, "ls") == 0 )
{
command_args[index++] = (char*)"--color=auto";
}
// strtok截取成功,返回字符串其实地址
// 截取失败,返回NULL
while(command_args[index++] = strtok(NULL, SEP));
// 4. 内建命令
if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
{
ChangeDir(command_args[1]); //让调用方进行路径切换, 父进程
continue;
}
if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
{
// 目前,环境变量信息在command_line,会被清空
// 此处我们需要自己保存一下环境变量内容
strcpy(env_buffer, command_args[1]);
PutEnvInMyShell(env_buffer); //export myval=100, BUG?
continue;
}
// 5. 创建进程,执行
pid_t id = fork();
if(id == 0)
{
// child
// 6. 程序替换
execvp(command_args[0], command_args);
exit(1); //执行到这里,子进程一定替换失败
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("等待子进程成功: sig: %d, code: %d\n", status & 0x7F, (status>>8) & 0xFF);
}
}
}