本文重点:
1.进程创建:
2.进程终止;
3.进程等待;
4.程序替换;
一.进程创建:
1.fork()函数:
在linux中fork()函数非常重要,它从已存在进程中创建一个新进程;新进程为子进程,而原进程为父进程;
(1)fork()函数的表示:
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1;
(2)调用过程:
进程调用fork,当控制转移到内核中的fork代码后,内核做:分配新的内存块和内核数据结构给子进程将父进程部分数据结构内容拷贝至子进程 添加子进程到系统进程列表当中 fork返回,开始调度器调度;
当一个进程调用fork之后,就有两个二进制代码相同的进程;而且它们都运行到相同的地方;但每个进程都将可以 开始它们自己的旅程;
如下代码所示:
#include <unistd.h>
#include <stdio.h>
int g_val=10;
int main(){
printf("hello world------ pid:%d\n",getpid());
pid_t pid=fork();
if(pid<0){
printf("fork error\n");
return 1;
}
else if(pid==0){
g_val=20;
printf("------i am child------%d g_val:%d--------%p\n",getpid(),g_val,&g_val);
}
else {
sleep(3);
printf("-----i am parent-----%d g_val:%d--------%p\n",getpid(),g_val,&g_val);
}
while(1){
sleep(1);
}
return 0;
}
在没有进行fork()创建子进程之前运行的是父进程,打印的是父进程的id,当创建子进程后打印子进程的id;因此可以得出结论:fork之前父进程独立执行,fork之后,父子两个执行流分别执行;
注意,fork之后,谁先执行完全由调度器决定;
(3)创建子进程时有哪些流程:
写时拷贝技术:操作系统通过复制父进程创建子进程,子进程初始时与父进程同用一块相同的内存区域,当子进程的数据发生改变时,会为子进程重新开辟内存更新页表;
(4)fork()函数用法:
(1)一个父进程希望复制自己,使父子进程同时执行不同的代码段;例如,父进程等待客户端请求,生成子 进程来处理请求;
(2)一个进程要执行一个不同的程序;例如子进程从fork返回后,调用exec函数;
2.vfork()函数:
vfork()创建一个子进程并阻塞父进程;
(1)接口:
#include <unistd.h>
pid_t vfork(void);
代码演示:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(){
int pid=vfork();
if(pid==0){
printf("i am child!!\n");
sleep(3);
exit(0);
}
else{
printf("i am parent\n");
}
return 0;
}
总结:
1.为什么子进程不退出,父进程不运行????
因为vfork创建出来的子进程和父进程用的是同一块虚拟地址空间;(vfork创建的子进程有自己的PCB,但没有自己的虚拟地址空间,和父进程同用一块虚拟地址空间)
2.实现原理:
子进程先运行,父进程等到子进程exit退出或者程序替换之后,父进程才会运行,否则同时运行因为父子进程共用虚拟地址空间,因此会造成调用栈混乱。因此阻塞父进程;
vfork子进程如果return退出,释放资源会导致父进程陷入混乱或错误;
3.fork, vfork ,clone他们三个函数之间的关系???
fork和vfork函数内部调用的都是clone函数来创建的进程;在clone之前会通过不同的判断做不同的操作,fork创建子进程会为子进程创建它自己的独立的虚拟地址空间;而vfork他创建一个子进程的PCB之后指向的是同一个结构体,相当于用的是同一个虚拟地址空间;
二.进程退出:
1.进程退出场景:
(1)正常退出:
代码运行完毕,结果符合预期正常退出;
代码运行完毕,结果不符合预期正常退出;
(2)异常退出:
代码异常终止;
2.进程常见退出的方法:
(1)main函数中的return:程序退出时通过/n来刷新缓冲区;
(2)exit()函数退出:void exit(int status);这是一个库函数,程序退出并刷新缓冲区;
(3)_exit()函数退出:void _exit(int status); 这是一个系统调用接口;不会刷新缓冲区,缓冲区的数据被丢弃;
#include <unistd.h>
#include <stdio.h>
#include <stdlib,h>
int main(){
printf("nihao~");
sleep(3);
return 0;
//exit(0);
//_exit(0);
}
按照以上程序的三种顺序来进行输出得到结果如下图如所示;
三.进程等待:
进程等待的实质是:等待子进程的退出(子进程状态的改变),获取子进程的退出返回值;
1.为什么要进行进程等待???
因为子进程退出时为了保存退出原因,因此操作系统不能释放子进程的全部资源,通知父进程获取子进程的退出返回值,允许释放资源;但是通常这个操作是静音的导致父进程没有关注到子进程的退出,因此子进程成为僵尸进程;
若父进程获取了子进程的返回值,僵尸子进程就没有了存在的意义,就会被释放资源;因为不知道进程何时退出,因此只能创建之后一直等着子进程的退出;
2.进程等待的方法:
(1)wait等待:
1.接口:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
返回值: 成功返回被等待进程pid,失败返回-1;
参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL;
2.wait接口的功能:
一直等待子进程退出,子进程退出后,获取到的返回值,放入到传入的参数status中;如果子进程一直不退出,wait函数将一直阻塞;
(阻塞是指:为了完成某个功能发起调用,当前若不具备完成条件,等待直到条件具备完成功能后返回;非阻塞是指:为了完成某个功能发起调用,当前若不具备完成条件,立即报错返回;)
(2)waitpid方法:
1.接口:
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;
获取子进程退出返回值:
获取低7位:status & 0x7f;
获取低16位中的高8位:(status>>8)& 0xff;
status的结构:高16位不用,低6位中的高8位,最大255;存储子进程的退出返回值;低8位中的高1位中存储core dump标志;低7位中存储异常退出信号;
测试代码如下:
//测试wait函数的代码;
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(){
int pid=fork();
if(pid<0){
printf("fork error:%s\n",strerror(errno));
perror("fork error");
//errno是全局变量,存储的是每次系统调用出现的错误的原因编号;
//strerror是通过错误原因编号获取字符串的错误原因;
//perror是直接打印系统调用的错误原因;
}
else if(pid==0){
sleep(3);
exit(111);
}
else{
int status;
int ret=wait(&status);
if(ret>0 && (status & 0x7f)==0){
printf("child exit node:%d\n",(status>>8)&0xff);
}
else if(ret>0){
rintf("sig code : %d\n", status&0X7F );
}
}
while(1){
printf("i am parent\n");
sleep(1);
}
return 0;
}
程序输出结果为:
3s后子程序退出,打印退出时的exit code;然后打印父进程;
//wait_pid 的测试代码;
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(){
int pid=fork();
if(pid<0){
printf("fork error:%s\n",strerror(errno));
perror("fork error");
}
else if(pid==0){
sleep(3);
exit(111);
}
//pid_t waitpid(pid_t pid, int *status, int options);阻塞等待任意的一个子进程或者指定的子进程的退出;
//pid=-1;等待任意一个子进程; pid>0;等待指定的子进程;
//options:将wait_pid设置成为非阻塞;
//返回值:若WNOHANG被指定,没有子进程退出则立即报错返回0;错误则返回-1;
int status;
while(waitpid(-1,&status,WNOHANG)==0){
printf("drink coffee\n");
sleep(1);
}
if((status&0x7f)==0){
printf("exit code:%d\n",(status>>8)& 0xff);
}
if(WIFEXITED(status)){
printf("exit code:%d\n",WEXITSTATUS(status));
}
while(1){
printf("i am parent\n");
sleep(1);
}
return 0;
}
程序执行结果为:
四.程序替换:
1.程序替换的概念:
替换进程所运行的程序,重新初始化虚拟地址空间,更新页表信息(pcb并没有变,pcb所运行的程序变了)
即重新加载一个新的程序到物理内存中,对一个进程的代码通过页表在物理内存中的地址进行修改映射关系,让程序的代码段进过页表的转换后,指向了一个新的程序位置;
2.如何进行程序替换:exec函数族
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 *file, char *const argv[],char *const envp[]);
其中path:路径(告诉接口要替换哪个程序,这个程序在硬盘的哪个位置);file只需要告诉需要替换的程序的名称;char*const envp[] :环境变量(想要给进程传递什么环境变量,就将这些变量放到该函数中)
带p和不带p的区别:要加载的程序是否需要确定给出所在路径;
带v和带l的区别:程序的运行参数是函数的参数平铺或者是直接组织成为字符串指针数据给与;
带e和不带e的区别:要运行程序,是否需要重新自定义环境变量;
3.函数解释:
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回;
如果调用出错则返回-1;
因此exec函数只有出错的返回值而没有成功的返回值;
4.总结:
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
---|---|---|---|
execl | 列表 | 不是 | 是 |
execlp | 列表 | 是 | 是 |
execle | 列表 | 不是 | 不是,必须自己组装环境变量 |
execv | 数组 | 不是 | 是 |
execvp | 数组 | 是 | 是 |
execve | 数组 | 不是 | 不是,必须自己组装环境变量 |