引言
上一次,我们已经学习过了什么是进程和进程与程序的区别,这次我们要学习如何创建,终止,等待进程。
1.进程创建
1.1 fork函数初识
头文件:#include<unistd.h>
pid_t fork(void);
返回值:自进程返回0,父进程返回子进程id,出错返回-1。
当系统调用fork的时候,内核要做:
- 1.分配新的内存块和内核数据结构给子进程。
- 2.将父进程部分数据结构(子进程能使用到的)内容拷贝给子进程
- 3.添加子进程到系统进程列表中(将子进程放入链表中组织起来)
- 4.由调度器调度(父子进程先后顺序由调度器确定)
注意:在fork之前父进程的操作与子进程无关,fork之后,谁先执行完全由调度器决定。
1.2fork返回值:
- 子进程返回0。
- 父进程返回子进程ID。
1.3写时拷贝
注意两点:
- 1.当fork创建子进程的时候,代码共享,数据功效
- 2.当父进程/子进程任意一个程序尝试对数据进行修改,操作系统会采用写实拷贝:重新建设该写入进程与物理内存的映射关系,各自写时拷贝一个副本。
- 好处:不会浪费多余的资源
1.4fork用法:
- 1.父进程希望复制自己,使子进程和父进程执行不同的代码段
- 2.一个进程要执行不同的程序。
1.5fork失败的原因:
- 系统由太多进程
- 实际用户的进程数超过限制
2. 进程终止
2.1进程退出情况
- 1.代码运行成功,结果正确
- 2.代码运行成功,结果错误(与预期不符)
- 3.代码异常终止(除0,野指针)
2.2 进程常见退出方法
正常退出:
- return返回
- exit
- _exit
2.4_exit函数
头文件:#Include<unistd.h>
void exit(int status);
参数:status定义了进程的终止状态,父进程通过wait来获取该值
2.3exit函数
头文件:#include<unistd.h>
void exit(int status);
让我们实用一下:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
printf("cpp foever\n");
exit(10);
return 0;
}
查看返回值:
exit和_exit比较:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("cpp foever");
sleep(1);
//_exit(10);
exit(10);
return 1000;
}
exit的情况:
_exit的情况:
由此我们可以看出exit和_exit是有区别的
区别在于exit要另外执行以下两点:
- 执行用户定义的清理函数
- 关闭所有打开的流,所有缓存的数据均被写入。
2.4 return退出
=执行exit,因为调用main的运行时函数会将main的返回值当作exit的参数。
3.进程等待
3.1作用:
- 1.回收进程的资源,避免内存泄漏。
- 2.父进程可能需要直到子进程的返回信息。
3.2等待方式:
- 1.wait方法
头文件:#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid(父进程等待子进程,返回子进程pid),失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心可以设置为NULL
- 2.waitpid方法
头文件:#include<sys/types.h>
#include<sys/wait.h>
void waitpid(pid_t pid, int* status, int options);
返回值:
>0,等待成功
<0,等待失败
==0,等待进程未结束
参数:
pid:
pid = -1,等待全部子进程。
pid > 0,等待其进程与pid相等的子进程,输入你要等待的进程
status:
输出型参数,获取子进程退出状态
WIFEXITED:若位正常终止子进程的类型,则为真。
WEXITSTATUS:若WIFEXITED非0,提取退出码。
options:
0:阻塞式。
!0:非阻塞式
-
3.使用的情况
- 1.如果子进程已经退出,调用wait/waitpid,会立即返回,并且释放资源,获得子进程的退出信息。
- 2.任意时刻调用wait/waitpid,如果子进程存在且再运行,则父进程会阻塞等待
- 3.如果不存在子进程,则返回错误。
-
4.获取子进程status
- 不论wait和waitpid都有一个status参数,该参数是一个输出参数,有操作系统填充
- 如果传值为NULL,表示不关心子进程退出信息
- 否则,该参数是子进程的退出信息
- status要作为为徒看待,如下图(只研究status低16bit位)
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("I am child :.. %d\n", getpid());
sleep(5);
exit (13);
}
else
{
printf("father : pid : %d, ppid : %d\n", getpid(),getppid());
int status = 0;
int ret = waitpid(id, &status, 0);
if(ret < 0)
{
printf("wait error, wait ret : %d\n", ret);
}
else
{
printf("wait success...%d\n", ret);
printf("wait status :%d\n", ((status) >> 8) & 0xFF);//显示第8比特位
printf("exit signal :%d\n", status & 0x7F);//显示错误信号的位置
}
}
return 0;
}
运行结果
进程可以通过发送信号的方式人为终止,或者因为进程内部出现错误被终止。
- 5.等待方式:
- 阻塞式:条件不发生,父进程一直等待
- 非阻塞:条件不发生,父进程返回运行其他程序,然后再去验证条件是否达成,如果还是没有,运行上一步,如此循环。
- 什么是阻塞?
当某个条件不满足,操作系统将该进程设置为非运行状态,并将该进程(PCB)放入等待队列,当条件满足时,从等待队列中将该进程唤醒(将该进程状态设为R状态),并从等待队列中移除,交由调度器管理。
阻塞式:
int ret = waitpid(id + 1, &status, 0);
非阻塞式写法:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
printf("I am child :.. %d\n", getpid());
sleep(5);
exit (13);
}
else
{
printf("father : pid : %d, ppid : %d\n", getpid(),getppid());
int status = 0;
while(1)
{
int ret = waitpid(id, &status, WNOHANG);
if(ret < 0)
{
printf("wait error, wait ret : %d\n", ret);
}
else if(ret > 0)
{
printf("wait success...%d\n", ret);
printf("wait status :%d\n", ((status) >> 8) & 0xFF);
printf("exit signal :%d\n", status & 0x7F);
break;
}
else
{
sleep(1);//做其他事
printf("parent wait again! \n");//轮询等待
}
}
}
return 0;
}
4.进程替换
4.1原理:
- 用fork创造子进程后父子进程执行的是和父进程相同的程序,如果想要执行另外一个程序,子进程需要通过一种exec函数。当进程调用了exec函数之后,进程代码段和数据段完全被新程序替换,从新程序的启动例程开始执行。
- 调用exec不创建新的进程,所以调用exec前后进程id不变。
4.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[]);
-
返回值:exec只有错误的返回值,但是没有正确的返回值,因为其一旦正确,他就会加载新的程序开始执行。
-
解释:
- l(list):表示参数采用列表
- v(vector):参数用数组
- p(path) : 有P自动搜索环境变量PATH
- e(env):表示自己维护环境变量
为了理解函数的正确使用方式,让我们看看下面的代码: