在上一篇文章中,我提到了关于进程的一些东西,说到了fork,以及僵尸进程,但是没有细说,关于fork的一些细节,还有僵尸进程会占用系统资源的问题该怎么解决之类的,今天这篇文章就来进一步说说,好了,我们进入正题。如果文章中有错误,请私信评论指出,感谢!
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include
pid_t fork(void);
//返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度
子进程返回0,
父进程返回的是子进程的pid。
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
对于这个问题,就需要对程序运行时在系统中执行的各个细节都有些了解。
首先,程序在内存中执行,具体执行到某一步的时候,对于cpu,他读取到的内存地址,是虚拟内存还是物理内存呢,答案是虚拟内存。
对于程序在系统中运行的时候,cpu是如何拿到程序的代码并执行的,这个时候就要捋清楚一个关系:
对于Linux系统:每一个进程都由PCB管理(具体详情查看上一篇文章),所以cpu只需要访问PCB就可以得到某个程序进程的相关内容。
但是这个时候cpu通过PCB拿到的其实是进程代码的虚拟地址,这个虚拟地址和物理地址之间的映射关系都记录在页表中,所以呢,cup每次拿到的虚拟地址需要通过页表才能访问到真正的程序代码。
所以说呢,每个进程都有自己的虚拟地址,所以对于fork出来的子进程,他为了保持进程的独立性,它会将父进程的某些东西拷贝下来,这其中包括父进程的虚拟空间和页表。
那子进程是不是原封不动的将父进程的程序地址空间拷贝给自己了呢,是的,那么父子进程的虚拟内存就是一摸一样的两份,同时指向的物理内存地址也都是一样的,也就是说,两个进程共享这这些资源。
那为什么子进程不将这些东西都深拷贝一份给自己,答案是没有必要,对于父进程的资源,子进程只读,或者不访问,父进程完全可以跟子进程共享,相反的,如果进程深拷贝,反而是对物理内存的一种浪费,
说到这里,就可以从字面上理解写时拷贝是啥意思了,就是说,子进程和父进程共享同一资源,对于两个进程来说,谁需要对某一数据进行写操作的时候,再去进行拷贝,拷贝出自己独立拥有的一份,进行写操作,这是一种延时拷贝。
#include
#include
#include
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
//执行结果
//child[3046]: 100 : 0x80497e8
//parent[3045]: 0 : 0x80497e8
上述代码的执行结果不难看出,两个进程的同一资源是相同的地址空间,但是修改完后,却又不同的值,通过虚拟空间就可以很好的解释这种现象,他们的资源虽然有着相同的地址,但时写时拷贝会进行深拷贝,这个时候虽然虚拟内存地址相同,但是通过各自的页表映射的物理内存地址已经不同,所以值也就不同。
同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
wait方法
#include
#include
pid_t wait(int*status);
//返回值:
//成功返回被等待进程pid,失败返回-1。
//参数:
//输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
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。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。如果不存在该子进程,则立即出错返回。
获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
进程的阻塞等待方式:
int main()
{
pid_t pid;
pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
} else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(257);
} else{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
printf("this is test for wait\n");
if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
//运行结果:
//[root@localhost linux]# ./a.out
//child is run, pid is : 45110
//this is test for wait
//wait child 5s success, child return code is :1.
进程的非阻塞等待方式:
#include
#include
#include
#include
int main()
{
pid_t pid;
pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
}else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(1);
} else{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if( ret == 0 ){
printf("child is running\n");
}
sleep(1);
}while(ret == 0);
if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
其实有六种以exec开头的函数,统称exec函数:
#include `
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[]);
int execve(const char *path, char *const argv[], char *const envp[]);
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define NONE_REDIR 0;
int redir_status = NONE_REDIR;
char* CheckRedir(char* start){
assert(start);
char* end = start + strlen(start) - 1;
while (end >= start) {
if (*end == '>') {
if (*(end - 1) == '>'){
//追加重定向
redir_status = APPEND_REDIR;
*(end - 1) = '\0';
end++;
break;
}
//输出重定向
redir_status = OUTPUT_REDIR;
*end = '\0';
end++;
break;
}
else if(*end == '<'){
//输入重定向
redir_status = INPUT_REDIR;
*end = '\0';
end++;
break;
}
else{
end--;
}
}
if (end >= start) {
return end;
}
}
//利用程序进程替换实现一个迷你shell
//思想就是,输入一个指令,以字符串的方式做解析,然后调用相应的指令文件,
int main() {
int index = 0;
char *args[64] = { 0 };//用来存储输入的指令
char instruct[1024] = {'\0'};
while (1) {
//清空命令接收数组
memset(instruct, '\0', sizeof(instruct) - 1);
cout << "[root@我的主机 ~]#";
//刷新输出缓冲区
fflush(stdout);
//获取用户输入的指令
cin.getline(instruct, sizeof(instruct),'\n');
instruct[strlen(instruct)] = '\0';
//判断指令是否有重定向
char *sep = CheckRedir(instruct);
//分割指令
args[0] = strtok(instruct, " ");
index = 1;
if (strcmp(args[0],"ls") == 0) {
args[index++] = "--color=auto";
}
if (strcmp(args[0], "ll") == 0){
args[0] = "ls";
args[index++] = "-l";
args[index++] = "--color=auto";
}
while (args[index++] = strtok(NULL, " "));
//如果是cd指令,那就执行切换目录的函数
if (strcmp(args[0], "cd") == 0){
if (args[1] != NULL) chdir(args[1]);
continue;
}
//创建子进程
size_t id = fork();
if (id == 0) {
//子进程
int fd = -1;
if (sep != NULL) {
switch(redir_status){
case INPUT_REDIR :
fd = open(sep,O_RDONLY);
dup2(fd,0);
break;
case OUTPUT_REDIR :
fd = open(sep, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd,1);
break;
case APPEND_REDIR :
fd = open(sep, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd,1);
break;
default:
cout << "bud?" << endl;
break;
}
}
execvp(args[0],args);
exit(1);
}
//父进程
int status = 0;
size_t ret = waitpid(-1, &status, 0);
if ( WIFEXITED(status) && ret==id ) {
//子进程成功执行:
cout << "return code:" << WEXITSTATUS(status) << endl;
}
else {
cout << "wait process failed!"<< endl;
return 1;
}
}
return 0;
}