目录
1、进程创建
1.2、fork(补充)
1.2、写时拷贝(补充)
2、进程终止
3、进程等待
4、程序替换
5、自己写一个shell
1、进程创建
1.2、fork(补充)
//一次创建一批进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define N 5
void runChlid()
{
int cnt = 10;
while(cnt) //执行10秒之后自动退出
{
printf("I am child: %d, ppid:%d\n",getpid(),getppid());
sleep(1);
cnt--;
}
}
int main()
{
for(int i = 0; i < N; i++)
{
pid_t id = fork();
if(id == 0)
{
runChild();
exit(0); //子进程结束
//父进程往下走
} //父进程 再继续去循环 这样就可以一次性创建出5个子进程
}
sleep(1000);
return 0;
}
父子进程或者兄弟进程,谁先运行,完全是由调度器决定的,谁先被放到运行队列里谁就先运行,这是不能被确定的。
1.2、写时拷贝(补充)
补充一个细节问题:OS是如何知道我们此时父子进程共享数据块,此时是需要发生写时拷贝的呢?
在父进程创建了子进程时候,父进程的数据段就会由原来的读写段被暂时改为只读段,子进程也是只读。只读区不变。所以,不论是父进程还是子进程谁想写,就会触发系统层面的权限问题,但是在权限方面审核的时候,OS会识别出来,历史上这个区域是可以被写入的,只不过是暂时是只读状态。因此这时触发的权限问题,OS不会做异常异常处理,而是将异常转为 将数据拷贝一份,谁写的就将谁的页表重新映射,拷贝完之后在页表中将只读的标签去掉,就可以正常做修改了。
其实,写时拷贝本质上就是用时再用,延迟申请,按需申请!
2、进程终止
问题:为什么以前写代码main函数总是会返回return 0?1?2? 这个值给谁了?为什么要返回这个值?
进程退出场景:a、代码运行完毕,结果正确。(不关心)
b、代码运行完毕,结果不正确。(关心为什么)
c、代码异常终止
输入 echo $? //用来表示最近一个程序退出时的退出码
0 //这个0会被父进程即bash拿到,得到上一个程序的退出码
[]$ ./myproc
.........
[]$ echo $?
11 //假如11是myproc程序的退出码
[]$ echo $?
0 //打印第二遍为什么会变为0? 因为它的上一个是echo命令,echo命令执行是正确的,所以就是0
进程中,谁会关心我运行情况?一般而言是父进程要关心
结果为什么不正确?可以用return 的不同返回值数字,表征不同的出错原因,这些不同的数字就叫作退出码
总而言之,main函数的返回值本质表示:进程运行完成时是否正确的结果,如果不是,可以用不同的数字表示不同的出错原因。
退出码是数字 这些数字是给计算机看的,我们看的应该是一些错误信息。
#include<string.h>
int main()
{
for(int i = 0; i < 200; i++)
{
printf("%d: %s\n", i, strerror(i)); //打印每个数字对应的错误信息
}
return 0;
}
系统提供的错误码和错误码描述是有对应关系的。 错误码表示退出原因,错误信息展示具体详细的错误。
那么,我们可不可以自己设计一套退出码体系呢? 答案是:当然可以
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
const char *errString[] = {
"success",
"error 1",
"error 2",
"error 3",
"error 4",
"error 5",
};
int main()
{
for(int i = 0; i < 200; i++)
{
printf("%d: %s\n", i, strerror(i)); //打印每个数字对应的错误信息
}
return 0;
}
结果正确或者说不正确,统一会采用进程的退出码来进行判定!!
问题:为什么以前写代码main函数总是会返回return 0?1?2? 这个值给谁了?为什么要返回这个值? 因此,这个问题就可以解决了。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
int ret = 0;
char *p = (char*)malloc(1000*1000*1000*4);
if(p == NULL) //申请失败
{
printf("malloc error,%d: %s\n",errno,strerror(errno)); //知道错误码 错误信息
ret = 1;//并且还可以将错误码转为进程的 退出码,让父进程也知道
}
else
{
//使用申请的内容
printf("malloc success\n");
}
return ret;
}
[]$ ./myproc
malloc error, 12: Cannot allocate memory
[]$ echo $?
12 //退出码
errno 是C语言给我们提供的一个全局变量。保存最近一次执行的错误码。c语言调用库函数不成功的时候,返回值告诉我们这个函数的情况,结果对不对,至于结果不对的原因,比如当库函数调用失败了,C语言就会将我们的errno全局变量设置成对应的数字,这个数字表示我们当前调用的函数出错时的错误码。 如果连续调函数都失败了,这个errno每次都会被覆盖,它保留的时last error
如果代码出现了异常(第三种情况),退出码还有意义吗??答案是:本质可能代码没有跑完,进程的退出码无意义,我们不关心退出码。
因此,进程退出的时候,我们首先关心有没有发生异常,没出异常就是代码跑完了,再看结果是否是正确的(再看退出码);如果出异常了,比如野指针的时候就相当于你即将要访问一个虚拟地址,这个虚拟地址在页表当中并没有建立对应的映射关系,或者说建立了映射关系它的权限设置成立只读权限,这些问题都会被OS识别,OS就会向目标进程发信号。进程出现异常,本质是我们进程收到了对应的信号(父进程看有没有收到信号)!
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
printf("hello Linux\n");
exit(12); //让一个进程退出 12 这个数字是退出码
//和return在main函数里是等价的
}
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
void show()
{
printf("hello show!\n);
printf("hello show!\n);
printf("hello show!\n);
printf("hello show!\n);
printf("hello show!\n);
exit(13);
printf("end show \n");
printf("end show \n");
printf("end show \n");
int main()
{
show();
printf("hello Linux\n");
exit(12);
}
运行结果:
hello show!
hello show!
hello show!
hello show!
hello show!
退出码是13
结论:exit 在任意地方被调用,都表示调用进程直接退出
return在main函数外面,只表示当前函数返回,返回到main函数。
因此,return在其他函数中代表函数返回,在main函数中代表进程退出;而exit在任意地方都代表进程退出!
exit与_exit之间的区别
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
printf("you can see me!\n");
sleep(1);
exit(11);
//_exit(11);
}
上面的代码exit与_exit的结果是一样的,因为有\n,退出也是一样的。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
printf("you can see me!");
sleep(1);
exit(11);
}
printf没有加\n的时候数据会暂时放到缓冲区当中,所以上面的代码数据并没有立即刷出来,停了一秒,当整个程序退出的时候,系统才会将数据刷出来。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
printf("you can see me!");
sleep(1);
_exit(11);
}
没有打印出来you can see me!
_exit是系统调用接口,程序直接调用系统接口,直接在进程层面就将这个程序控制了,而exit会在应用层将你之前打开的文件..进行刷新(刷新缓冲区),然后exit再调_exit.因为先把结果刷新了(关闭所有打开的流,所有的缓存数据均被写入 )再退出,所以就可以看到数据。
printf一定是先把数据写入缓冲区中,等到合适的时候,再进行刷新。比如说遇到\n 、进程退出。
那么这个缓冲区绝对不在哪里??? 答案是:绝对不在内核里!因为如果在内核里,_exit和exit都会对这个缓存区进行刷新,因为OS不会做任何浪费空间的行为,维护缓冲区就一定要进行刷新,因此不在。它其实在用户区。
3、进程等待
是什么??
通过系统调用wait/waitpid,来对子进程状态进行检测与回收的功能!
为什么??
a、父进程不管子进程,子进程会僵尸,僵尸就会造成内存泄露。
b、僵尸状态的进程不能通过任何操作被杀死,只能等待父进程回收它
a和b是我们必须解决的
c、我们要通过进程等待获得子进程的退出情况,这样是为了知道我布置给子进程的任务,它完成的怎么样了。
c我们要么关心,要么不关心------可选的
怎么办??
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork"); //perror是c语言的系统调用,会将错误码转化成错误码描述,
//将对应的函数带上再打印出错误信息
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
exit(0); //子进程退出
}
else
{
//parent
int cnt = 10;
whlie(cnt)
{
printf("I am father, pid:%d, ppid:%d, cnt: %d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
pid_t ret = wait(NULL);
if(ret == id)//这个id就是要等待的子进程的pid
{
printf("wait success, ret: %d\n", ret);
}
sleep(5);
}
return 0;
}
上面代码的现象:前5s子进程运行,运行5s,子进程变为僵尸状态,父进程运行5s子进程此时还是僵尸进程,之后,父进程回收子进程,然后僵尸进程没有了,父进程单独运行5s
ret返回值刚好也是子进程的pid 那么,我们就看到子进程被回收了。
到目前位置,进程等待是必须的!
wait等待的是任意一个子进程,如果我们有多个子进程呢?
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10
void RunChild()
{
int cnt = 5;
while(1) //子进程执行5秒
{
printf("I am Child Process, pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
cnt--;
}
}
int main()
{
for(int i = 0; i < N; i++) //这个for循环因为执行流的原因,父进程会通过for循环创建一批子进程
{
pid_t id = fork(); //使用for循环创建出一批进程
if(id == 0)
{
RunChild();
exit(0); //子进程退出
}
printf("create child process: %d success\n",id);//这句话只有父进程会执行
//这个id值是父进程创建出的子进程的pid
}
sleep(10);
//子进程执行5秒,父进程10秒,现在就要让父进程对子进程进行等待
for(int i = 0; i < N; i++)
{
pid_t id = wait(NULL); //因为wait每次只等待一个进程退出,所以等待一批进程就要用for循环
if(id > 0)
{
printf("wait %d success\n",id);
}
}
sleep(5);
}
如果子进程不退出,父进程默认在wait的时候,调用这个系统调用的时候不返回,这就叫阻塞状态。进程等待是不是只会等待硬件资源呢?并不是!现在这个进程就是在等待软件。
pid_ t waitpid(pid_t pid, int *status, int options);返回值,等待一个进程成功了,返回值就是你所等待的那个进程的pida、 pid = -1,等待任意一个进程,这样写的话效果与wait一样pid > 0, 等待进程id和pid相等的<1> status是输出型参数:就是能通过函数的参数把值带出来的参数,我给函数传参不是为了给函数什么,而是为了让函数交给我们什么。<2> int是被当作及部分来使用的 一个int占32位
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork"); //perror是c语言的系统调用,会将错误码转化成错误码描述,
//将对应的函数带上再打印出错误信息
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
exit(1); //子进程退出 退出码是1
}
else
{
//parent
int cnt = 10;
whlie(cnt)
{
printf("I am father, pid:%d, ppid:%d, cnt: %d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
// pid_t ret = wait(NULL);
int status = 0;
pid_t ret = waitpid(id, &status, 0);//如果关心子进程的退出状态,
//就给status这个参数,就可以得到进程的退出信息
if(ret == id)//这个id就是要等待的子进程的pid
{
printf("wait success, ret: %d\n", ret);
}
sleep(5);
}
return 0;
}
运行结果:.........
wait success, ret: 26472(子进程的pid), status:256
为什么退出信息是256呢?我们设置的退出码不是1嘛????
0000 0000 0000 0000 <0000 0001> 0000 0000
次低8位是退出码
子进程的退出场景有几种??
三种。代码运行完毕,结果正确
代码运行完毕结果不正确
代码异常终止
父进程等待,期望获得子进程退出的哪些信息呢??
a、子进程代码是否异常?
b、没有异常,结果对吗? 结果对不对,可以有退出码来判断。不对是因为什么?
1 2 3 4 ..不同的退出码表示不同的出错原因!
低8位表示子进程运行的时候是否出异常
检测进程在运行的时候低7位是否为0,如果是0,就代表我们没有收到信号,我们的代码就没有异常。这一步就可以判断我们的代码是否跑完。 再通过次低8位,获得进程的退出状态,即进程的退出码。
还有一个问题,我们不能通过设置一个全局变量,在子进程退出的时候,只需要让子进程将这个变量进行修改,我们父进程就可以拿到子进程的状态,这样做可以吗?
答案是:不能,因为进程具有独立性,子进程将数据进行修改的时候,就会发生写时拷贝,子进程对数据修改,父进程是拿不到修改之后的数据的。因此,只能调用系统调用函数来获得子进程的退出状态。
当子进程执行完毕之后,子进程的退出信息都会被保存在子进程的PCB里。调用pid_t waitpid(pid_t id,int* status,int XXX) 就是读取子进程的task_struct内核数据结构对象。因此,这些行为只能由OS来完成。
pid_ t waitpid(pid_t pid, int *status, int options);WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) 正常终止那么状态为真。 检验status变量的信号位, 如果为0(0代表没有收到任何信号),那么就是1;如果检测到信号不为0,那么就不为1WEXITSTATUS(status): 若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork"); //perror是c语言的系统调用,会将错误码转化成错误码描述,
//将对应的函数带上再打印出错误信息
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
exit(1); //子进程退出 退出码是1
}
else
{
//parent
int cnt = 10;
whlie(cnt)
{
printf("I am father, pid:%d, ppid:%d, cnt: %d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
// pid_t ret = wait(NULL);
int status = 0;
pid_t ret = waitpid(id, &status, 0);//如果关心子进程的退出状态,
//就给status这个参数,就可以得到进程的退出信息
if(ret == id)//这个id就是要等待的子进程的pid
{
if(WIFEXITED(status))//WIFEXITED判断子进程是否异常,即是否由信号发出,为0代表没有信号发出,WIFEXITED(status)就为真
{
printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
//WEXITSTATUS提取子进程的退出码
}
else
{
printf("进程出异常了\n"); //是子进程出异常了
}
printf("wait success, ret: %d\n", ret);
}
else{
printf("wait failed!\n");//这是waipid函数调用失败了
}
sleep(5);
}
return 0;
}
多个进程:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10
void RunChild()
{
int cnt = 5;
while(cnt) //子进程执行5秒
{
printf("I am Child Process, pid: %d, ppid: %d\n",getpid(),getppid());
sleep(1);
cnt--;
}
}
int main()
{
for(int i = 0; i < N; i++) //这个for循环因为执行流的原因,父进程会通过for循环创建一批子进程
{
pid_t id = fork(); //使用for循环创建出一批进程
if(id == 0)
{
RunChild();
exit(i); //子进程退出
}
printf("create child process: %d success\n",id);//这句话只有父进程会执行
//这个id值是父进程创建出的子进程的pid
}
sleep(10);
//子进程执行5秒,父进程10秒,现在就要让父进程对子进程进行等待
for(int i = 0; i < N; i++)
{
//pid_t id = wait(NULL); //因为wait每次只等待一个进程退出,所以等待一批进程就要用for循环
int status = 0;
pid_t id = waitpid(-1, &status, 0);//-1 代表等待任意一个进程
if(id > 0)//等待子进程成功
{
printf("wait %d success,exit code: %d\n", id, WEXITSTATUS(status));
//WEXITSTATUS(status)获得进程退出码
}
}
sleep(5);
}
pid_ t waitpid(pid_t pid, int *status, int options);options:WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID 。
等待进程的两种方式:当进程不退出时,一直阻塞等待;非阻塞轮询
非阻塞轮询:非阻塞+循环 非阻塞轮询+做自己的事情
ret_pid > 0 等待成功
ret_pid = 0 等待的条件还没有就绪 这种情况会就绪等待,直到大于0或小于0
ret_pid < 0 等待不成功
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define TASK_NUM 10
typedef void(*task_t)();
task_t task[TASK_NUM];
//任务
void task1()
{
printf("这是一个执行打印日志的任务");
}
void task2()
{
printf("这是一个检测网络健康状态的任务");
}
void task3()
{
printf("这是一个绘制图形界面的任务");
}
//任务的管理代码
void InitTask()
{
for(int i = 0;i < TASK_NUM; i++) tasks[i] = NULL;
}
int AddTask(task_t t)
{
int pos = 0;
for(;pos < TASK_NUM;pos++)
{
if(!task[pos]) break;
}
if(pos == TASK_NUM) return -1;
task[pos] = t;
return 0;
}
void DelTask()
{}
void CheckTask()
{}
void UpdateTask()
{}
void ExecuteTask()
{
for(int i = 0; i < TASK_NUM; i++)
{
if(!tasks[i] continue;
tasks[i]();
}
}
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork"); //perror是c语言的系统调用,会将错误码转化成错误码描述,
//将对应的函数带上再打印出错误信息
return 1;
}
else if(id == 0)
{
//child
int cnt = 5;
while(cnt)
{
printf("I am child, pid:%d, ppid:%d, cnt: %d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
exit(1); //子进程退出 退出码是1
}
else
{
//parent
int cnt = 10;
whlie(cnt)
{
printf("I am father, pid:%d, ppid:%d, cnt: %d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
// pid_t ret = wait(NULL);
int status = 0;
//pid_t ret = waitpid(id, &status, 0);//如果关心子进程的退出状态,
//就给status这个参数,就可以得到进程的退出信息
InitTask();
AddTask(task1);
AddTask(task2);
AddTask(task3);
while(1) //轮询
{
pid_t ret = waitpid(id, &status, WNOHANG); //非阻塞
if(ret > 0) //说明等待成功了
{
if(WIFEXITED(status))//WIFEXITED判断子进程是否异常,即是否由信号发出,为0代表没有信号发出,WIFEXITED(status)就为真
{
printf("进程是正常跑完的,退出码:%d\n",WEXITSTATUS(status));
//WEXITSTATUS提取子进程的退出码
}
else
{
printf("进程出异常了\n"); //是子进程出异常了
}
break;
}
else if(ret < 0)
{
printf("wait failed!\n");//这是waipid函数调用失败了
break;
}
else
{
//ret == 0
//printf("子进程还没有退出,我再等等...\n");
ExecuteTask();
}
}
sleep(5);
}
return 0;
}
4、程序替换
先看一个现象(单进程版):
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
//这类方法的标准写法
execl("usr/bin/ls", "ls","-a","-l",NULL);//执行一个指定的文件
printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
return 0;
}
我们自己写的代码执行了 ls 只打印了before没有 after,上面这个现象就是程序替换
程序替换的基本原理:
ls这个命令的程序和代码在磁盘上,当在调用程序替换函数的时候,就会用新程序的代码替换旧程序的代码,新程序的数据替换旧程序的数据。替换完之后,只需要将页表的右侧再重新调整一下,就好了,pcb,进程地址空间以及页表的左侧不变。替换完之后,让cpu执行当前新的入口地址,来执行新程序,我们只是将当前进程的代码和数据进行了替换而并没有创建新进程,这就叫程序替换。
多进程版:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{ //child
printf("before: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(5);
//这类方法的标准写法
execl("usr/bin/ls", "ls","-a","-l",NULL);//执行一个指定的文件
printf("after: I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
exit(0);
}
//father
pid_t ret = waitpid(id, NULL, 0);
if(ret > 0)
printf("wait success,father pid: %d, ret id: %d\n",getpid(), ret);
sleep(5);
return 0;
}
上面代码实验现象:创建子进程,子进程去执行程序替换,执行ls命令,在子进程执行完毕之后,父进程对子进程进行等待回收。
那么,这个子进程发生进程替换为什么不会影响父进程呢?因为写时拷贝和进程具有独立性。那替换数据的时候可以进行写时拷贝,那么替换代码的时候也可以是发生写时拷贝吗?
答案是:是的!代码也可以实现写时拷贝! 当我们要想自己对代码区进行写入时,OS会对我们的行为进行拦截,但是现在我们调用的是系统函数,对代码区进行写时拷贝的工作是由OS来完成的,因此,是可以的。
程序替换有没有创建新进程?
答案是:没有!只进行程序和代码的替换工作。 因此,进程的内核数据结构就没有被释放和重新建立,只需要改某些字段就欧克了。
是父子关系发生写时拷贝,不是父子关系,直接替换。
为什么after没有被打印? 因为after属于老程序的代码,这个代码在execl之后,老代码早已被替换,因此不会执行。程序替换失败了?才可能执行后续的代码。 那么,exec系列的函数只有失败返回值,没有成功返回值!!
替换函数: 有六种exec系列的函数
#include <unistd.h>`int exec l (const char *path, const char *arg, ...);前面都是exec, l:list ,后面带l的就是参数一个一个传,并且是可变参数。第一个参数是我如何找到该程序,并且所有的exec函数都一样。从第二个参数开始,命令行怎么写,这个参数就怎么传,后面的参数代表找到程序之后如何执行这个程序,主要是要不要涵盖选项,涵盖哪些。int exec lp (const char *file, const char *arg, ...);p是PATH,第一个参数不用给路径,只需要给出文件就可以。后面的参数还是一样。eg:execlp("ls","ls","-a","-l",NULL); 第一个ls是找到执行的程序,后面是选项int exec le (const char *path, const char *arg, ...,char *const envp[]);e:env(环境变量)int exec v (const char *path, char *const argv[]); 字符串指针数组v:vector 第一个参数是找到可执行程序 第二个是 将我们的选项放到指针数组里eg:char *const myargv[] = {"ls","-l","-a",NULL};execv("user/bin/ls",myargv);ls是已经编译好的程序,并且它肯定也有main函数,有main函数肯定就有命令函参数,那么它的命令函参数是谁提供的?是execv系统调用由myargv传入的,execv是系统函数,系统获取了命令行参数然后传给ls的main函数就好了在命令行当中所有的进程都是bash的子进程,因此,所有的进程在启动的时候,都是采用exec系列的函数进行启动执行的。所以,exec系列的函数承担的是一个加载器的效果!! 其实,execv就是代码层面的加载器。可以将可执行程序导到内存里,也可以获得命令行参数,执行ls调用main函数时,就是将myargv参数传进去的。int exec vp (const char *file, char *const argv[]);p就是只给文件名 第二个参数和上面相同
环境变量是什么时候给进程的?环境变量也是数据,创建子进程的时候,环境变量就已经被子进程继承下去了! 因此,程序替换中环境变量信息不会被替换!
5、自己写一个shell
shell:是OS外的一层外壳程序,负责帮用户进行指令的执行,负责将指令交给OS,OS执行完之后通过shell将结果交给用户,因此它叫作外壳程序。shell/bash也是一个进程,指令执行的时候,本质就是自己创建子进程去执行的!
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " "
#define LINE_SIZE 1024
#define ARGC_SIZE 32
#define EXIT_CODE 44
int lastcode = 0;
int quit = 0;
char commandline[LINE_SIZE];
const char *getusername()
{
return getenv("USER");
}
const char *gethostname()
{
return getenv("HOSTNAME");
}
const char *getpwd()
{
return getenv("PWD");
}
void interact(char *cline, int size)
{
printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getusername(),gethostname(),getpwd());
//scanf("%s",commandline); //scanf是以空格结束的 输入的时候以空格作为分隔符,只会读入一个,因此不能用scanf
char *s = fgets(cline, size, stdin);
assert(s);
(void)s;//会面不会用s 避免警告提示
//"abcd\n\0" 输入字符串之后会输入回车 那么我们需要将\n置0 它的下标是strlen(commandline)-1
cline[strlen(cline)-1] = '\0';
int spliststring(char cline[], char *_argv[])//输出型参数
{
int i = 0;
argv[i++] = strtok(cline, DELIM);
while(_argv[i++] = strtok(NULL,DELIM)); //strtok一次切割一个字符串 所以需要循环调用
return i - 1; //i 是命令行参数个数 因为最后是NULL,所以需要-1
}
void NormalExcute(char *_argv[])
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
else if(id == 0)
{
//让子进程执行命令
execvpe(_argv[0],_argv,environ);
exit(EXIT_CODE);//程序替换有返回值必定出错 如果出错了就以这样的方式退出
}
else
{
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id) //等待成功
{
lastcode = WEXITSTATUS(status);
}
}
}
int bulidCommand(argv,argc)
{
if(argc == 2 && strcmp(argv[0],"cd") == 0)
{
chdir(argv[1]); //更改当前程序的路径
getpwd();
sprintf(getenv("PWD"),"PWD=%s",argv[1]);//格式化到环境变量里
return 1;
}
return 0;
}
int main()
{
extern char **environ;
char commandline[LINE_SIZE];
char *argv[ARGC_SIZE];
while(!quit) //shell本质上是一个死循环
{
//2、交互问题,获取命令行
interact(commandline ,sizeof(commandline));
//printf("echo : %s\n", commandline);
//commandline -> "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"将一个大字符串切割成四个小字符串,那么小字符串的地址保存在哪里???
//3、子串分割问题,解析命令行
int argc = spliststring(commandline, argv);
if(argc == 0) countinue; //切割之后发现是空串 继续输入
//4、指令的判断 如果是内键命令就让程序自己执行,不需要子进程执行
//内键命令本质就是shell内部的一个函数
int n = bulidCommand(argv,argc);
//debug
//for(int i = 0;argv[i]; i++)printf("[%d]: %s\n", i, argv[i]);
//5、普通命令的执行
if(!n) NormalExcute(argv);//传入刚刚解析的argv(也就是需要执行的命令)
}
return 0;
}
1. 获取命令行2. 解析命令行3. 建立一个子进程( fork )4. 替换子进程( execvp )5. 父进程等待子进程退出( wait )