一、页表&&写时拷贝的进一步理解
页表中不仅仅只有虚拟地址到物理地址的映射,还包括了很多选项,其中就包括了映射条目的权限。当我们进程的代码和数据加载到内存并和进程地址空间建立映射关系时,如果数据的内容不允许被修改(比如说常量字符串),对应数据在页表中的映射条目的权限就会被设置为'r',表示该数据是只读的,不能被修改。这就是为什么当我们要对常量字符串的内容做修改程序运行阶段会报错的底层原因。
通过页表的权限位,我们也可以很好地解释写时拷贝是如何做到的。当父进程创建子进程时会发生写时拷贝,写时拷贝会把大部分映射条目的权限都由‘rw’设置成‘r’。当子进程或者是父进程要对数据进行修改时,发现要修改数据对应的映射条目的权限位‘r’,无法进行修改,这时操作系统就会介入。操作系统发现子进程(假设是子进程要对数据进行修改)要对数据进行修改,且操作时合法的,这是就会在内存中申请一块空间重新建立映射关系,再将父子进程该对应的映射条目都改成‘rw’,这才算完成了一次写时拷贝。通过以上过程操作系统就可以按需进行写时拷贝。
二、进程终止
main函数的返回值我们叫做进程的退出码。一般0表示进程执行成功,非0表示进程执行失败。可以用非0的数字表示进程失败的原因。错误码可以转换成错误描述,可以使用语言和系统自带的方法进行转化,也可以自定义。其他函数错误码仅仅表示函数调用结束。
2.1、echo $?
bash进程会记录最近一个进程退出的退出码,可以查看echo $?可以查看最近一个进程退出的退出码。
调用函数我们通常想看到两种结果,一是函数的执行结果(比如说fopen打开文件,打开成功就返回文件指针,打开失败返回NULL,这叫函数的执行结果),二是函数的执行情况(比如说同样是fopen打开文件,我们函数的执行情况对应的数字会被保存在errno(错误码)这个变量中)。
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> int main() { FILE* fp = fopen("./log.txt", "r"); printf("%d %s\n", errno, strerror(errno)); return 0; }
错误码和main函数的退出码本质上是一样的。 错误码只会记录系统提供的函数的执行情况。
2.2、进程退出的场景
进程提出的场景与三种:
1、进程代码执行完了,结果是正确的。
2、进程代码执行完了,结果不正确。
3、进程代码没有执行完,进程出异常了。进程出异常时,进程的退出码是没有意义的。
第三种情况进程出异常是进程收到了操作系统发出的异常信号,每个信号都有不同的编号,不同的信号编号表示异常的原因。
任何进程最终的执行情况我们都可以用两个数字来表示。一个是进程的退出信号(exit_signal)(退出信号为0表示进程没有出异常),一个是进程的退出码(exit_code)。进程的退出信号是由操作系统发送给进程,以指示进程应该终止或进行某种操作的信号,是上对下的操作。而进程的退出码则是当进程结束运行时返回给操作系统的一个整数值,用于表示进程的执行状态或结果,是下对上返回的结果。
2.3、exit
exit就是用来终止进程的,exit括号中的内容就是进程的退出码。在我们的代码进程中,在任意地方调用exit都表示进程退出。
三、进程等待
3.1、进程等待的必要性
1、之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
2、另外,进程一旦变成僵尸状态,那就刀枪不入,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
3、最后,父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果对还是不对,或者是否正常退出。
4、父进程通过进程等待(wait)的方式,回收子进程资源,获取子进程退出信息
3.2、进程等待的方法
3.2.1、wait方法
wait方法里的参数为输出型参数,可以设置为NULL。调用wait函数父进程默认进行阻塞等待,会等待任意一个子进程退出。等待成功,wait会返回子进程的pid,等待失败返回小于0的值。
fork之后父子进程谁先运行不确定,但fork之后一定是父进程后退出,因为父进程要回收子进程。
3.2.2、waitpid方法
以上pid参数为要回收子进程的pid(pid如果为-1,表示等待任意一个子进程,与wait等效),wstatus参数同样为输出型参数,可以设置为NULL(也可以设置为一个int变量的地址,可以查看子进程的退出码), options参数设置为0表示阻塞等待,设置为宏 WNOHANG表示非阻塞等待。采用非阻塞的方法等待,子进程退出成功返回子进程的pid,子进程还在继续自己的工作返回0,子进程出错返回小于0的数。阻塞等待时父进程会阻塞在waitpid这里一直等待子进程返回,非阻塞等待采用轮询的方法查看子进程的退出信息,在轮询的间隙父进程可以继续做别的工作。
3.2.3、wstatus参数详解
*wstatus表示一个int整形变量,由三十二个比特位组成,其中前16个比特位我们不用,第17到第24个比特位用来表示进程退出时的退出码,第26到第32个比特位用来表示进程退出时收到的退出信号。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0) { // child int cnt = 5; while(cnt) { printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(1); cnt--; } exit(1); } int status = 0; pid_t rid = waitpid(id, &status, 0); // 阻塞等待 if(rid > 0) { printf("wait success, rid: %d, status: %d\n", rid, status); } return 0; }
这就可以解释为什么上面的status变量为256了。因为子进程的退出码为1,status的第24个比特位被设置为1,没有收到退出信号,所以status后8个比特位都为0,所以status等于2的8次方等于256。
3.2.4、使用位操作从status变量中提取出进程的退出信号和退出码。
int main() { pid_t id = fork(); if(id == 0) { int cnt = 5; while(cnt) { printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(1); cnt--; } exit(1); } int status = 0; pid_t rid = waitpid(id, &status, 0); // 阻塞等待 if(rid > 0) { printf("wait success, rid: %d, status: %d, exit_signal: %d, exit_code: %d\n", rid, status, status&0x7f, (status>>8)&0xff); } return 0; }
3.2.5、使用宏获取进程的退出码
一般用户不是很关心进程的退出状态只想获取子进程的退出码就可以使用这种方法。
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)。
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)。
int main() { pid_t id = fork(); if(id == 0) { int cnt = 5; while(cnt) { printf("Child is running, pid: %d, ppid: %d\n", getpid(), getppid()); sleep(1); cnt--; } exit(1); } int status = 0; pid_t rid = waitpid(id, &status, 0); // 阻塞等待 if(WIFEXITED(status)) { printf("wait success, rid: %d, status: %d, exit_code: %d\n", rid, status,WEXITSTATUS(status)); } return 0; }