我们前面了解到了fork函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
那么在复习fork函数之前先来解决几个问题
问题1:如何理解fork函数有两个返回值?
问题2:如何理解fork函数返回之后,给父进程返回子进程的pid,给子进程返回0?
问题3:如何理解同一个id,怎么能够保存两个不同的值,同时执行if和else if语句呢?
这里我们就一一来解答一下,先来看问题2:
我们知道现实生活中,一个孩子只有一个父亲,但是一个父亲可以有多个儿子。父亲为了区分每一个儿子就会给他们取不同的名字,方便父亲找到对应的孩子。那么进程也是同理,父进程可以有多个子进程,但是子进程只能有一个父进程,所以fork函数给父进程返回子进程pid就是为了方便找到对应的子进程,然后对该子进程进行相关操作
就先来来解决问题1:
当我们的代码走到fork函数的时候,就开始创建子进程了,进行上面图片中的一系列操作,最终创建好了子进程。但是,我们知道,在return的时候,我们程序的核心代码已经全部完成了,也就是说,return的时候,子进程已经被创建出来了。那么在return的时候,就已经有了两个进程,一个父进程,一个子进程;这个时候两个进程各自调用return语句,所以有了两个返回值。
知识点:
返回的本质就是:写入
那么,由于父进程和子进程的先后执行顺序由调度器决定的,所以我们也不知道谁先返回。但是,谁先返回,谁就先
写入id;又因为上节我们学到的知识,因为进程具有独立性,所以先返回的进程会发生写时拷贝
最后我们来解决问题3:
我们前面学了进程地址空间,那么这里就不难理解了。我们同一个id,地址一样,但是内容却不一样,因为进程的独立性,发生了写时拷贝,所以id的地址相同,内容不同。然后,fork函数调用完之后,父进程的代码是被父子进程共享的,相当于父进程执行一遍,子进程执行一遍,那么执行两遍代码,得出两个结果,然后执行对应的if和else if语句也就不奇怪了
# include
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
1、分配新的内存块和内核数据结构给子进程
2、将父进程部分数据结构内容拷贝至子进程
3、添加子进程到系统进程列表当中
4、fork返回,开始调度器调度
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序:
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运行结果:
这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢?如下图所示:
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定
我们在学习一下写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
简单来说:哪一个进程先要改变物理内存中数据,该进程就发生写时拷贝,会在物理内存重新找一块空间拷贝原来物理内存的数据,然后重新与进程地址空间构成映射关系
1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
2、一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1、系统中有太多的进程
2、实际用户的进程数超过了限制
提问:我们前面写main函数为什么总是要在最后写一个return 0呢?
在linux中,这个0是进程退出的时候,对应的
退出码
。这个退出码能够标定进程执行结果是否正确
我们先来看看一段代码:
1 #include
2 #include
3 int addsum(int a,int b)
4 {
5 int ret=0;
6 for(int i=a;i
我们这个程序的本意是:判断结果是不是等于5050,是退出码为0,否则退出码为1
但是,可以看到./a.out的时候,没有显示退出码。不过指令echo $?可以显示退出码
就下来就介绍这个指令
echo $?
永远记录最近一个进程在命令行中执行完时对应的退出码(也就是:main -> return ?)
小结
如何设定main函数返回值呢??如果不关心进程退出码,return 0就行
如果未来我们是要关心进程退出码的时候,要返回特定的数据表明特定的错误
一般而言,我们返回0表示成功,非0表示失败
退出码的意义:>0:success, !0:标识失败, !0具体是几,标识不同的错误
但是数字对人不友好,对计算机友好,所以,我们一般而言,退出码,都必须有对应的退出码的文字描述,1. 可以自定义 2. 可以使用系统的映射关系(不太频繁)
那么,我们接下来就来看看系统中有哪一些帮我们设置好的退出码,以及每个退出码对应的信息:
#include
int main()
{
for(int i=0;i<200;++i)
{
printf("%d:%s\n",i,strerror(i));
}
}
可以看到有100多种错误码,其中0表示正确,其他的每个错误码都对应不同的错误信息
进程退出一共无非三种情况:
情况1:代码执行完了,结果正确——return 0
情况2:代码执行完了,结果不正确——return !0(退出码这个时候起效果)
情况3:代码没有执行完,程序异常,退出码无意义
1、main函数,return返回
2、任意地方调用void exit(int status)函数,status就是退出码
举例:
1 #include
2 #include
3 #include
4 #include
5 int addsum(int a,int b)
6 {
7 int ret=0;
8 for(int i=a;i
3、任意地方调用_exit(int status)
函数,status就是退出码
1 #include
2 #include
3 #include
4 #include
5 int addsum(int a,int b)
6 {
7 int ret=0;
8 for(int i=a;i
exit是库函数,而_exit是系统调用;所以,两个是上下层关系。其中,exit在_exit的上层
接下来举例说明:
printf("hello");//前面学到过,不带\n的话,没有进行行刷新,所以hello在缓存区里面不被刷新,等到sleep两秒之后才被刷新打印出来
sleep(2);
exit(1);
这里的hello的确过了两秒被打印出来了,因为exit函数把缓存区的数据刷出来了
printf("hello");
sleep(2);
_exit(1);//这里换成_exit函数
这里不会打印hello,也就是说_exit函数不会刷新缓存区
exit函数结束进程,会主动刷新缓存区(前面学过,exit就是对_exit系统函数进行了封装)
_exit函数结束进程,不会主动刷新缓存区
缓存区的位置在:用户层,不在内核层(OS)。因为exit函数是库函数,在用户层;而_exit函数是系统调用函数,在内核层。如果缓存区在内核层,那么_exit函数也会刷新缓存区的,由此可见,缓存区是在用户层的,是一个用户级的缓存区
我们前面学到了进程的一种状态——Z:僵尸状态
,这种状态的进程是要等待父进程回收僵尸进程的退出信息来处理的,不然的话会造成内存泄漏等问题
那么可以通过进程等待的方式来解决僵尸状态进程的问题!
·之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
·另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
·最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
·父进程通过进程等待的方式:1、回收子进程资源;2、获取子进程退出信息
所以进程要等待的原因就是:1、回收子进程资源;2、获取子进程退出信息
这里主要讲如何回收子进程资源,至于获取子进程退出信息,下面再讲
接下来我们直接来见见进程等待,首先要介绍等待接口wait
wait函数等待成功就返回子进程的pid,否则就返回-1
#include
#include
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
9 pid_t id = fork();
10 if(id == 0) //子进程
11 {
13 int cnt = 5;
14 while(cnt)
15 {
16 printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
17 sleep(1);
20 }
26 exit(12); //子进程退出
27 }
28 // 父进程
29 //sleep(15);
30 pid_t ret = wait(NULL);
31 if(id > 0)
{
printf("wait success: %d\n",ret);
}
38 sleep(5); //父进程休眠5秒,观察子进程被回收的过程
接下来我们继续深入学习一下,怎么获取子进程退出信息。这就就要用到waitpid
接口了
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid:
Pid==-1,等待任一个子进程。与wait等效。
Pid>0,等待其进程ID与pid相等的子进程。
pid == 0 等待其组I D等于调用进程的组I D的任一子进程。换句话说是与调用者进程同在一个组的进程。
pid < -1 等待其组I D等于p i d的绝对值的任一子进程。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
0:阻塞式等待
WNOHANG: 非阻塞等待
若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
1、wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
2、如果传递NULL,表示不关心子进程的退出状态信息。
3、否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
4、status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
这个core dump标志我们后面再讲。
终止信号:表示代码是否执行完毕
退出状态:表示代码结果是否正确
注意:如果终止信号不为0,也就是代码没有正常跑完,中途挂了,那么退出状态就是0,因为此时退出状态的位图全都是0,不会被填写
接下来我们还是见见猪跑:
int main()
{
pid_t id = fork();
if(id == 0) //子进程
{
int cnt = 5;
while(cnt)
{
printf("我是子进程: %d, 父进程: %d, cnt: %d\n", getpid(), getppid(), cnt--);
sleep(1);
}
// 运行完
// 1. 代码完,结果对
// 2. 代码完,结果不对
// 异常
// 3. 代码没跑完,出异常了
exit(12); //进程退出
}
// 父进程
//sleep(15);
//pid_t ret = wait(NULL);
int status = 0; // 不是被整体使用的,有自己的位图结构
pid_t ret = waitpid(id, &status, 0);
if(id > 0)
{
printf("wait success: %d, sig number: %d, child exit code: %d\n", ret, (status & 0x7F), (status>>8)&0xFF);
}
sleep(5);
知识点:
1、如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
2、如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
3、如果不存在该子进程,则立即出错返回。
但是一直使用位操作的方法来获取退出状态和终止信号太复杂了,所以linux中有特定的宏来完成:
int ret = waitpid(id, &status, 0);
if(ret > 0)
{
if(WIFEXITED(status)) // 是否正常退出
{
printf("exit code: %d\n", WEXITSTATUS(status)); // 判断子进程运行结果是否ok
}
else
{
//TODO
printf("child exit not normal!\n");
}
//printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
//不再用位操作这么麻烦了
}
子进程在退出的时候,会释放掉子进程的代码和数据,但是子进程的退出信息(退出码和退出信号)会保存在子进程的PCB(进程控制块)里面(进程退出,pcb不会消失,要等待父进程来处理)。
此时,子进程就变成了Z状态也就是僵尸状态,如果父进程调用wait/waitpid函数接口,通过id找到对应的僵尸进程,把该进程的退出信息(退出状态,终止信号)写入status里面,通过&status的操作拿到僵尸进程的退出状态和终止信号,返回僵尸进程的pid。最后,父进程在将子进程的PCB(进程控制块)释放掉,这样才算将一个子进程退出了
waitpid的第三个参数options为0时表示阻塞等待,为WNOHANG表示非阻塞等待
阻塞等待:当父进程执行的waitpid函数时,如果子进程没有退出,父进程就只能阻塞在waitpid函数,一直等到子进程退出。然后,父进程通过waitpid函数读取子进程退出信息之后,才能继续执行后面的代码
非阻塞等待:当父进程执行的waitpid函数时,如果子进程没有退出,父进程会直接读取子进程的状态并且返回,然后父进程继续执行其他代码完成其他任务,不用等待子进程退出
轮询的前提条件是:非阻塞等待
在非阻塞等待的前提下,父进程多次/循环的非阻塞等待子进程就形成了轮询。父进程在轮询期间并不是一直进行非阻塞等待子进程,还会做其他工作
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 int main()
9 {
10 pid_t id = fork();
11 assert(id != -1);
12 if(id == 0)
13 {
14 //child
15 int cnt = 10;
16 while(cnt)
17 {
18 printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
19 sleep(1);
20 }
21
22 exit(10);
23 }
24
25 // parent
26 int status = 0;
27 while(1)
28 {
29 pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞-> 子进程没有退出, 父进程检测时候,立即返回
30 if(ret == 0)
31 {
32 // waitpid调用成功 && 子进程没退出
33 //子进程没有退出,我的waitpid没有等待失败,仅仅是监测到了子进程没退出.
34 printf("wait done, but child is running...., parent running other things\n");
35 }
36 else if(ret > 0)
37 {
38 // 1.waitpid调用成功 && 子进程退出了
39 printf("wait success, exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
40 break;
41 }
42 else
43 {
44 // waitpid调用失败
45 printf("waitpid call failed\n");
46 break;
47 }
48 sleep(1);
49 }
50 }
1、为了回收子进程资源和获取子进程退出信息,我们需要进行进程等待;
2、进程等待的本质是父进程从子进程的 task_struct(进程控制块PCB)中读取退出信息,然后保存到 status 中;
3、我们可以通过 wait 和 waitpid 系统调用接口对子进程进行进程等待;
4、status参数是一个输出型参数,父进程通过 wait/waitpid 函数将子进程的退出信息写入到 status 中;
5、status以位图方式存储,包括退出状态和退出信号,若退出信号不为0,则退出状态无效;
6、我们可以使用系统提供的宏 WIFEXITED 和WEXITSTATUS 来分别获取 status 中的退出状态和退出信号;
7、进程等待的方式分为阻塞式等待与非阻塞式等待,阻塞式等待用0来标识,非阻塞式等待用宏 WNOHANG 来标识;
8、非阻塞式等待不会等待子进程退出,所以我们需要以轮询的方式来不断获取子进程的退出信息。而父进程也会在轮询期间做其他的工作
上面我们说了,创建一个子进程无非有两个目的:
1、想让子进程执行父进程代码的一部分(截止到进程等待为止,我们的fork创建子进程都是在完成这个目的)
2、为了让子进程执行一个全新的代码(进程程序替换就是:让子进程想办法加载磁盘上指定的程序,执行该指定程序的代码和数据)
刚才已经提到了:
让子进程想办法加载磁盘上指定的程序,执行该指定程序的代码和数据
并且,原来进程的task_struct(进程控制块)和mm_struct(进程地址空间)以及进程的pid都不会改变,只是有可能改变页表(按需索取,也有可能页表不会改变)。所以,>进程程序替换并不会创建一个新进程,而是让原来的进程执行我们指定程序的代码和数据
原理就是:
用新程序(我们指定的程序)的代码和数据来替换原来进程物理地址中的代码和数据,除了可能改变原来进程页表的映射关系之外,原来进程的task_struct和mm_struct等等内核数据都不会改变
linux中提供了一系列的exec函数来实现进程程序替换操作,其中有一个系统调用,六个库函数。这六个库函数都是对那个系统调用接口进行的封装等处理:
可以看到,man之后,man 2也就是系统调用接口就一个,而man 3库函数有6个。不过我们主要学习的还是库函数的那六个,毕竟就行了封装等处理,使用更加简单、方便
这六个库函数统称为exec函数
:
#include `
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
因为exec函数一旦调用成功,就表示我们已经把新程序(指定程序)的代码和数据给替换到物理地址了。原来进程的代码和数据就不会被执行了,也就不存在有返回值。如果exec函数调用失败,原程序的代码和数据才能够继续往下执行,这个时候exec函数返回值才会被使用,也就是返回-1
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
函数名 | 参数格式 | 是否需要带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 否 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 否 | 否,需要自己组装环境变量 |
execv | 数组 | 否 | 是 |
execvp | 数组 | 是 | 是 |
execve | 数组 | 否 | 否,需要自己组装环境变量 |
我们上面的六个函数接口其实都是具有一定相对应关系的
我们执行程序分为两个步骤:
1、找到执行程序
2、指定程序执行的方式
而exec函数中,带上p和不带p表示查找程序;l和v来表示指定程序执行方法;e表示指定环境变量
int execl(const char *path, const char *arg, …);
第一个参数:找到要执行的程序地址
剩下参数:按照什么方式执行 (你在linux命令行怎么执行,就怎么传参)
最后的三个点(…):可变参数列表——给函数传递不同个数个参数(参数可以传多个)
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8 int main()
9 {
10 printf("process is running...\n");
11 execl("/usr/bin/ls","ls","--color=auto",NULL);//这里要注意, 所有execl函数最后都要以NULL结尾,证明我们把参数传完了
12 printf("process running done...\n");//这里的printf在execl函数之后的,execl执行完之后,代码和数据已经全部覆盖了,这个时候开始执行新的代码了,所以printf就无法执行了
//只有execl函数调用失败的时候,才打印该printf的内容
13
14 }
还有很多用法: execl(“/usr/bin/ls”,“ls”,“–color=auto”,NULL),我们可以把ls改成mv,pwd…只要路径正确,后面的参数全部都是字符串类型的,内容就是我们的选项,只要最后带上NULL就行
而我们一般进程替换都是子进程执行的,因为进程具有独立性,所以一般都fork创建一个子进程之后,子进程来进行进程替换:
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8
9 int main()
10 {
11 pid_t id = fork();
12 if(id < 0)
13 {
14 perror("fork");//打印失败原因
15 return 1;
16 }
17 else if (id == 0)//子进程
18 {
19 printf("pid: %d, child process is runnning...\n", getpid());
20 int ret = execl("/usr/bin/ls", "ls", "-l", "-a", "--color=auto", NULL); //进程程序替换
21 if(ret < 0)//替换失败执行下面语句
22 {
23 printf("process exec failed\n");
24 exit(1);
25 }
26 printf("pid: %d, child process is done...\n", getpid());
27 return 0;
28 }
29 //父进程
30 int status = 0;
31 pid_t ret = waitpid(id, &status, 0); //进程等待
32 if(ret < 0)
33 {
34 perror("waitpid");
35 return 1;
36 }
37 else
38 {
39 printf("wait pid: %d, exit signal: %d, exit code: %d\n", ret, (status & 0x7f), (status >> 8 & 0xFF));
40 }
41 return 0;
42 }
以前我们只是知道数据被写入会发生写时拷贝,现在代码也会发生写时拷贝,在进程程序替换的时候,代码就会发生写时拷贝
int execlp(const char *file, const char *arg, …);
这里多了一个字符p,接下来介绍一下:
exec函数带了p,就表示函数不需要知道程序路径,只要告诉函数要替换成为什么内容,函数会自动在环境变量PATH里面找可执行程序,然后进行替换
所以,带了p的exec系列函数,参数不需要路径,只需要替换程序。当然,前提条件是:在环境变量PATH中存在替换程序,不存在也是不能够完成函数调用的!!!
…三个点是可变参数列表:给函数传递不同个数个参数(参数可以传多个)
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7 #include
8
9 int main()
10 {
11 pid_t id = fork();
12 if(id < 0)
13 {
14 perror("fork");
15 return 1;
16 }
17 else if (id == 0)
18 {
19 printf("pid: %d, child process is runnning...\n", getpid());
20 execlp("ls", "ls", "-l", "-a", "--color=auto", NULL); //这里第一个参数就不需要带上路径了,直接给替换程序就行,因为该替换程序在环境变量PATH里面存在
21 exit(-1);
22 }
23 int status = 0;
24 pid_t ret = waitpid(id, &status, 0);
25 if(ret >0)
26 {
27 printf("wait pid: %d, exit signal: %d, exit code: %d\n", ret, (status & 0x7f), (status >> 8 & 0xFF));
28 }
29 return 0;
30 }
int execv(const char *path, char *const argv[]);
这里带了一个v,参数变成了一个指针数组argv
也就是说:带v,可以将所有的执行参数放入数组中,统一传递,不需要使用可变参数的方案
char* const argv[]={
(char *) "ls",
(char*) "-a",
"-l",//这里是警告,我们强转一下就行,不转也没有事
"--color=auto",
NULL
};
execv("/usr/bin/ls",argv);
int execvp(const char *file, char *const argv[]);
这就不用多说了,直接拿捏了
char* const argv[]={
(char *) "ls",
(char*) "-a",
"-l",//这里是警告,我们强转一下就行,不转也没有事
"--color=auto",
NULL
};
execv("ls",argv);
接下来我们讲点其他的:
Makefile:
1 .PHONY:all
2 all: mybin points
3
4 mybin:mybin.c
5 gcc -o $@ $^ -std=c99
6 points:points.c
7 gcc -o $@ $^ -std=c99
8 .PHONY:clean
9 clean:
10 rm -f points mybin
~
这里我们直接生成两个可执行程序:
接下来我们要让points来调用mybin
execl("./mybin","mybin",NULL);
这里就通过一个程序调用起另一个程序了
int execle(const char *path, const char *arg, …,char *const envp[]);
e就是指自定义环境变量
也就是说我们使用带e的函数的时候,可以将环境变量传进来
mybin.c:
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int main()
5 {
6 // 系统就有
7 printf("PATH:%s\n", getenv("PATH"));
8 printf("PWD:%s\n", getenv("PWD"));
9 // 自定义
10 printf("MYENV:%s\n", getenv("MYENV"));
11
12 printf("我是另一个C程序\n");
13 printf("我是另一个C程序\n");
14 printf("我是另一个C程序\n");
15 printf("我是另一个C程序\n");
16 printf("我是另一个C程序\n");
17 printf("我是另一个C程序\n");
18 printf("我是另一个C程序\n");
19 printf("我是另一个C程序\n");
20 printf("我是另一个C程序\n");
21
22 return 0;
23 }
~
char *const envp_[] = {
(char*)"MYENV=11112222233334444",
NULL
};
execle("./mybin", "mybin", NULL, envp_); //自定义环境变量
char *const envp_[] = {
(char*)"MYENV=11112222233334444",
NULL
};
extern char **environ;//系统环境变量 实际上,默认环境变量你不传,子进程也能获取
extern char **environ;
//execle("./mybin", "mybin", NULL, envp_); //自定义环境变量
putenv((char*)"MYENV=4443332211"); //将指定环境变量导入到系统中 environ指向的环境变量表
execle("./mybin", "mybin", NULL, environ);
我们前面知道我们写的程序要被加载到内存中,那么谁加载我们的程序到内存呢?
答案就是exec系列的函数
exec系列函数把我们的程序加载到内存中,又因为程序是先被加载,任何被执行的,其中main函数也是函数,也要被调用、执行、传参、加载的。所以,exec系列函数是比main函数先执行的
程序替换的execve系统调用,其他的都是封装,为了让我们有更多的选择性
exec系列的函数可以替换调用任何后端语言
学习了上面的知识,我们可以自己写一个迷你版的shell,主要包含下面几点:
1、在界面输出提示符
2、从终端获取命令行输入
3、解析命令行输入信息
4、创建子进程
5、进程程序替换
6、进程等待
7、进程终止
#include
#include
#include
#include
#include
#include
#define NUM 1024 //一个命令的最大长度
#define OPT_NUM 64 //一个命令的最多选项
char lineCommand[NUM];
char* argv[OPT_NUM];
int main() {
while(1) {
//输出提示符
printf("[用户名@主机名 当前路径]$ ");
fflush(stdout);
//获取输入
char* ret = fgets(lineCommand, sizeof(lineCommand)-1, stdin); //最后留一个位置来存放极端情况下的\0
if( ret == NULL ) {
perror("fgets");
exit(1);
}
lineCommand[strlen(lineCommand) - 1] = '\0'; //消除命令行中最后的换行符
//将输入解析为多个字符串存放到argv中,即字符串切割
argv[0] = strtok(lineCommand, " ");
int i = 1;
while(argv[i++] = strtok(NULL, " "));
//创建子进程
pid_t id = fork();
if(id == -1) {
perror("fork");
exit(1);
} else if (id == 0) { //子进程
//进程程序替换
int ret = execvp(argv[0], argv);
if(ret == -1) {
printf("No such file or directory\n");
exit(1);
}
} else { //父进程
//进程等待
pid_t ret = waitpid(id, NULL, 0);
if(ret == -1){
perror("wait");
exit(1);
}
}
}
return 1; //while循环正常情况下不会结束
}
这里shell什么都可以,但是,cd之后,pwd不会改变路径
exe表示当前进程执行的是磁盘的哪一个程序
cwd表示当前进程的工作目录——这才是我们说的当前路径
接下来我们就来解释为什么上面我们的shell进行cd之后,pwd没有变化:
myshell 是通过创建子进程的方式去执行命令行中的各种指令的,也就是说,cd 命令是由子进程去执行的,那么自然被改变也是子进程的工作目录,父进程的工作目录不受影响。而当我们使用 PWD 指令来查看当前路径时,cd 指令对应的子进程已经执行完毕退出了。cd和PWD是两条命令。此时 myshell 又会给 PWD 创建一个新的子进程,且这个子进程的工作目录和父进程 myshell 相同,所以 PWD 打印出来的路径不变。
当然,我们也是可以改当前工作目录的,通过chdir系统调用接口来改变一个进程的工作目录
所以,我们的shell迷你版想要cd之后,pwd能够更改路径就需要下面的一段代码:
if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
{
if(myargv[1] != NULL) chdir(myargv[1]);
continue;
}
不需要让我们的子进程来执行,而是让shell自己执行的命令就叫做内建命令
内建命令由shell程序来完成的,它的功能是在bash中实现的,不需要创建子进程来完成,也不用外部程序文件来运行;是通过shell本身来完成内建命令的。
而外部命令则是通过创建子进程,再进行进程程序替换,运行外部程序文件等方法来完成的
我们可以通过type命令来区分内建命令和外部命令
所以,我们上面的cd命令就是以内建命令的方法来处理的,myshell遇到cd命令是,自己直接来进行cd,改变了进程工作目录,处理完之后continue,并不会创建子进程进行cd。但是,pwd目录我们没有将其处理为内建命令
同时,这也解释了为什么前面的echo $变量
可以查看本地变量、echo $?
为什么可以获取最近一次的进程退出码了
因为echo就是一个内建命令,由shell直接完成操作,不用创建echo子进程。虽然本地变量只在当前进程有效,但是使用 echo 查看本地变量时,shell 并不会创建子进程,而是直接在当前进程中查找,自然可以找到本地变量;
同理,shell 可以通过进程等待的方式获取上一个子进程的退出状态,然后将其保存在 ? 变量中,当命令行输入 “echo $?” 时,直接输出 ? 变量中的内容,不需要创建子进程,然后将 ? 置为0 (echo 正常退出的退出码)
myshell添加echo的功能代码:
int EXIT_CODE; //退出码 -- 全局变量
if(argv[0] != NULL && strcmp(argv[0], "echo") == 0) //处理echo内建命令
{
if(strcmp(argv[1], "$?") == 0){ //echo $?
printf("%d\n", EXIT_CODE);
EXIT_CODE = 0;
} else { //echo $变量
printf("%s\n", argv[1]+1);
}
continue;
}
//fork后面的内容
} else { //父进程
int status = 0;
pid_t ret = waitpid(id, &status, 0); //进程等待
EXIT_CODE = (status >> 8) & 0xFF; //获取退出码
if(ret == -1){
perror("wait");
exit(1);
}
}
1 #include <stdio.h>
2 #include <string.h>
3 #include <stdlib.h>
4 #include <unistd.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
7 #include <assert.h>
8
9 #define NUM 1024
10 #define OPT_NUM 64
11
12 char lineCommand[NUM];
13 char *myargv[OPT_NUM]; //指针数组
14 int lastCode = 0;
15 int lastSig = 0;
16
17 int main()
18 {
19 while(1)
20 {
21 // 输出提示符
22 printf("用户名@主机名 当前路径# ");
23 fflush(stdout);
24
25 // 获取用户输入, 输入的时候,输入\n
26 char *s = fgets(lineCommand, sizeof(lineCommand)-1, stdin);
27 assert(s != NULL);
28 (void)s;
29 // 清除最后一个\n , abcd\n
30 lineCommand[strlen(lineCommand)-1] = 0; // ?
31 //printf("test : %s\n", lineCommand);
32
33 // "ls -a -l -i" -> "ls" "-a" "-l" "-i" -> 1->n
34 // 字符串切割
35 myargv[0] = strtok(lineCommand, " ");
36 int i = 1;
37 if(myargv[0] != NULL && strcmp(myargv[0], "ls") == 0)
38 {
39 myargv[i++] = (char*)"--color=auto";
40 }
41
42 // 如果没有子串了,strtok->NULL, myargv[end] = NULL
43 while(myargv[i++] = strtok(NULL, " "));
44
45 // 如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
46 // 像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
47 if(myargv[0] != NULL && strcmp(myargv[0], "cd") == 0)
48 {
49 if(myargv[1] != NULL) chdir(myargv[1]);
50 continue;
51 }
52 if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0], "echo") == 0)
53 {
54 if(strcmp(myargv[1], "$?") == 0)
55 {
56 printf("%d, %d\n", lastCode, lastSig);
57 }
58 else
59 {
60 printf("%s\n", myargv[1]);
61 }
62 continue;
63 }
64 // 测试是否成功, 条件编译
65 #ifdef DEBUG
66 for(int i = 0 ; myargv[i]; i++)
67 {
68 printf("myargv[%d]: %s\n", i, myargv[i]);
69 }
70 #endif
71 // 内建命令 --> echo
72
73 // 执行命令
74 pid_t id = fork();
75 assert(id != -1);
76
77 if(id == 0)
78 {
79 execvp(myargv[0], myargv);
80 exit(1);
81 }
82 int status = 0;
83 pid_t ret = waitpid(id, &status, 0);
84 assert(ret > 0);
85 (void)ret;
86 lastCode = ((status>>8) & 0xFF);
87 lastSig = (status & 0x7F);
88 }
89 }