文章目录
前言
进程控制,实则就是对进程进行的创建,终止,等待,替换操作,本文内容也将围绕这几个点进行哟!
1、进程创建
那么该如何创建一个进程呢?!?
pid_t fork(void); 写时拷贝技术的方式创建一个子进程,父子进程(代码共享,数据独有)
pid_t vfork(void); 创建一个新的子进程,父子进程共用同一块虚拟地址空间
1.1、fork(void)函数
fork()函数,以及其中的写时拷贝技术,在上文中有详细介绍,有需要的铁铁的们阔以瞅瞅~~~
博客连接-------进程概念(包含fork函数详细讲解)
1.2、vfork(void)函数
因为vfork函数,其创建子进程之后会与父进程共用同一块虚拟地址空间~
因此其用的是同一个栈,故一个进程对数据进行改变,也会在另一方体现(这其中就存在了调用栈混乱的隐患!!!
)
- 面对上述隐患,故而vfork创建子进程后,父进程需阻塞,知道子进程exit退出或者进行了进程替换,创建了自己的地址空间,以及各项数据
2、进程终止、
2.1、概念及场景
终止: 即进程退出运行
退出场景: 正常(符合预期&不符合预期) 异常
2.2、退出方式
- 1.main函数中return退出;(注:return只有在main中才是退出运行)
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int func()
5 {
6 printf("func()\n");
7 return 0;
8 }
9 int main()
10 {
11 func();
12 printf("hello world\n");
13 return 0;
14 }
hello world之所以被打印出来,是由于func函数中的return并未退出进程!!
- 2.库函数: void exit(int return_val); // 可以在程序任意位置进行调用退出进程
- 3.系统调用接口:void _exit(int return_val);//可以在程序任意位置进行调用退出进程
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int func()
5 {
6 printf("func()\n");
7 exit(0);
8 // _exit(0);
9 }
10 int main()
11 {
12 func();
13 printf("hello world\n");
14 return 0;
15 }
较刚刚那段代码,也体现了exit和_exit函数可于任何位置退出进程;
但是,exit()这个库函数是_exit这一系统调用接口的封装,区别在哪里呢???
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 void func()
5 {
6 printf("func()");//无\n刷新缓冲区
7 exit(0);
8 }
9 void func1()
10 {
11 printf("不存在‘\n’的func1()");//无\n刷新缓冲区
12 _exit(0);
13 }
14 int main()
15 {
16 func();
17 func1();
18 return 0;
19 }
- 其中只有func()函数打印结果输出!!!
区别:
1. 库函数,退出前刷新缓冲区中的数据;
2. 系统调用接口,直接退出释放资源
附:
- buff-缓冲区:相对于文件来说,就是数据写入文件前,先放到缓冲区,积累成大数据后,一次性刷新缓冲区,写入文件中,减少IO次数
- cache-缓存:相对于文件中读取数据,一次拿出的是一个磁盘块的磁盘数据放到内存中,下次读取数据先从缓存中对比寻找
3、进程等待
作用:等待子进程退出,获取子进程退出返回值,释放子进程资源,防止其成为僵尸进程
3.1、进程等待的方法
3.1.1、wait方法
pid_t wait(int* status);
参数:status,输出形参数,用于获取子进程退出的返回值
(若不关心其可设置为NULL)
返回值: 成功,返回子进程的pid,大于0
失败,返回-1
wait接口是一种阻塞(可中断休眠)等待,等待任意子进程退出,若无子进程退出,则阻塞,一直等待
#include <stdio.h>
#include <stdlib.h>
#include <sys/uio.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t ret =fork();
if(ret<0)
{
perror("fork error\n");
return -1;
}
else if(ret==0)
{
sleep(3);
exit(66);
}
int exit_code;
int res=wait(&exit_code);//阻塞等待任意一个子进程退出
if(res<0)
{
perror(“wait error”);
return -1;
}
printf("%d\n",exit_code);
}
3.1.2、waitpid方法
pid_t waitpid(pid_t pid,int* status,int options);
参数:pid,制定等待子进程的ID,-1表示等待任意进程退出
status,同wait函数中status参数
options,操作选项:
0 :表示阻塞等待,
WNOHANG:非阻塞等待,若无子进程退出(包括子进程已经退出),不阻塞
——需循环使用,否则可能参数僵尸进程
返回值:
正常:返回子进程pid,大于0
若设置了WNOHANG,若未处理到退出的进程,放回0
出错,返回-1
若wait,waitpid等待之前,已经有子进程退出,那么会直接进行退出子进程之后的操作,返回子进程ID
int main()
{
pid_t ret =fork();
if(ret<0)
{
perror("fork error\n");
return -1;
}
else if(ret==0)
{
sleep(3);
exit(66);
}
int exit_code;
//int res=waitpid(ret,&exit_code,WNOHANG);//非阻塞等待可能会导致僵尸进程
int res;
while((res=waitpid(-1,&exit_code,WNOHANG))==0)//非阻塞等待需循环调用直至等到接受子进程退出的返回值,才不会产生僵尸进程
{
printf("子进程还没退出,那我等你一哈嘛!!!\n");
sleep(1);
}
if(res>0)//成功获取到子进程pid
{
printf("pid: %d exit\n",res);
}
else if (res==0)//WNOHANG中子进程已经退出或无进制退出情况
{
printf("have no stutas exited!\n");
}
else //出错
{
perror("error fork");
return -1;
}
while(1)
{
sleep(1);
printf("parents-------------\n");
}
return 0;
}
若使用WNOHANG操作选项,是进行循环处理,则可能代码运行至waitpid处,子进程还并未退出,同时WNOHANG是非阻塞操作,进而子进程资源未被释放,导致僵尸进程
3.2、关于进程退出返回值
关于进程退出返回值(正常退出,异常退出则不获取返回值),实际只用了一个字节保存,即只能返回0~255的数据
进程退出,分为正常退出(exit,main中return),异常退出(中途崩溃)
一个进程异常退出,是没有返回值的,这时候保存返回值的空间中的数据是不可知,获取其返回值也无意义
- 在wait和waitpid中的输出参数,status,低七位是异常退出码,如果不为0就表示进程异常退出
- 因此在进程获取返回值之前,应当先获取其异常退出码,若正常,在获取其退出返回值
可通过位移操作:
- 异常退出码:(4字节中低7位)status & 0x7f
- 退出返回值:(4字节中低16位的高8位) (status>>8)& 0xff
宏函数操作:
- int WIFEXITEND(status); 等价于 status & 0x7f
- int WEXITSTATUS(status);等价于 (status>>8)& 0xff
落实于代码中:
if(res>0)
{
if(exit_code & 0x7f==0)
{
printf("pid:%d exited! exit code:%d\n",res,exit_code);
}
else
{
printf("child abnormal termination!\n");
}
}
4、进程替换
4.1、概念
- 前言: 创建一个子进程(pcb),复制了父进程大量的信息,代码共享,可以分担任务处理的压力,但这不是主要目标,主要目标是进行程序替换,让子进程管理另外一个程序的运行
- 程序替换 : 将一个新的程序加载到内存中,然后改变一个进程的页表映射信息,将其更改到新的程序的指令和数据上,这时候当前的pcb管理的程序,便不是原来的程序,而是新程序
4.2、程序替换接口
int execl(const char *path, const char *arg, ...);
参数:path:带路径的新程序名称额,arg是参数,是一个不定参,以NULL结尾
int execlp(const char *file, const char *arg,...);
参数:file:不带路径的新程序名称
不带路径,则默认在path环境变量指定的路径下去找程序,常用于指令程序的替换
int execle(const char *path, const char *arg ,..., char * const envp[);
参数:path:带路径的新程序名称, arg是参数,envp是环境变量;
int execv(const char *path, char *const argv[]);
参数:argv[],与execl的区别在于,参数不是一个一个给,而是组织成为字符指针数组一次性表示
int execvp(const char *file, char *const argv[]);
参数:与execlp相比,在指定路径下找程序,参数通过数组表示
~~~总结:
1.l与v,在于参数是一个个列出,还是一数组的形式
2.不带p和带p,在于是否同path环境变量指定路径下去找程序
3.带不带e,即参数中是否需要环境变量
4.3、实例分析
//exec.c程序
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello world\n");
char* argv[]={"./example","-a","-s","-a",NULL};
execv("./example",argv);
printf("hello monkey\n");
return 0;
}
//example.c程序
#include <stdio.h>
int main()
{
printf("example()\n");
return 0;
}
exec运行结果:
图形代码分析:
小结:
- 1.函数若程序替换成功,即修改了页表映射关系,而新的程序加载到内存中,之前的程序则被释放
- 2.调用出错返回-1
- 3.exec函数只有出错的返回值而没有成功的返回值
总结
- 以上的,从进程创建,退出,等待,替换四个方面,讲解了进程控制;
- 后续也将以一篇mini_shell实现的文章,便于大家更好的去感受进程控制!!!
文章有不足的地方,也希望大家能多多指点指点~~~~