文章目录
1.进程创建
1.1fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h> //fork函数的头文件
pid_t fork(void); //
返回值:创建成功给子进程返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核(OS)做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程(fork创建子进程是以父进程为“模板”的)
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
如上图,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
1.2fork函数返回值
- 子进程返回0。
- 父进程返回的是子进程的pid。
其中,默认情况下,父子进程共享代码,但是数据各自私有一份!
代码共享:所有代码共享,不过一般都是从fork之后开始执行。
- 问题:为什么代码共享?
代码不可被修改,所以各自私有浪费空间。
数据私有:
- 问题:数据为啥要私有一份?
因为进程之间具有独立性。数据是很多的,不是所有的数据都立马要使用,并且不是所有的数据都要进行拷贝。但是如果立马要独立,就需要将数据全部拷贝,把本来可以在后面拷贝的,甚至不需要拷贝的数据都拷贝了,比较浪费时间和空间。
1.3 写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
问题:现在有数据10M,写入的时候1M,发生写时拷贝,是拷贝10M还是1M?
答案是:1M。数据段在页表中的映射是很细的,只会拷贝写入的新的数据。
1.3 fork的常规用法
1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
2.一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因:
- 系统中有太多的进程。
- 实际用户的进程数超过了限制。
总结:如何理解子进程创建?
- 本质是系统多了一个子进程,子进程以父进程为模板。
2.进程终止
2.1 进程退出的情况
- 代码跑完了,结果正确
- 代码跑完了,结果不正确
- 代码没跑完,程序异常终止
2.2 进程常见退出方法
正常终止(可以通过 echo $?
查看最近一次进程退出码):
- 从main返回
- 调用exit
- _exit
异常退出:
- ctrl + c,信号终止
1、main函数用return退出进程;
main函数退出时, return的数字就是程序的退出码。
问题: 为什么main的return一般会写成0?
因为0在函数设计中,一般代表正确。非0代表错误。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
int main()
{
cout << "hello" << endl;
return 0;//退出码为0
}
2、exit函数退出
#include <unistd.h>
void exit(int status);
exit:终止整个进程,任何地方都会调用,都会终止。
return:终止函数。main函数return 代表进程退出。
3、_exit函数
#include <unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255。
exit函数和_exit区别:
1 #include <iostream>
2 #include <unistd.h>
3 #include <sys/types.h>
4 #include <stdlib.h>
5
6 using namespace std;
7
8 int exe()
9 {
10 _exit(12);
11 return 12;
12 }
13
14 int main()
15 {
16 cout << "you should running here!";
17 sleep(5);//sleep5秒
18 exit(13);
19 return 0;//退出码为0
20 }
_exit时系统调用,exit函数和_exit函数的最大区别就是exit函数可以刷新缓冲区而_exit函数不会刷新缓冲区。
3.进程等待
3.1进程等待的必要性
- 子进程退出,父进程如果不处理,就可能造成僵尸进程的问题,从而导致内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,kill -9 也没办法杀死,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
3.2进程等待wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
3.3 waitpid方法
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: 默认是0,代表阻塞方式
- WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
3.4 获取子进程status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
如果我们只想获取退出码的话我们可以(st>>8)&0xff
。
4.进程程序替换
4.1替换原理
用fork创建子进程:想让子进程和父进程执行不同的程序。
这要调用exec函数来执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,代码也要发生写时拷贝。
调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
这些新的代码和数据的替换过程是由os操作的,os会给我们提供接口
4.2 替换函数
有六种以exec开头的函数,统称为exec函数:
#include <unistd.h>
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[]);
4.3 函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1。
- 所以exec函数只有出错的返回值而没有成功的返回值。
4.4 命名理解
-
l(list) : 表示参数采用列表
-
v(vector) : 参数用数组
-
p(path) : 有p自动搜索环境变量PATH
带p的传入系统中的程序或者命令的时候,会自动去搜索环境变量PATH中寻找对应的指令,不需要传入文件路径不带p的,则需要自己传入文件路径。 -
e(env) : 表示自己维护环境变量
4.5示例
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6
7 execl("/usr/bin/ls", "ls", "-i", "-l", "-a", NULL);
8
9 //带p的,可以使用环境变量PATH,无需写全路径
10 execlp("ls", "ls", "-i", "-l", "-a",NULL);
11
12 char *arg[] = {"ls", "-a", "-l", "-i", NULL};
13
14 execv("/usr/bin/ls", arg);
15
16 //带p的,可以使用环境变量PATH,无需写全路径
17 execvp("ls", arg);
18 printf("you should running here!\n");
19 return 0;
20 }