目录
一、进程创建
1、pid_t fork(void)
2、写时拷贝技术(父子进程间代码共享、数据独有)
3、vfork()--创建一个子进程
4、fork创建子进程流程是什么样的?
5、一个关于fork的程序
6、程序a
7、 程序b
二、进程终止
1、在main函数中return 仅在main函数中使用时 退出程序运行
2、库函数 void exit(int retval) 在任意位置调用都会退出程序
3、系统调用接口 void _exit(int retval); 在任意位置调用都会退出程序
4、进程退出返回值的意义
5、进程的三种退出场景
6、void perror(const char* msg); 出错打印错误信息
7、const char* strerror(int errno);获取对应编号错误信息
三、进程等待
1、为什么要进行进程等待?
2、阻塞接口与非阻塞接口
3、pid_t wait(int *status)
4、pid_t waitpid(pid_t pid, int *status, int options);
5、使用wait与waitpid接口模拟阻塞等待
①、利用waitpid接口接收退出的pid为child_pid的子进程
②、假如随便等待一个pid为12345的进程
③、使用waitpid等待任意一个子进程退出
6、使用waitpid模拟非阻塞进程等待
7、分清status与wait接口返回值
① 概念认知
② 代码举例
③ 随机取一个退出码,将上方exit(0) 替换为exit(99)
④ status的内层结构剖析
⑥ 使用257作为退出码(超出范围截断)
⑦ 获取statuts中的退出码与异常信号值
四、程序替换
1、概念理解与简单应用
2、execve接口学习
3、int execv(char* path, char* argv[])
4、execvp函数学习
5、不定参数替换函数
关于LInux的第二篇博文就提到了fork()这个接口,这个接口的作用就是通过复制父进程来创建一个子进程,可以利用pit_t 的返回值来使用if判断来实现父子进程分流,返回值保存了子进程的pid
pit_t ret = fork();创建一个子进程
ret == -1 创建失败
ret == 0 子进程
ret > 0 父进程
再来剖析一下这段代码
1 #include
2 #include
3
4 int g_val = 10;
5 int main()
6 {
7 int ret = fork();
8 if(ret<0){
9 printf("error fork\n");
10 }else if(ret==0){
11 g_val = 300;
12 printf("i am child, my g_val = %d,my &g_val = %p\n",g_val,&g_val);
13 }else{
14 printf("i am parent, my g_val = %d,my &g_val = %p\n",g_val, &g_val);
15 }
16 return 0;
17 }
当子进程中没有对全局变量进行修改时,二者打印的数据以及地址都是一样的
上图为定义一个全局变量g_val 父进程通过虚拟地址空间存放g_val,然后通过页表将虚拟地址映射到物理内存当中去,并在屋里内存中给变量g_val开辟一块空间存放值为10;
接着父进程调用fork()接口,创建了一个子进程,这个子进程的信息几乎和父进程一样,它复制了父进程的上下文数据、内存指针,以及虚拟地址空间存放g_val。形成了下方这个闲适恬淡的氛围
可是这时候子进程开始了分裂割据,子进程希望拥有属于自己的g_val于是对g_val的值进行了修改
操作系统知道了它这个请求之后于是在物理内存中重新给它开辟了一块空间,这块空间中存放了子进程的g_val=300,于是子进程的虚拟地址空间在经过页表进行映射的时候就会指向新开辟的空间
子进程复制了父进程中大部分的信息,因此子进程有自己的变量,但是自己的变量经过页表映射后与父进程访问的是同一块物理内存,当这块内存空间中的数据将要修改,则给子进程重新开辟空间,并拷贝数据过去
每个进程都应该有它们自己的存储空间,这样才能互不影响(进程之间的独立性)
那么为什么不直接给子进程开辟一个空间呢?而是要通过这样当修改的时候才进行开辟?就像寒假作业不写,等老师开学了跟我要,我才开始写这个作业。
万一出现了给子进程开辟空间了,最后发现没有对子进程进行修改,那岂不是做无用功了,所以操作系统索性直接不给它开辟,等到它用的时候才进行开辟。
写时拷贝技术意义:主要是为了提升子进程的创建效率,避免不必要的内存消耗
那么如果是给父进程变量修改内容呢?
还是给子进程重新开辟空间,因为原本的这块空间就是人家父进程的
例子:malloc动态申请一块空间——其实只是先分配了一个虚拟地址(物理内存并没有直接被开辟),当第一次要修改空间数据时才会分配。
(在fork实现了写时拷贝技术之后就少用了)
创建一个子进程、父子进程共用同一块虚拟地址
fork与vfork的区别:
fork创建进程之后,父子进程谁先运行不一定,看系统调度
但是vfork创建子进程,一定是子进程先运行,只有子进程退出或者程序替换之后父进程才继续运行
fork 父子进程代码共享数据独有
vfork 父子进程共用同一块虚拟地址
不刷新缓冲区,所以当创建第二个进程的时候,第一个子进程的*也被保留下来
子进程复制父进程,复制了整个地址空间,所以地址空间中没有被刷新的数据也就保存了一份。
对于程序b有个知识点:程序在return 0 之前进行刷新缓冲区
加入了_exit(0) 退出程序并且不刷新缓冲区,结果就没有,说明程序在return的时候就会进行刷新缓冲区
如何终止一个进程
return是终止一个函数,并返回一个数据;
main函数是程序的入口函数,入口函数一旦退出,程序运行就会终止
#include
系统调用时操作系统向上层提供的用于访问内核的接口,功能结构都比较单一
大佬们针对典型场景,对系统调用接口进行封装,封装除了适用于典型场景的库函数
#include
exit与_exit的区别就在于_exit不会刷新缓冲区,即不会将缓冲区中的数据进行刷新写入文件中。只有刷新了缓冲区,printf里面的数据才会被打印出来。
return以及exit给出的数据其实就是进程的退出码
(必须得有一种方式告诉我们这个任务完成的怎么样)
作用:进程的退出码就是表示当前进程任务处理的结果
任务完美完成,正常退出;
任务没有完成,正常退出;
异常退出
打印上一步系统调用接口使用失败的原因信息
在出错处理中是非常有用的,因为只有知道了为什么出错,才能知道如何改进。
5 int main() 6 { 7 printf("hello\n"); 8 FILE *fp = fopen("tst.txt", "r"); 9 if(fp == NULL) 10 { 11 perror("fopen error"); 12 return -1; 13 } 14 return 0; 15 }
根据错误编号,返回对应编号的字符串错误原因
先来回忆一下什么是僵尸进程,僵尸进程就是子进程先于父进程退出,父进程没有获取子进程退出的返回值,导致子进程无法完全释放资源。而解决僵尸进程的方式一个就是直接退出父进程(kill或者exit(int retval))另外一个就是进行进程等待。
进程等待作用:父进程在创建子进程之后,等待子进程退出,获取子进程的退出码,释放子进程的资源,避免出现僵尸进程
阻塞接口:为了完成一个功能发起一个系统调用,但是这个调用完成条件不具备,则接口一直等待不返回,直到调用条件达成,才进行返回。(占用资源,但是具有及时性)
非阻塞接口:当调用条件不具备的时候,立即报错返回(资源利用率高,不及时)
wait是一个阻塞接口,功能是等待当前调用者的任意一个子进程退出(如果已经有退出的直接处理),获取返回值,释放资源
status 参数是一个int空间的地址,用于向指定空间中存放子进程的退出返回码
返回值:成功返回处理的退出子进程的pid; 失败返回-1;
1 #include
2 #include //fork接口 3 #include // wait/waitpid接口 4 #include // exit() 5 6 int main() 7 { 8 int ret = fork(); 9 if(ret<0){ 10 perror("fork error"); 11 return -1; 12 }else if(ret == 0){ 13 exit(0); // 子进程直接退出了 14 } 15 int status; 16 ret = wait(&status);//已经有子进程退出,则直接处理,子进程没有退出则会一直等待(阻塞) 17 if(ret!=-1){ 18 printf("%d子进程退出了, 退出返回值为%d\n",ret,status); 19 } 20 return 0; 21 }
在wait接收之前先进行一个sleep看一看进程信息
15 sleep(6); 16 int status; 17 ret = wait(&status); 18 if(ret!=-1){ 19 printf("%d子进程退出了, 退出返回值为%d\n",ret,status); 20 } 21 sleep(1000000);
僵尸子进程33203退出了
waitpid接口既可以等待任意一个子进程退出,也可以等待指定的子进程退出
pid参数: >0 则表示等待指定pid的子进程退出; -1表示等待任意一个子进程退出
waitpid接口既可以表示阻塞等待,也可以使用非阻塞等待
options参数: 0 表示默认阻塞等待
WNOHANG-设置为非阻塞(当前没有子进程退出则会立即报错返回)
返回值:成功则返回处理的退出子进程的pid, 若没有子进程退出则返回0;出错返回-1;
①、利用waitpid接口接收退出的pid为child_pid的子进程
1 #include
2 #include //fork接口 3 #include // wait/waitpid接口 4 #include // exit() 5 6 int main() 7 { 8 pid_t child_pid = fork(); 12 if(child_pid == 0){ 13 exit(0); // 子进程直接退出 14 } 15 sleep(6); 16 int status; 17 //ret = wait(&status); // wait接口 18 int ret = waitpid(child_pid, &status, 0); 19 if(ret<0){ 20 perror("waitpid error"); 21 return -1; 22 } 23 printf("%d子进程退出了,退出返回值为%d\n",ret, status); 24 return 0; 25 }
②、假如随便等待一个pid为12345的进程
无法找到pid为12345的进程,出错返回-1,并使用perror打印出错信息
18 int ret = waitpid(12345, &status, 0); 19 if(ret<0){ 20 perror("waitpid error"); 21 return -1; 22 } 23 printf("%d子进程退出了,退出返回值为%d\n",ret, status);
③、使用waitpid等待任意一个子进程退出
代码 waitpit(-1, &status, 0) 与 wait(&status) 等价,均表示等待任意一个子进程退出
18 int ret = waitpid(-1, &status, 0); 19 if(ret<0){ 20 perror("waitpid error"); 21 return -1; 22 }
6 int main() 7 { 8 pid_t child_ret = fork(); 12 if(child_ret == 0){ 13 sleep(6);// 6秒之后子进程退出 14 exit(0); 15 } 16 int ret, status; //第三个参数使用WNOHANG 17 while((ret = waitpid(-1, &status, WNOHANG)) == 0) 18 { 19 printf("现在waitpid的返回值为0,所以目前没有子进程退出,2秒之后再次检测\n"); 20 sleep(2);// 休眠2秒之后再次进行结果判断 21 } 22 if(ret<0){ 23 perror("waitpid error"); 24 } 25 printf("%d子进程退出了,退出返回值为%d\n",ret, status); 26 return 0; 27 }
注意需要while循环的使用,对于waitpid接口返回值的循环检测
如果while循环内部选择每次休眠5秒之后再进行检测,子进程在第6秒就退出了,而检测却在第10秒才检测出来,这个就体现了非阻塞等待的不及时性。
wait接口的返回值,当进程等待成功了返回处理的退出子进程pid,为一个大于0的值。
status是一个int类型的空间,用来存放子进程的退出码
下方这个代码中,子进程使用exit(0)接口退出,exit(int retval);中retval就是退出码,然后这个retval也就存储在这个status中,打印出来也是0.
6 int main()
7 {
8 pid_t child_pid = fork();
9 if(child_pid == 0){
10 sleep(2);
11 exit(0);// 使用exit接口退出子进程,退出码为
12 }
13 int status;
14 int ret = wait(&status);
15 if(ret != -1){
16 printf("%d子进程退出了,返回码为%d\n", ret, status);
17 }
18 return 0;
19 }
这时我们发现,返回码竟然是这样一个数25344????打开计算器输入99与25344发现他们的16进制都有一个63?? 25344--> 0x6300, 99--> 0x0063
status是一个int类型的数据,一共有四个字节32个比特位,而其高16位我们不关心。
而其中低16位中的高8位是用来保存进程退出码,使用一个字节进行保存,如果超过这1个字节的数据就会进行截断。(0~255)
异常信号值是用低7位来存储的进程时异常退出原因,0表示正常退出,非0表示异常退出。
有了上面的知识,那么99理解过来也就是,首先程序正常退出,所以6300后俩位为0,然后99转化为63存储在这个保存进程退出码的这一个字节中。
⑥ 使用257作为退出码(超出范围截断)
exit(257);// 使用exit接口退出子进程,退出码为257
使用257作为退出码,最后返回的是256?
上面说了存储退出码的只有一个字节空间,超出的就会截断,那么这一个字节中只有257最后那个1了,转为16进制就是1,然后在加上status中的后8位0就是 0x100 -->256就是上面编译器中出现的256.
status中低7位为异常信号值 可以通过 与运算 来获取这低七位 (status & 0x7f)
上面说过异常信号值低16位中的高八位为进程退出码(status>>8)& 0xff
int ret = wait(&status);
if((status&0x7f) == 0)
{
printf("正常退出,%d子进程退出了,退出返回值为%d\n",ret, (status>>8)&0xff);
}
替换掉一个pcb所描述的要调度管理的程序,替换成另外一个程序
简单理解:将一个程序加载到内存中,然后修改当前pcb进程的页表映射信息,刷新虚拟地址空间,让其管理新程序。
1 #include
2 #include
3
4 int main()
5 {
6
7 printf("这是我代码的开始\n");
8 execlp("ls","ls","-1",NULL);
9 printf("这是我代码的结束\n");
10 return 0;
11 }
发现并没有打印我的下一个printf内容,只是执行了ls -l的命令。这就是程序替换
execlp命令使原来虚拟内存映射到原先代码的物理内存转移到映射ls命令的代码中去了。
int execve(char* path, char* argv[], char* env[]);
功能:让path这个路径的程序加载到内存中,然后让系统调度运行这个程序。
而程序运行有可能会有运行参数与环境变量
argv用于设定运行参数 env用来设定环境变量
失败:返回-1 替换成功没有返回值(因为替换成功就去执行新程序了)
下面这个代码A就是之前关于main函数的参数的代码,打印结果横线上层为运行参数,下方为环境变量。
int main(int argc, char* argv[], char* env[])
{
for(int i = 0; argv[i]!=NULL; i++)
{
printf("argv[%d]=[%s]\n",i,argv[i]);
}
printf("-----------------------------------------------------------------\n");
for(int i = 0; env[i]!=NULL; i++)
{
printf("env[%d]=[%s]\n",i, env[i]);
}
return 0;
}
这俩段代码在同一个文件下,所以path就直接给出了./argc以达到使用argc来替换当前程序,并给出argv与env参数
#include
#include
#include
int main()
{
printf("这是我程序的开始\n");
//execve(char* path, char* argv[], char* env[]);
char* argv[] = {"hello","-o","-p",NULL};
char* env[] = {"myval=1000","open=100","word=999",NULL};
execve("./argc", argv, env);
printf("这是我程序的结尾\n");
return 0;
}
少了一个参数环境变量参数,默认使用PATH环境变量中找。
int main(int argc, char* argv[], char* env[])
{
printf("begin\n");
char* argv1[] = {"ls","-l",NULL};
execv("/bin/ls", argv1);
printf("end\n");
return 0;
}
是基于execve系统调用接口封装来的一种库函数 ,它的第一个参数不需要指定路径,默认会在PATH环境变量下找
它更方便于进行使用系统指定命令进行程序替换,该命令只需要file文件名和argv运行参数 这俩个参数,它会自动查询当前环境变量中的系统库命令路径下的命令进行程序替换
使用execvp库函数(头文件unistd.h)调用ls程序替换当前程序
execvp("ls", argvv); ==》 execve("/bin/ls", argv, env-->main函数第三个参数)
这三个是等价的均打印下面文件信息内容
char* argv1[] = {"ls","-l",NULL};
execve("/bin/ls", argv1, env); 默认手动设置 系统调用接口
execv("/bin/ls", argv1); 少了一个环境变量 基于上方接口封装出来的库函数
execvp("ls",argv1); 不需要指定路径 进一步封装
int main(int argc, char* argv[], char* env[])
{
printf("这是我程序的开始\n");
char* argv1[] = {"ls","-l",NULL};
execve("/bin/ls", argv1, env);
execv("/bin/ls", argv1);
execvp("ls",argv);
printf("这是我程序的结束");
return 0;
}
int execl("path", "ls","-l");
execl("/bin/ls", "ls", "-l", NULL);
int execlp("file", "ls", "-l");
execlp("ls", "ls", "-l", NULL);
int execle("path", "ls", "-l", env)
后面有p就说明不需要指定路径与环境变量,会默认在PATH里面找,直接使用命名名即可
后面有e就表示需要手动设置环境变量