文章目录
1 创建进程
fork()函数:从已经存在的进程中创建一个新进程,新进程为子进程,原来的进程为父进程。
进程调用fork函数之后,当控制转移到内核中的fork代码后,
内核做:
①分配新的内存块和内存数据结构给子进程。
②将父进程部分数据结构内容拷贝至子进程。
③添加子进程到系统进程列表当中。
④fork返回,开始调度器调度
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。fork之后,谁先执行完全由调度器决定。
写时拷贝
通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入数据,便以写时拷贝的方式各自一份副本。具体见下图
写时拷贝的好处:使父子进程得以彻底分离,可以实现进程独立性
写时拷贝,是一种延时申请技术,可以提高整机内存的使用率
2 进程终止
2.1 进程退出情况
进程退出情况
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
进程运行正确,返回值为0,代表success;
进程运行错误,返回非0值作为错误码,一个错误码对应着一个错误信息
退出码只能确定程序结果运行是否正确,当程序异常退出时,错误码就没有任何意义了。
自己可以使用这些退出码和含义,当然,也可以自己设计出一套退出方案
2.2 进程终止的常见方式
2.2.1 return语句
只有main函数的return语句才代表进程终止,其它函数的return语句只代表函数结束。
由此可知:main函数的返回值不一定要是0,而是要根据自己的实际情况选择不同的返回值
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int number=0;
int sum=0;
for(number=1;number<=100;number++)
{
sum+=number;
}
if(sum==5050)
{
return 0;//结果正确
}
else{
return 1;//结果错误
}
}
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int number=0;
int sum=0;
for(number=1;number<=100;number++)
{
sum+=number;
}
sum=0;//将结果改为错误的
if(sum==5050)
{
return 0;//结果正确
}
else{
return 1;//结果错误
}
2.2.2 exit()函数
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("hello world\n");
sleep(3);
exit(0);
}
exit()放在任意位置(主函数或者其他函数)都可以结束进程
参数是退出码,可以是任意整数
exit()在结束进程的同时会刷新缓冲区
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("hello world");
sleep(3);
exit(22);
}
printf输出打印的时候没有加\n,数据会先在缓冲区,休眠3秒以后,exit()函数会刷新缓冲区,打印出语句
2.2.3 _exit()函数
和exit()唯一的区别就是_exit()只会终止进程,不会刷新缓冲区
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("hello world");
sleep(3);
_exit(22);
}
可以看到没有打印出语句
3进程等待
3.1 进程等待的重要性
子进程退出,如果父进程什么都不管,就会造成僵尸进程问题,进而会产生内存泄露
另外,进程一旦变成僵尸状态,即使发送kill -9信号,也无法杀掉僵尸进程
所以父进程通过进程等待的方式,回收子进程资源,获取子进程的退出信息,进而也可以知道父进程派给子进程的任务完成如何
3.2 进程等待的方法
3.2.1 wait()方法
等待成功,返回所终止子进程的pid;等待失败返回-1
#include<stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int cnt=5;
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);
}
else if(id==0)
{
while(cnt--)
{
printf("I am child\n");
sleep(1);
}
}
else{
sleep(10);
printf("I am father\n");
pid_t ret=wait(NULL);
if(ret>0)
{
printf("等待子进程成功 ,子进程pid:%d\n",ret);
sleep(3);
}
else{
printf("等待子进程失败\n");
}
}
}
3.2.2 waitpid()方法
pid_t waitpid(pid_t pid, int *status, int options)
组成 | 说明 |
---|---|
返回值 | ①等待成功&&子进程退出:返回子进程的pid ②等待失败:返回-1 |
参数pid | ①设置为指定进程的pid,代表等待某个指定的进程,②若设置为-1,代表等待任意的进程 |
参数status | 是输出型参数,获得进程退出的结果 |
参数option | option为等待方式,①设置为0代表默认行为,即阻塞等待 ②设置为WNOHANG,代表非阻塞等待 |
参数status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
如果传递为NULL,表示不关心子进程的退出状态信息
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
status不能简单的当做整形来看待,可以当做位图来看待
下标[0,7)的7个比特位代表终止信号,如果收到了信号,说明进程是异常终止,此时便不需要关注退出码
下标[8,16)这8个比特位代表退出码,如果是正常退出,此时信号为0,需要关注退出码
下标为7的比特位代表core dump标志
正常退出的情况
#include<stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int cnt=5;
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);
}
else if(id==0)
{
while(cnt--)
{
printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
exit(20);
}
else{
sleep(10);
printf("I am father\n");
// pid_t ret=wait(NULL);
int status=0;
pid_t ret=waitpid(id,&status,0);//父进程的id就是子进程的pid
if(ret>0)
{
printf("等待子进程成功 ,子进程pid:%d,子进程收到的信号编号:%d,子进程退出码:%d\n",ret,status&0x7F,(status>>8)&0xFF);
sleep(3);
}
else{
printf("等待子进程失败\n");
}
}
}
异常退出的情况
#include<stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int cnt=5;
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);
}
else if(id==0)
{
while(cnt--)
{
printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
int *p=NULL;
*p=100;//野指针
exit(20);
}
else{
sleep(10);
printf("I am father\n");
// pid_t ret=wait(NULL);
int status=0;
pid_t ret=waitpid(id,&status,0);//父进程的id就是子进程的pid
if(ret>0)
{
printf("等待子进程成功 ,子进程pid:%d,子进程收到的信号编号:%d,子进程退出码:%d\n",ret,status&0x7F,(status>>8)&0xFF);
sleep(3);
}
else{
printf("等待子进程失败\n");
}
}
}
可以看到 11号信号为SIGSEGV
SIG是信号名的通用前缀
SEGV为segmentation violation 即存储器区段错误,无效的内存引用
参数option
阻塞等待:在子进程执行任务期间,父进程一直处于阻塞队列,直到子进程退出
非阻塞等待:不断的轮询检测子进程的状态,若子进程没有退出,父进程可以执行自己的代码
此时返回值就分为三种情况
等待成功&&子进程退出:返回子进程的pid
等待成功&&子进程未退出:返回0(非阻塞等待)
等待失败:返回-1
非阻塞等待案例
#include<iostream>
#include<vector>
#include<stdio.h>
#include <sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
typedef void (*handler_t)();//函数指针类型
std::vector<handler_t>handlers;
void fun_one()
{
printf("这是一个临时任务1\n");
}
void fun_two()
{
printf("这是一个临时任务2\n");
}
void Load()//父进程闲了想要执行任何方法的时候,只要向Load里面注册,就可以让父进程执行对应的方法
{
handlers.push_back(fun_one);
handlers.push_back(fun_two);
}
int main()
{
int cnt=5;
pid_t id=fork();
if(id<0)
{
perror("fork");
exit(1);
}
else if(id==0)
{
while(cnt)
{
printf("我是子进程:%d\n",cnt--);
sleep(1);
}
exit(20);
}
else{
int quit=0;
while(!quit)
{
int status=0;
pid_t res=waitpid(-1,&status,WNOHANG); //以非阻塞方式等待
if(res>0)
{
//等待成功&&子进程退出
printf("等待子进程成功,退出码:%d\n",WEXITSTATUS(status));
quit=1;
}
else if(res==0)
{
//等待成功&&子进程未退出
printf("子进程还在运行中,父进程可以等一等\n");
if(handlers.empty())
{
Load();
}
for(auto iter:handlers)
{
iter();
}
}
else{
printf("wait失败\n");
quit=1;
}
sleep(1);
}
}
}
对于获取子进程退出结果的参数status,除了使用位操作,还可以使用系统定义好的宏
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
4 进程替换
4.1 替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数,以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
简单来说:用exec()函数进行程序替换,就是将新的磁盘上的程序加载到内存,并和当前进程的页表,重新建立映射
4.2 替换函数
①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[]);
函数解释
这些函数如果调用成功则加载新的程序,从启动代码开始执行,不再返回。
如果调用出错则返回-1。
所以exec函数只有出错的返回值而没有成功的返回值。
命名理解
①l(list) : 表示参数采用列表
②v(vector) : 参数用数组
③p(path) : 有p自动搜索环境变量PATH
④e(env) : 表示自己维护环境变量
常用的exec函数举例
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
# define NUM 16
const char* myfile="./mycmd";
int main()
{
pid_t id=fork();
if(id==0)
{
// char* _argv[NUM]={"ls","-a","-l",NULL};
char* _argv[NUM]={"mycmd","-a",NULL};
printf("子进程开始运行,子进程pid:%d\n",getpid());
sleep(3);
// execl("/usr/bin/ls","ls","-a","-l",NULL);//参数使用列表的方式执行ls -a -l命令
// execv("/usr/bin/ls",_argv);//参数使用数组的方式执行ls -a -l命令
// execlp(myfile,"mycmd","-b",NULL);//自动在环境变量中找命令
// execlp(myfile,"mycmd","-a",NULL);
// execvp(myfile,_argv);
execlp("java","java","hello",NULL);//在自己的程序里面又去调用java程序
exit(1);
}
else{
printf("父进程开始运行,父进程pid:%d\n",getpid());
int status=0;
pid_t res=waitpid(-1,&status,0);//阻塞等待
if(res>0)
{
printf("wait success,exit code:%d\n",WEXITSTATUS(status));
}
}
return 0;
}
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
int main(int argc,char* argv[])//mycmd -a或mycmd -b
{
if(argc!=2)
{
printf("can not execute\n");
exit(1);
}
if(strcmp(argv[1],"-a")==0)
{
printf("hello a!\n");
}
else if(strcmp(argv[1],"-b")==0)
{
printf("hello b!\n");
}
else{
printf("default!\n");
}
return 0;
}