操作系统允许一个进程创建另一个进程,并且允许子进程继承父进程所拥有的资源,当子进程被终止时,其在父进程处继承的资源应当还给父进程。同时,终止父进程同时也会终止其所有子进程。
注意:Linux操作系统对于终止有子进程的父进程,会把子进程交给1号进程接管。
进程创建:1、命令行启动命令(程序、指令等) 2、通过程序自身,fork出子进程
创建进程的过程:
父进程通过调用fork函数创建一个新的运行的子进程。
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。父进程和新创建的子进程之间的最大区别在于它们有不同的PID
#include
pid_t fork(void);
//返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,OS做:
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 const char *str = "hello world";
7
8 pid_t pid = fork();
9 //之后才会运行
10 if(pid == 0){
11 while(1){
12 printf("child: ppid: %d, pid: %d, str: %s\n", getppid(), getpid(), str);
13 sleep(1);
14 }
15 }
16 else if(pid > 0){
17 while(1){
18 printf("father: ppid: %d, pid: %d, str: %s\n", getppid(), getpid(), str);
19 sleep(1);
20 }
21 }
22 else{
23 perror("fork");
24 }
25 return 0;
26 }
注意:虽然父子进程代码共享,但fork之后才有子进程,所以子进程是执行fork之后的代码。
fork常规用法:
1、一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如:父进程等待客户端请求,生成子进程来处理请求。
2、一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
1.为什么fork有两个返回值?
2.一个变量里面,怎么会有两个不同的值,从而让父子进入不同的业务逻辑。
> fork后父进程返回时,本质是把返回值写入变量pid,而此时子进程已经创建好了,必定发生了写时拷贝。
所以这一个变量名,内容是不同的,而本质是父子页表映射数据到了不同的内存区域。所以接下来父子进程读取pid拿到的值就不一样。
通常,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
写时拷贝的过程实现是由OS参与完成的。
为什么要有写时拷贝(数据的)?
保证父子进程的“独立性”
1.节省资源。父子进程创建时,拷贝不需要写入修改的数据(只读)是没有意义的,如果直接把数据各自拷贝一份,就浪费了内存和系统资源。
2.提高fork创建的效率。fork时创建数据结构,如果还要将数据拷贝一份,fork效率降低
3.减少fork失败的概率。fork本身就是向系统要更多的资源,而要越多的资源就越容易导致fork失败。
进程退出的情况分类:
1.代码跑完,结果正确。退出码:0
2.代码跑完,结果不正确。逻辑问题,但是没有导致程序崩溃。退出码:!0
3.代码没有运行完毕,程序崩溃了,退出码没有意义。
进程常见退出方法:
正常终止(可以通过
echo $?
查看进程退出码):
1.main函数return
2.任何函数exit
异常退出:
ctrl+c,信号终止
main函数中,return的值(退出码)代表进程退出,结果是否运行正确。0代表成功。而return的0是给系统看的,以此确认进程执行结果是否正确。如果我们想看最近一次执行的一个程序运行结束时的退出码,可以用echo $?
来查看
退出码:可以认为定义,也可以使用系统的错误码list
当程序运行失败时,最关心的是失败的原因。而计算机擅长处理整数类型的数据(0, 1, 2, 3…)。 int(整数)-> string(错误码描述)
父进程一般需要知道子进程退出的结果,即进程的退出码。但父进程也可以不关心子进程的运行结果。
进程非正常结束:野指针、/0、越界等,此时退出码无意义。(此时是由信号来终止的)
main函数return。非main函数的return不是终止进程,而是结束函数。例如:
int show()
{
return 0;
}
int main()
{
show();
return 0;
}
这里main函数中调用完show,这个进程并不会终止。
exit:在任何函数中exit都表示直接终止进程
exit:在退出时会执行用户定义的资源清理函数,包括刷新缓冲区,关闭流等。
_exit:在退出时不会进行后续资源处理,直接终止进程。
可以看到使用_exit时,退出码照样是11。
站在OS角度,如何理解进程终止?
核心思想:归还资源
1."释放"曾经为了管理进程所维护的所有的数据结构对象。
2."释放"程序代码和数据占用的内存空间。
3.取消曾经该进程的链接关系。
释放:不是真的把数据结构对象销毁,而是设置为不用状态,然后保存起来。如果不用的对象多了,就有了一个"数据结构的池"。
内存池:先申请分配一定大小的空间,在需要使用时再使用内存池中的空间,就不需要每次需要内存时都进行new/malloc申请空间,提高了用户的效率。
释放数据结构对象:当要创建进程时,需要将内存池中拿出一块空间,并将这块空间强转成task_struct*,再进行访问。但如果每次都要强转就太麻烦。当一个pcb没人用时,可以将该pcb取出并链接到数据结构池中,该过程就是释放不用的数据结构对象,而需要用时再从池中取出,就不用进行强转了。这种释放规则叫做Slab分派器。
释放代码:不是将代码和数据结构清空,而是把内存设置为无效即可。
例如我们在下载电影资源时,所需下载拷进电脑的时间很多,删除却很快,说明写入和删的逻辑是不同的。写入时需要开辟空间,而删的本质是标识数据对应在磁盘上无效,一旦标识无效即意味着可以被覆盖,在写入新数据时,将该无效数据被覆盖也就是被清除了。
如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父。init进程的PID为1,是在系统启动时由内核创建的,它不会终止,是所有进程的祖先。如果父进程没有回收它的僵死子进程就终止了,那么内核会安排init进程去回收它们。不过,长时间运行的程序,比如shell或者服务器,总是应该回收它们的僵死子进程。即使僵死子进程没有运行,它们依然消耗系统的内存资源。
一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。
等待的必要性:
进程等待的方法:
wait/waitpid
**wait:**等待任意一个子进程。当子进程退出,wait就可以返回。
#include
#include
pid_t wait(int* status);
返回值:成功则返回被等待进程pid,失败返回-1
参数:输出型参数,获取子进程退出状态,不关心则可以设置为NULL
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6
7 int main()
8 {
9 pid_t id = fork();
10 if(id < 0){
11 perror("fork");
12 return 1;//自定义
13 }
14 else if(id == 0){
15 //child
16 int count = 5;
17 while(count){
18 printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
19 sleep(1);
20 }
21 printf("child quit...\n");
22 exit(0);
23 }
24 else{
25 printf("father is waiting...\n");
26 pid_t ret = wait(NULL);
27 printf("father is wait done, ret: %d\n", ret);
28 }
29 }
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;//自定义
}
else if(id == 0){
//child
int count = 5;
while(count){
printf("child is running: %d, ppid: %d,pid:%d\n", count- -, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
exit(0);
}
else{
printf("father is waiting...\n");
sleep(10);
pid_t ret = wait(NULL);
printf("father is wait done, ret: %d\n", ret);
sleep(3);
printf("father quit...\n");
}
return 0;
}
可以看到,5s后子进程变为僵死状态,再过5s后子进程被回收。
一般而言,我们需要fork之后,让父进程等待。
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:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
默认options设置为0,是阻塞式等待。
int main()
{
pid_t id = fork();
if(id == 0){
int count = 5;
while(count){
printf("child is runing: %d, ppid: %d, pid:%d\n", count-- , getppid(),getpid());
sleep(1);
}
printf("child quit...\n");
exit(0);
}
//father
sleep(8);
pid_t ret = waitpid(id, NULL, 0);
printf("father wait done, ret : %d\n", ret);
sleep(3);
}
获取子进程status
wait和waitpid都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,则表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的推出信息反馈给父进程。status不能简单地当作整型来看待,可以当作位图来看待。(之研究status低16bit位)
正常终止:
int main()
{
pid_t id = fork();
if(id == 0){
int count = 5;
while(count){
printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
exit(123);
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
int code = (status >> 8) & 0xFF;
printf("father wait done, ret : %d\n, exit code: %d\n", ret, code);
if(code == 0){
printf("漂亮,事情办成了!\n");
}
else{
printf("完了,需要重来了!\n");
}
}
注意:不能定义全局变量code来拿到子进程的退出结果,因为父子进程是独立的。当写入变量时,会进行写时拷贝,此时父进程看不到该变量,也就无法取得子进程退出状态。
子进程虽然已经结束了,但子进程还是僵尸,子进程数据结构并没有完全被释放,当进程退出时,如task_struct里会被填上子进程退出时的退出码,所以waitpid拿到的status的值,是通过task_struct内部拿到的。
异常终止:
一般进程提前终止,本质是该进程收到了os发送的信号。
此时status的低7位标识当前进程退出时的终止信号。
信号是从1开始的,也就是说如果检测到低7位全是0,那就是正常终止,此时的退出状态才有意义。
int main()
8 {
9 pid_t id = fork();
10 if(id == 0){
11 int count = 5;
12 while(count){
13 printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
14 sleep(1);
E> 15 int *p = 0x12345;
16 *p = 100;
17 }
18 printf("child quit...\n");
19 exit(10);
20 }
21 //father
22 int status = 0;
23 pid_t ret = waitpid(-1, &status, 0);
24 int code = (status >> 8) & 0xFF;
25 int sig = status & 0x7F; //0111 1111
26 printf("father wait done, ret : %d\n, exit code: %d, sig: %d\n", ret, code, sig);
}
野指针操作,程序崩溃,sigle不为0,父进程得知子进程异常终止。
完整的等待过程:
7 int main()
8 {
9 pid_t id = fork();
10 if(id == 0){
11 int count = 5;
12 while(count){
13 printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
14 sleep(1);
15 }
16 printf("child quit...\n");
17 exit(10);
18 }
19 int status = 0;
20 pid_t ret = waitpid(id, &status, 0);
21 if(ret > 0){
22 printf("wait success!\n");
23 if((status & 0x7F) == 0){
24 printf("process quit normal!\n");
25 printf("exit code: %d\n", (status>>8)&0xFF);
26 }
27 else{
28 printf("process quit error!\n");
29 printf("sig: %d\n", status&0x7F);
30 }
31 }
}
7 int main()
8 {
9 pid_t id = fork();
10 if(id == 0){
11 int count = 5;
12 while(count){
13 printf("child is runing: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
14 sleep(1);
15 }
16 printf("child quit...\n");
17 exit(10);
18 }
19 int status = 0;
20 pid_t ret = waitpid(id, &status, 0);
21 if(ret > 0){
22 printf("wait success!\n");
23 if(WIFEXITED(status)){
24 printf("process quit normal!\n");
25 printf("exit code: %d\n", WEXITSTATUS(status));
26 }
27 else{
28 printf("process quit error!\n");
29 }
30 }
}
如果options传WNOHANG,等待方式为非阻塞,如果传0,默认是阻塞的。我们目前所调用的函数(都是单执行流,简单),都是阻塞函数。阻塞等待:调用方一直在等待,期间不做任何事。而非阻塞是在不断检测状态。
非阻塞轮询方案:父进程多次调用waitpid,检测子进程的运行状态,最终如果检测到了子进程的退出状态,waitpid才成功返回,而在此之前都是失败返回。
注意:waitpid的失败返回有两种意思:1、并不是真正失败,仅仅是对方的状态还没有达到预期(下次再检测)。2、真的失败了
阻塞等待中,是父进程在等待,子进程在跑代码。
“等”:将当前进程放入等待队列,并将进程状态设置为非R状态。
唤醒进程->等待队列->运行队列->R
当我们运行过多程序,计算机卡住时,有可能就是因为运行进程太多,导致OS把进程放入等待队列。
非阻塞等待:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6
7 int main()
8 {
9
10 pid_t id = fork();
11 if(id == 0){
12 int count = 3;
13 while(count){
14 printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
15 sleep(1);
16 }
17 printf("child quit..\n");
18 exit(10);
19 }
20 int status = 0;
21 while(1){
22 pid_t ret = waitpid(id, &status, WNOHANG);
23 if(ret == 0){
24 printf("wait next!\n");
25 printf("father do other thing!\n");
26 }
27 else if(ret > 0){
28 printf("wait success, ret: %d, code: %d\n", ret, WEXITSTATUS(status));
29 break;
30 }
31 else{
32 printf("wait failed\n");
32 printf("wait failed\n");
33 break;
34 }
35 }
36 }
创建子进程的目的有:1.执行父进程的部分代码。2.执行其它的代码。
进行进程替换的目的就是让子进程执行其它程序的代码。
子进程不改变进程内核的数据结构,只修改部分的页表数据,然后将新程序的代码和数据加载到内存,重新构建映射关系,和父进程彻底脱离关系,就是进程替换。
在进行程序替换的时候,没有创建新的进程。子进程的pid没改变。
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
如何进行进程替换?
int execl(const char *path, const char *arg, ...);
int main()
{
printf("my process begin\n");
execl("/usr/bin/ls", "ls", "-a", "-l", "-i", NULL);
printf("my process end!\n");
return 0;
}
exec* 程序替换,一旦替换完成,原程序后面的代码就不再执行,所以end也没有输出,返回值也没有意义。所以exec*函数不用考虑返回值,只要返回。
exec是特殊的加载器,当要运行软件时,可以直接将进程读取进内存。
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6
7 int main()
8 {
9 pid_t id = fork();
10 if(id == 0){
11 //child
12 printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
13 execl("/usr/bin/ls", "ls", "-l", NULL);
14 exit(1);
15 }
16
17 int count = 3;
18 while(count){
19 printf("I am father, pid: %d\n", getpid());
20 sleep(1);
21 count--;
22 }
23 int status = 0;
24 //father
25 pid_t ret = waitpid(id, &status, 0);
26 if(ret > 0){
27 printf("child status -> sig: %d, code: %d\n", status&0x7F, (status >> 8) & 0xFF);
28 }
29 else{
30 printf("wait error!\n");
31 }
32 return 0;
}
这样让创建子进程后让子进程去执行新的程序(没有创建新进程),父进程,父进程得到结果,检测命令并回收子进程的退出信息
这里的退出码code是子进程的退出码,但是是执行完ls后的退出码。
其它接口:
int execv(const char *path, char *const argv[]);
将child中代码改为:
char *const my_argv[]={
"ls",
"-l",
"-a",
"-i",
NULL
};
execv("/usr/bin/ls", my_argv);
结果同上
命名理解:
l(list):表示参数采用列表
v(vector):参数用数组
p(path):有p自动搜索环境变量PATH
e(env):表示自己维护环境变量
execlp:
含有p:可以自动搜索环境变量PATH(系统的命令才可以找到,或者把自己的命令导入到PATH中),不用写路径
int execlp(const char *file, const char *arg, ...);
例如:
execlp("ls", "ls", "-l", "-a", NULL);
execlp("top", "top", NULL);
execvp类似,只不过后面是用一个指针数组传参
execle:
e:传入默认的或者自定义的环境变量给目标可执行程序
int execle(const char *path, const char *arg, ...,char *const envp[]);
20 char *const my_env[] = {
E> 21 "MYENV=helloworld!",
22 NULL
23 };
24 execle("./mycmd", "mycmd", NULL, my_env);
25 exit(1);
26 }
在exec_cmd中调用execl传入环境变量MYENV的值,mycmd中接收并打印出。如果单独运mycmd依旧为空。
exec_cmd能执行系统的命令,也可以执行自己写的命令。
如果想要跨语言之间耦合,如C语言想调C++的代码,就可以exec这样的程序替换。
execve:
int execve(const char *path, char *const argv[], char *const envp[]);
20 char *const my_argv[] = {
W> 21 "mycmd",
22 NULL
23 };
24 char *const my_env[] = {
W> 25 "MYENV=helloworld!",
26 NULL
27 };
28 execve("./mycmd", my_argv, my_env);
也可以将main函数中的环境变量参数env传入,但需要导出环境变量export MYENV=helloworld
。main函数可以获得这个环境变量,并把这个环境变量导给子进程。
1.什么是程序替换:通过exec系列的函数,让特定进程去加载磁盘中的其它程序,以达到运行的目的,期间不创建新的进程。
2.为什么要程序替换:子进程执行新的程序的需求。
3.如何进行程序替换:原理->进程地址空间的问题->磁盘换入程序到内存->对可执行程序的理解(exe 文件) exec*
4.后续:a.exec*只要返回了,就说明出错了。b.各种借口的理解:l,v,p,e
用户在命令行输入某些命令,交给shell解释器。shell解释器解释命令时,并不是自己解释,而是调用fork创建子进程,子进程再执行命令(其实就是OS执行命令),OS再把结果通过shell解释器返回给用户。而子进程是通过exec系列函数执行命令的。
#include
#include
#include
#include
#include
#include
#define NUM 128
#define SIZE 32
char command_line[NUM];
char *command_parse[SIZE];
int main()
{
while(1){
memset(command_line, '\0', sizeof(command_line));
printf("[ymz@myhost 我的shell]$ ");
fflush(stdout);
//1. 数据读取
if(fgets(command_line, NUM-1, stdin)){
command_line[strlen(command_line) - 1] = '\0';
//ls -a -l -i
//2. 字符串(命令行数据分析)
int index = 0;
command_parse[index] = strtok(command_line, " ");
while(1){
index++;
command_parse[index] = strtok(NULL, " ");
if(command_parse[index] == NULL){
break;
}
}
//3. 判断命令
//a. 内置命令
//b. 第三方命令
if(strcmp(command_parse[0], "cd") == 0 && chdir(command_parse[1]) == 0){
continue;
}
//4. 执行非内置命令
if(fork() == 0){
//子进程
execvp(command_parse[0], command_parse);
exit(1);
}
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
if(ret > 0 && WIFEXITED(status)){
printf("Exit Code: %d\n", WEXITSTATUS(status));
}
}
}
return 0;
}