赞
踩
对于前面的地址空间的学习,我们现在了解到原来所谓变量的地址其实是虚拟地址,该虚拟地址是通过页表的映射关系从而找到内存中真实的物理地址!下面我们进入到关于进程的控制
现在我们对进程就有了新的定义:进程 = 内核的相关管理数据数据结构(task_struct + mm_struct + 页表) + 代码和数据。 其中代码是父子进程共享的,数据是判断是否发生写实拷贝的。
fork函数的返回值
我们之前也有过介绍,fork函数是用来创建子进程的,创建子进程成功则返回0,对于父进程的返回值则是子进程的pid,这一点虽然我上次没有细讲,但是在截图时就会发现。可以看看我之前博客——>进程理解
为什么父进程返回的是子进程的pid,给子进程返回的却是0呢?
要记住,我们在讲解进程状态的时候,对于僵尸进程我们有过介绍,父进程是会管理子进程的,谈到管理永远是6个字——>”先描述,再组织“。
所以当然是为了让父进程方便对子进程进行标识,进而进行管理!
fork函数的常规用法
fork调用失败的原因
进程终止是在做什么呢?
首先我们应该清楚一件事情——>当加载进程是,应当是先创建PCB、页表和地址空间等,再是加载代码和数据。
所以进程终止是在:
- 释放曾经的代码和数据所占据的空间。
- 释放内核数据结构(若task_struct受僵尸状态,则演示释放)
进程终止的3种情况
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("I am a process! pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
return 100;
}
[!TIP]
我们可以使用指令:echo $?
该指令代表父进程bash获取到子进程的退出码,0表示成功,非0表示失败。退出码的意义就是告诉关心方(父进程),我把任务处理的怎么了。
但我要是再次执行echo $?指令,表示的就是刚刚子进程的退出码,毕竟该指令是获取最近进程的退出码。
那这个退出码究竟有什么用,你想返回100或者0不是都可以吗。诶,这个时候操作系统会给我们提供个新的系统调用函数:strerror函数用来获取系统错误信息或打印用户程序错误信息,下面我们来用用这个系统调用
[!TIP]
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <string.h> int main() { int errorcode = 0; for(errorcode = 0; errorcode <= 255; ++errorcode) { printf("%d -> %s\n", errorcode, strerror(errorcode)); } return 100; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
我们会打印特别多的错误码,每个错误码都对应了一个描述。所以为什么我们最后都会返回0,就是因为如果全部代码都执行完毕后,那肯定是不会存在问题的,那么就返回0。如果其中有一处有错误,系统就会直接进行返回相应的错误码。
这里是我随便ls一个文件夹,因为在当前目录未找到该文件夹,所以执行该指令的进程就会向bash返回相对应的错误码2。
所以我们在输入指令的时候,本质也是OS创建子进程然后子进程在执行。这点我们通过指令echo $?可以很好的证明。
这也能很好地说明bash为什么要得到子进程的退出码,为了知道子进程退出的情况(是否成功,失败又是什么原因),当然bash只是提供信息,不会自动解决,这只是一种为用户负责的体现
我们也可以实现自定义退出码
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <string.h> enum { ERROR_DIV = -1, NOMARL_DIV }; int exit_code = NOMARL_DIV; int Div(int a, int b) { if(b == 0) { exit_code = ERROR_DIV; //printf("This is error!\n"); return exit_code; } exit_code = NOMARL_DIV; return a/b; } const char* ErrorMode(int mode) { switch(mode) { case ERROR_DIV: return "Div zero"; case NOMARL_DIV: return "Div nomarl"; default: return "Unknow error!"; } } int main() { int ans = Div(4, 0); printf("ans is-> %d[%s]\n", ans, ErrorMode(exit_code)); ans = Div(4, 2); printf("ans is-> %d[%s]\n", ans, ErrorMode(exit_code)); return 0; }
通过判断分母是否为0,从而实现判断结果是否是正确的!
退出信号
int main()
{
int* p = NULL;
*p = 3;
printf("%d\n", *p);
return 0;
}
对于上述代码,我们执行起来百分之百会出现报错,如果我们在VS上也写过这样的bug,那么该错误就是一个很典型的——段错误
这个时候代码不会跑完,因为在执行的过程中出现了异常,就提前退出了。就像在VS写代码时,代码崩溃了,此时OS发现你的进程做了不该做的事情,OS就会杀掉进程,一旦出现了异常,退出码就没意义了。
比如此时我使用指令echo $? 打印退出码,是会发现退出码为139,但是退出码在133后就是未知了:
为什么会出现异常呢?
本质上是因为进程收到了OS发给进程的信号。
我们可以使用指令:kill -l
段错误的出现就是对应的11号信号
所以衡量一个进程的退出,我们(父进程bash)只需要两个数字:退出码 + 退出信号
第一步是先确认是否异常
若不是异常就一定是代码跑完了,看退出码就好了。
[!IMPORTANT]
所以进程终止的3种情况:
- 代码跑完,结果正确
- 代码跑完,结果不正确
- 代码执行时,出现了异常,提前退出了
exit( ) VS _exit( )
exit( )是库函数,_exit( )是系统调用
exit函数会在进程退出的时候,重置缓冲区,而 _exit( )不会,因此我们可以猜测缓冲区是在exit那一层,也可以说明使用exit( )库函数的本质是在调用系统调用函数 _exit( ).
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Hello, this is a test code!");
exit(0);
}
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Hello, this is a test code!");
_exit(0);
}
任何进程,在退出的情况系,一般必须要被父进程进行等待!
为什么父进程要等待?
该怎么进行等待?
我们可以使用系统调用函数:wait( ) 和 waitpid( ) 函数
#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int* status);
pid_t waitpid(pid_t pid, int* status, int options);
wait( )
返回值:成功返回被等待进程的pid,失败返回-1.
参数:输出型参数,获取子进程退出状态,不关心则可以设置成NULL
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <sys/wait.h> void ChildRun() { int cnt = 1; while(cnt <= 5) { printf("%d-> I am child process, pid: %d, ppid: %d\n", cnt, getpid(), getppid()); sleep(1); cnt++; } } int main() { printf("I am process!\n"); pid_t id = fork(); if(id == 0) { // child ChildRun(); printf("Child quit...\n"); exit(123); } // father pid_t rid = wait(NULL); if(rid > 0) { printf("wait success, rid: %d\n", rid); } else { printf("wait failed!\n"); } sleep(2); return 0; }
最后是等待成功并且返回了子进程的pid
waitpid( )
回收子进程资源,解决僵尸问题的同时,还能够获取子进程退出信息。
返回值:
参数:
pid_t pid
int* status
int options
指定父进程的等待方式,为 0 则让父进程进行 阻塞 等待,非 0 则进行 非阻塞 等待。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <sys/wait.h> void ChildRun() { int cnt = 1; while(cnt <= 5) { printf("%d-> I am child process, pid: %d, ppid: %d\n", cnt, getpid(), getppid()); sleep(1); cnt++; } } int main() { printf("I am process!\n"); pid_t id = fork(); if(id == 0) { // child ChildRun(); printf("Child quit...\n"); exit(123); } // father int status = 0; pid_t rid = waitpid(-1, &status, 0); if(rid > 0) { printf("wait success, rid: %d\n", rid); } else { printf("wait failed!\n"); } sleep(2); printf("father process quit, status: %d\n", status); return 0; }
最后的运行结果status是31488,这么奇怪的数字。
分析status
我们介绍过,status是输出型参数,记录了该进程的退出码 + 退出信号,但是我们应该如何利用一个值记录退出码和退出信号呢?
[!NOTE]
我们都知道,一个int* 变量的大小是4字节,那么也就是32个比特位,但status不能简单的当做int 来看待,可以当做位图来看,我们只研究低16比特位*
分为两种情况:
- 正常终止时:高8位为退出码信息,低8位默认全部为0
- 被信号杀掉时(出现异常):高8位不再使用,低7位存储终止信号,中间还有一位存core dump标志位
红色各段就代表着 退出码 + 退出信号,退出码拥有8个比特位,而终止信号拥有7个比特位
所以我们上述的31488其实是两个数据的整合,但是我们也可以打印出来看看。
我们需要将退出码先挪动到低8位,再转换为10进制,所以我们可以用位运算操作符:
打印退出码:(status >> 8) & 0xFF
打印退出信号:status & 0x7F
printf("exit_code: %d, exit_signal: %d\n", (status >> 8)&0xFF, (status & 0x7F));
- 1
最后的输出结果:
因为我的子进程最后exit了123,又因为这是正常终止,所以返回退出码接收到了123.
但如果我加一个段错误,再运行就会如下图所示:
此时是异常退出,status就会收到退出信号而非退出码。
我们也可以利用两个宏,来查出退出码:
if(WIFEXITED(status))
{
printf("child process quit success, chid exit code: %d\n", WEXITSTATUS(status));
}
else
{
printf("child process quit unnormal!\n");
}
我们上面讲的等待过程是属于阻塞等待,还记得我们在进程状态部分讲解过阻塞态吗,还记得阻塞态是会进入等待队列的吗?如果你有疑问的话可以去看看我之前写的博客:进程的祖册、挂起和运行状态。
那我们刚刚介绍waitpid( )系统调用时,也介绍了参数option是控制关于非阻塞等待的,如果参数option为0,那就是阻塞等待,父进程会等待子进程结束,再进行父进程的操作,这是阻塞等待。
非阻塞等待是父进程在等待子进程的过程中,父进程同时也在做某些操作,这就是非阻塞等待。
[!NOTE]
我们可以使用宏:WNOHANG 来表示父进程进入非阻塞等待。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <sys/wait.h> void ChildRun() { int cnt = 1; while(cnt <= 5) { printf("%d-> I am child process, pid: %d, ppid: %d\n", cnt, getpid(), getppid()); sleep(1); cnt++; } } void DoOtherThing() { printf(" I am doing my father things while child process is running!\n"); } int main() { printf("I am a process, pid: %d, ppid: %d\n", getpid(), getppid()); pid_t id = fork(); if(id == 0) { //child ChildRun(); printf("child process quit...\n"); sleep(1); exit(0); } //father int status = 0; while(1) { pid_t rid = waitpid(0, &status, WNOHANG); if(rid == 0) { printf(" Just checking child process...\n"); DoOtherThing(); sleep(1); } else if(rid > 0) { printf("wait suceess!\n"); break; } else { printf("wait failed!\n"); break; } } if(WIFEXITED(status)) { sleep(1); printf("father process quit success!\n"); printf("exit_code: %d, exit_signal: %d\n", WEXITSTATUS(status), status&0x7F); } else { printf("quit unnormal!\n"); } return 0; }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
通过以上代码,我们就能实现子进程运行的同时,父进程也在运行自己的任务:
现在我们已经学会了有关进程创建的话题,接下来我们将要讨论进程替换的话题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。