Linux进程控制
1.Linux进程创建
1.1 fork()函数的基本了解
在linux中fork()函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程!
- fork()函数的头文件:
#include<unistd.h>
- fork()函数原型:
pid_t fork(void)
- fork()函数返回值:
父程序返回子程序PID,子程序返回0,出错返回-1
1.2 fork()调用期间,内核的操作
进程调用fork,当控制转移到内核中的fork代码后,内核做了一下事情:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
当一个进程调用fork之后,父子进程共享代码,子进程写时拷贝父进程数据,每个进程都将可以开始它们自己的执行,看如下程序:
//给了if分流父子进程的fork现象
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("Now PID:%d\n",getpid());
pid_t id=fork();
if(id<0)
{
perror("fork error!\n");
return 1;
}
else if(id==0)
{
//child
printf("child pid:%d return:%d\n",getpid(),id);
sleep(2);
}
else
{
//father
printf("father pid:%d return:%d\n",getpid(),id);
sleep(2);
}
return 0;
}
再来看一个代码现象:
//未给if分流父子进程的fork现象
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
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。进程9045先打印before消息,然后它又打印after。另一个after 消息有9046打印的。注意到进程9046没有打印before,为什么呢?如下图所示:
上图解析:
- fork之前,我们是先执行before,所以第一行我们打印除了befor
- fork执行时,子进程写时拷贝一份父进程数据,fork返回两个返回值分别给父子进程
- fork之后,父子进程代码共享,但fork返回值有两个,于是执行后续的代码2次,打印两个after
值得注意的是:
- fork之后,父子进程谁先执行,完全由调度器决定
- fork之后,并不是重新创建一个新的子进程,而是使用老的父进程,也就是我们说的父子进程代码共享,所以进程还是一个,我们说的父子进程只是方便描述
- 发生写时拷贝,父子进程的虚拟地址相同,物理地址的映射不同,所以最后父子进程获得的getpid()和pid值是不一样的
1.3 fork()函数的返回值
思考一些问题:
- 为何要给子进程返回0,给父进程返回子进程的pid?
- 如何理解fork有两个返回值的问题?
第一个问题:
- 首先父子进程的 1 : n 的关系,所以在父子进程的立场中,父进程不需要标识,子进程需要标识。其次子进程是要执行任务的,父进程需要区分子进程,所以给父进程返回子进程的pid,因为父进程可以通过这个pid来区分是哪个子进程。给子进程返回0,本质上是因为子进程不需要访问父进程pid,因为子进程也不需要知道父进程pid,子进程不需要管理父进程,任务是给子进程的,它只需要知道自己调用成功了就可以
第二个问题:
1.4 写时拷贝技术
通常父子代码共享,父子在不修改时,数据也是共享的,当任意一方试图修改时,便以写时拷贝的方式各自一份副本。具体见下图:
对于写时拷贝的理解:
- 对这里的"共享"怎么理解?
- 答:父子进程对应的页表指向的是同一块物理内存。当任何一方写入的时候,以便使用写时拷贝的方式生成一份副本
- 为何要写时拷贝?
- 答:进程具有独立性!
- 为何不在创建的时候就分开?
- 答:子进程不一定会使用父进程的所有数据,写入,本质是需要的时候!也就是按需分配,这种方式还做到了一点:延时分配,因为当被创建的时候,不一定被立马调度,如果不立马被调度,那就不需要先给它分配空间。因为要是先给它分配空间了,那也就是在它被调度之前的时间段中,系统可用的内存是变少的,所以延时分配永远可以保证系统可用资源是最大化的!所以延时分配的本质是:可以高效使用任何内存空间!
- 为何代码不会写时拷贝?
- 答:90%的情况不会(但是不代表不能),因为我们学语言到现在,我们要改的永远是数据,我们没有在让程序运行的时候,改程序运行的逻辑
1.5 fork()函数的常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
1.6 fork()函数调用失败的原因
- 系统中有太多的进程:内存空间上的不支持
- 实际用户的进程数超过了限制:操作系统上的不支持
2.Linux进程终止
2.1 进程退出的三种情况
- 代码正常运行完毕,结果正确
- 代码正常运行完毕,结果不正确
- 代码运行异常
2.2 进程退出的常见方式
- 正常终止(可以通过
echo $?
查看最近一次进程退出码):
- 从main函数中return返回
- 调用exit()函数返回
- 调用_exit()函数返回
- 异常退出:ctrl+c,信号中止
Linux下错误码打印方法:
#include<stdio.h>
#include<string.h>
int main()
{
int i=0;//这里这样写是因为不支持C99,需要在编译的时候加个-std=c99
for(i=0;i<150;i++)
{
printf("错误码序号:%d,错误信息:%s\n",i,streror(i));
}
return 0;
}
注:Linux下只有133个错误码,每个错误码都有对应的错误信息!
2.3 exit()和_exit()的区别
两者的区别在于,exit会在进程结束做一些收尾工作(比如刷新缓冲区),而_exit不会,看下面的对比:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void show()
{
printf("我是show()函数!");
exit(10);
}
int main()
{
show();
printf("我是main()函数!");
return 0;
}
我们把exit()函数换成_exit()函数来看看效果:
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
void show()
{
printf("我是show()函数!");
_exit(10);
}
int main()
{
show();
printf("我是main()函数!");
return 0;
}
从上面来个代码效果和剖析函数原型我们可以得出结论:
- exit最后也调用了_exit,但exit还做了其他工作:
执行用户通过 atexit或on_exit定义的清理函数
关闭所有打开的流,所有的缓存数据均被写入
调用_exit
- exit和return的区别:
exit是终止整个进程,任何地方调用都会终止整个进程
return是终止函数,等同于exit(n)
2.4 关于退出码的一些思考
由退出码想到了一系列问题:
- 进程异常退出,退出码还有意义吗?
- 没有意义!说简单点,到异常的地方就已经被终止了,根本没有执行 return
- 进程终止了,操作系统在做什么?
- 释放曾经申请的数据结构,释放曾经申请的内存,从各种队列等数据中移除
- 为何要有进程等待?
- 回收子进程资源
- 获取子进程退出信息
3.Linux进程等待
3.1 进程为什么要等待
- 回收僵尸进程,解决内存泄漏(僵尸进程是杀不死了,kill -9不能干掉僵尸进程)
- 获取子进程的运行结束状态(交给子进程的工作做的怎么样了)
- 父进程晚于子进程退出,规范化的进行资源回收
等待的本质也是管理的一种方式,OS的核心就在于管理二字
简而言之,进程等待的原因:获取子进程的信息和防止内存泄漏
3.2 等待函数wait与waitpid的理解
我们用man 2 wait来看一下wait和waitpid函数的官方文档:
补充:在普通状态下用man -2 wait
查看,在vim下用! man 2 wait
可以快速查看
wait与waitpid理解:
- 头文件:
#include<sys/types.h>和#include<sys/wait.h>
- wait返回值:等待成功返回子进程pid,等待失败返回-1
- waitpid返回值:等待成功返回子进程的pid,如果指定了WNOHANG选项,且子进程正在运行(没有已退出的子进程可收集)则返回0,等待失败返回-1
3.3 wait()的使用
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
wait的使用案例:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 0;
while(count < 5)
{
printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
count++;
}
exit(0);
}
else
{
printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
pid_t ret = wait(NULL);
if(ret >= 0)
{
printf("wait child success!, %d\n",ret);
}
printf("Father running...");
sleep(5);
}
return 0;
}
- 结论:由此可见,在父进程等待的时候,子进程在运行,重要的是,在子进程运行结束后没有看到Z状态进程,这是因为父进程在等待子进程结束,然后回收子进程
- 一点小问题:
在子进程运行期间,父进程wait的时候,父进程在做什么?
- 就是在 “等” 什么也没干,就是在等子进程退出,这种等子进程退出的过程叫做阻塞等待
- 因为父子谁先运行不确定,但是wait之后,大部分情况都是子进程先退出,父进程读取子进程退出信息,父进程才退出。建议大家以后一定要让父进程等待子进程退出,如果不等的话,一定会导致僵尸进程的问题
补充:上面ps命令下面的########的显示方法
ps ajx | grep test(代码文件名称) | grep -v grep; sleep 1; echo "###########"; done
//这个代码里的while、do、done是shell编程里使用的,自行了解就行了
3.4 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相等的子进程
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞
- 如果不存在该子进程,则立即出错返回
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 0;
while(count < 5)
{
printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
count++;
}
exit(0);
}
else
{
printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
//pid_t ret = wait(NULL);
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret >= 0)
{
printf("wait child success!, %d\n",ret);
}
printf("Father running...");
sleep(5);
}
return 0;
}
结论:
- 由此可见,waitpid和wait没有什么区别
- 进程等待成功是否意味着子进程运行成功?
- 绝对不是,进程等待成功只意味着子进程退出了
3.5 进程子状态的获取:int* status
我们接下来看看status表示的是什么:
由此可见,status表示的不是退出码,这个数字很奇怪,那它到底表示的是什么呢?
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针
。如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为NULL
关于status参数的理解:
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
如果传递NULL,表示不关心子进程的退出状态信息
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
关于0xff与0x7f的使用理解:
0xff:通常用来取得低八位
- f二进制是:1111,即0xff为:0000 0000 1111 1111
- 这里的status>>8,取status的后8位,后八位全是0,而&操作,二进制位同真为真
- 如果(status>>8)&0xff结果为0,则表示后8位全是0,代码运行结果正确
- 如果(status>>8)&0xff结果不为0,则表示后8位不全是0,代码运行结果不正确
0x7f:通常用来取得低七位
- 7二进制是:0111,即0x7f为:0000 0000 0111 1111
- 异常终止高八位没用,所以这里不需要将status>>8
- status&0x7f,就是将status的第七位与111 1111进行&操作,从而得到终止信号
3.6 status的验证
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 0;
while(count < 5)
{
printf("I am child, pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
count++;
}
exit(77);
}
else
{
printf("I am father, pid: %d, ppid: %d\n",getpid(),getppid());
//pid_t ret = wait(NULL);
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret >= 0)
{
printf("wait child success!, %d\n",ret);
printf("status: %d\n",status);
printf("child exit code: %d\n",(status>>8)&0xFF);
}
printf("Father running...\n");
sleep(2);
}
return 0;
}
- 我们通过第9行代码获取到了子进程退出的退出码exit(77)
- 进程异常的时候,本质是进程运行的时候出现了某种错误,导致进程收到信号!
那么我们怎么知道我们收到信号了呢?我们接着看:
由此可见,我们没有收到任何信号,因为信号里没有0号信号!
由上图,我在进程运行的时候用2号信号把子进程给kill了,子进程立马终止然后传递过去了2号信号。且退出码为0(我刚才说过,如果进程出现异常,退出码没有任何意义!)
上面都是提到的单进程,接下来我就写一个多进程执行的,前面没有说waitpid的第一个参数,其实第一个参数在单进程的时候就标识的那个参数,多参数的时候就可以指定一个参数,也就是说一个waitpid只能等一个子进程:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t ids[3];
for(int i = 0; i < 3; i++)
{
pid_t id = fork();
if(id == 0)
{
int count = 3;
while(count > 0)
{
printf("child do something!: %d, %d\n",getpid(),getppid());
sleep(1);
count--;
}
exit(i);
}
//father
ids[i] = id;
}
int count = 0;
while(count < 3)
{
int status = 0;
pid_t ret = waitpid(ids[count], &status, 0);
if(ret >= 0)
{
printf("wait child success!, %d\n",ret);
printf("status: %d\n",status);
printf("child exit code: %d\n",(status>>8)&0xFF);
printf("child get signal: %d\n",status&0x7F);
}
count++;
}
return 0;
}
- 其实我们很少用到创建多进程的场景
- 在最开始我们说waitpid的时候给了两个宏,也就是说,我们可以不适用位操作,直接使用宏即可,如下代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
pid_t ids[3];
for(int i = 0; i < 3; i++)
{
pid_t id = fork();
if(id == 0)
{
int count = 3;
while(count > 0)
{
printf("child do something!: %d, %d\n",getpid(),getppid());
sleep(1);
count--;
}
exit(i);
}
//father
ids[i] = id;
}
int count = 0;
while(count < 3)
{
int status = 0;
pid_t ret = waitpid(ids[count], &status, 0);
if(ret >= 0)
{
printf("wait child success!, %d\n",ret);
if(WIFEXITED(status))//正常退出
{
printf("child exit code: %d\n",WEXITSTATUS(status));
}
else
{
//不正常退出
printf("child not exit normal!\n");
}
// printf("status: %d\n",status);
// printf("child exit code: %d\n",(status>>8)&0xFF);
// printf("child get signal: %d\n",status&0x7F);
}
count++;
}
return 0;
}
3.7 进程阻塞等待与非阻塞等待的区别
阻塞等待:父进程一直等子进程,什么也不干
非阻塞等待:就是父进程在等待的同时也在做自己的事,在子进程退出后再去读取子进程的退出信息
我们首先来剖析一下waitpid返回值的文档:
这里我解释一下waitpid()的返回值:如果成功的话,返回等待子进程的退出码,如果WNOHANG被指定的、并且指定的子进程是存在的、并且这个子进程的状态没有改变,就返回0,否则的话就返回-1
这里先说一个细节:waitpid的返回值要么大于0要么小于0这两个状态,要是设置成非阻塞就很有可能出现第三个状态,就是调用waitpid调用成功了,但是子进程并没有退出,没有退出的话调用waitpid检车的时候,就相当于,我等它的时候,他没有退出,但是我waitpid调用成功了,因为状态没有变,所以我直接返回了,就相当于我就进行了一次检测。所以如果waitpid的返回值是0的话就证明waitpid调用是成功的,只不过被等的那个子进程没有退出罢了
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 0;
while(count < 5)
{
printf("I am child, pid: %d, ppid: %d\n",getpid(), getppid());
sleep(1);
count++;
}
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if(ret > 0)
{
printf("wait success!\n");
printf("exit code: %d\n",WEXITSTATUS(status));
}
printf("ret: %d\n", ret);
return 0;
}
- 由此可见,此时waitpid的返回值立马就是0,但是子进程还在,而父进程立马就退出了,此时子进程的ppid立马改为了1,那么子进程也就变成了孤儿进程
- 所以我们可以得到一个结论,如果我们以非阻塞的方式进行等待的时候,此时我们就不应该只等待一次,而是让父进程不断的轮询式的等待
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 0;
while(count < 5)
{
printf("I am child, pid: %d, ppid: %d\n",getpid(), getppid());
sleep(1);
count++;
}
exit(1);
}
while(1)
{
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);
if(ret > 0)
{
printf("wait success!\n");
printf("exit code: %d\n",WEXITSTATUS(status));
break;
}
else if(ret == 0)
{
printf("father do other things!\n");
sleep(1);
}
else
{
printf("waitpid error!\n");
break;
}
}
return 0;
}
由此可见,在子进程在做自己的事情的时候,父进程并不是刻意的去等待,而是父进程也在做自己的事情,它们两个之间并不会相互影响,只不过每隔1秒进行一次检测,当子进程运行结束之后,父进程获取子进程相关的退出信息
这种检测方案叫做非阻塞接口的轮询检测方案
!
4.Linux进程替换
4.1 进程替换原理
- 磁盘中保存一程序的代码和数据,程序替换就是,将磁盘中保存的新程序的代码和数据替换进程中的程序和代码。从新程序代码开始执行
- 注意:
程序替换没有创建新进程
,所以该进程的pid和数据结构并没有改变
4.2 替换函数
在Linux中其实有六种以exec开头的函数,统称exec函数:
#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
所以exec函数只有出错的返回值而没有成功的返回值
//命名理解
//这些函数原型看起来很容易混,但只要掌握了规律就很好记
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
补充:execl系列的函数,根本就不需要判断返回值,因为只要是返回了就是失败!所以我们一般在程序替换后加上exit(1),也就是说只要你成功了,那你就被替换掉了,只要失败了就不往后走了,就终止进程
接下来我先来用几个,其实剩下的都是非常相似的:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("I am a process!\n");
sleep(2);
execl("/usr/bin/ls","ls", "-a", "-i", "-l", NULL);
return 0;
}
- 我们可以看到,我们利用execl成功的调用起了ls命令
- 我们得到一个结论,我的进程可以把别人的程序调用起来
我们学语言的时候肯定都听说过一句话,任何程序要被运行之前,必须要先从磁盘中加载到内存当中(因为冯诺依曼体系是这么决定的,因为磁盘属于外设),那么我们就有了以下一些疑问了?
那么程序如何被加载的呢?
- 我们刚才用的execl就可以称之为叫做Linux下的加载器所用的底层技术
当前进程在进行程序替换的时候,有没有创建新的进程?
- 没有!也就是说,我们在进行程序替换的时候,没有进行任何的程序创建。有的人可能认为不对啊!这里执行的代码和数据都已经被替换掉了,那怎么能是没创建新进程呢?
- 其实衡量一个进程是进程,并不是根据这个进程执行什么代码、访问什么数据决定的,衡量一个进程是进程是由它在内核中的相关数据结构决定的,而其中我们在进行程序替换时PCB、虚拟地址空间、页表这三种结构是没有发生质的变化的。我们只是把老的代码用新的磁盘上的文件的代码和数据进行了替换,仅此而已
- 所以这也就印证:进程不等价于程序,进程要比程序大的多
进程替换之后如果还有代码会执行么?
- 不会!因为已经被替换了,进程程序替换,一经替换,绝不返回,后续代码不会执行
如果程序替换失败呢?
- 程序替换失败后,程序后续并不会受到影响!也就是说,一旦替换失败,后面的代码正常运行
5.shell的实现原理
5.1 对shell的理解
- shell叫做命令行解释器,它的作用是将你输入的命令交给bash去执行,而bash本身不会亲自给你去实现命令,而是会创建一个子进程去帮你执行,因为进程和进程之间是由独立性的,你运行的命令如果没有BUG还好,有BUG的话,这个BUG命令如果bash亲自执行,可能会将bash搞挂了,bash挂了,就没有办法给用户提供新的命令行解释服务了,所以这种事一般由子进程去做,子进程挂掉也不影响,首先不影响父进程bash,而且运行后的结果不管对还是不对,父进程都可以拿到结果
shell的简单实现方法:
- 简单的shell,它的根本原理其中一定要有fork()这样的调用,这是其一
- 其二就是我们创建出来的子进程,我们不是为了让子进程帮我们去执行解释器部分的代码,它的任务只是执行命令,所以也就是创建子进程,让子进程去执行一个全新的程序(程序替换)
5.2 shell的实现
用下图的时间轴来表示事件的发生次序,其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束
实现流程:
- 获取命令行
- 解析命令行
- 建立一个子进程(fork)
- 替换子进程(execvp)
- 父进程等待子进程退出(wait)
实现代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<string.h>
#define LEN 1024
#define NUM 32
int main()
{
char cmd[LEN];
char* myarg[NUM];
while(1)
{
printf("[牟建波@my-centos_mc dir]$ ");
fgets(cmd, LEN, stdin);
// 我们创建出来的子进程要执行命令(命令再cmd中)
// 要执行命令就要将一个个命令拆开才可以调用
// 所以要解析字符串
//
// 将最后一个命令的\n去掉(换成\0就行了)
cmd[strlen(cmd) - 1] = '\0';
myarg[0] = strtok(cmd, " ");
int i = 1;
while(myarg[i] = strtok(NULL, " "))
{
i++;
}
pid_t id = fork();
if(id == 0)//child
{
execvp(myarg[0], myarg);
exit(-1);//随便写的
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
printf("exit code: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
对于实现shell后的一些结论:
- 我们说shell就是一个进程,现在就可以理解了,当我们 ./test 的时候 test 变成进程了,一直在运行,所以shell是在系统启动的时候就由某些任务将其进行启动
- 系统启动以及用户登录的时候,某些登陆软件会自动调用bash程序,将其运行起来变成进程