我们已经学习完了Linux进程的概念,进程的存储空间等等问题,接下来就是学习如何使用进程和管理进程
文章目录
目录
前言
一、进程的创建
我们之前在进程的概念时,为了认识进程,我们曾经用过fork函数,我们知道fork可以创建进程
1、fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
通过man函数查看fork的相关返回值和功能
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程
将父进程部分数据结构内容拷贝至子进程
添加子进程到系统进程列表当中
fork返回,开始调度器调度
fork完以后,创建一个子进程,然后子进程写实拷贝,将父进程的代码和数据拷贝一份
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以 开始它们自己的旅程,看如下程序。
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ((pid = fork()) == -1)perror("fork()"), exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
这里看到了三行输出,一行before,两行after。进程8762先打印before消息,然后它有打印after。另一个after 消息有8762打印的。注意到进程8763没有打印before,为什么呢?如下图所示
通过这几张图就能说明,我们fork函数创建了一个子进程,然后子进程对父进程的代码和数据写时拷贝了,然后子进程和父进程一起向下运行,所以After:pid is这句话打印了两次。
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器 决定。
2、fork函数返回值
子进程返回0, 父进程返回的是子进程的pid。
3、写时拷贝
这里的写时拷贝就要先学习上一篇博客所提出来页表等概念
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副 本。具体见下图:
4、fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
后面会系统的学习exec等相关函数
5、fork调用失败的原因
系统中有太多的进程
实际用户的进程数超过了限制
二、进程终止
1.进程退出场景
代码运行完毕,结果正确
代码运行完毕,结果不正确
代码异常终止
这些都是导致进程终止退出的原因
2、进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
1. 从main返回
2. 调用exit
3. _exit
异常退出:
ctrl + c,信号终止
那么有一个问题为什么我们的main函数从学习c语言到现在main函数返回值一直都是0,为1,2,3....等等可以吗?
在我们学习了进程的概念我们知道每一个进程都有自己的pid和ppid,子进程的ppid是父进程,父进程的ppid是bash,这是基于不是子进程套子进程的前提下的结论。
所以当main函数的返回值不是0时也没有事,只是bash收到的就是不是0了。
3._exit函数
通过man文档查看_exit
我们看到它的头文件为 unistd.h,有一个参数status,返回值为空。
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值 是255。(一会我们会在下面详细解释为什么是255,和这个返回值问题)
4、exit函数
通过man文档查看exit
我们看到它的头文件为 stdlib.h,有一个参数status,返回值为空。
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
exit最后也会调用exit, 但在调用exit之前,还做了其他工作:
1. 执行用户通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入
3. 调用_exit
上代码演示二者的区别
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
int main()
{
cout<<"I am _exit\n";
_exit(0);
return 0;
}
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
int main()
{
cout<<"I am exit";
exit(0);
return 0;
}
运行结果:
问题来了,为什么_exit什么都没有打印就就是了进程,而exit却打印了呢?
我们写进度条和c语言的时候我们知道了打印函数是先加载到缓存区内,最后再一次性打印的,而_exit却什么都没有打印,那么这就和缓冲区有关系了。也就证明了_exit直接退出了进程,而exit是先将执行清理函数,然后将冲缓冲区关闭缓冲区,最后退出进程。
而exit打印了却没没有换行是因为没有加回车换行符。
return退出 return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返 回值当做 exit的参数。
三、进程等待
1、进程等待必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
2、进程等待的wait方法
通过man 3 wait查看wait的功能和头文件,参数,返回值
头文件sys/wait.h ,返回值pid_t ,参数整型指针
功能:存在于父进程当中,子进程结束后用来接收字节的pid,失败了返回-1。
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
3、进程等待的waitpid方法
通过man 3 waitpid指令查看waitpid的功能和头文件,参数,返回值
返回值:
当正常返回的时候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,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
4、获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特 位):
0000 0000 0000 0000 0000 0000 0000 0000 我们只用的上这16个字节,前8个比特位是返回的数值,后8个比特位是退出的状态,再前面的exit的参数和返回值中,我们让exit的参数位-1,返回值却是255的原因是因为-1的二进制数为 1111 1111 1111 11111 1111 1111 1111 1111 而255的二进制是0000 0000 0000 0000 0000 0000 1111 1111 ,当我们截取出前8位时,-1的二进制就和255一样了。而参数是整型,所以返回的就是255.
四、阻塞等待
我们知道了wait和waitpid是在父进程中用来接受子进程退出和返回值的。那么当子进程没有运行结束,父进程都在干什么呢?
代码解释:
#include<sys/wait.h>
#include<iostream>
#include<unistd.h>
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
using namespace std;
void runchild()
{
int cnt =5;
while(cnt)
{
cout<<"22222222222222222222 pid: "<<getpid()<<"ppid: "<<getppid()<<endl;
sleep(1);
cnt--;
}
}
int main()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
return 1;
}
else if(id==0)
{
//子进程
runchild();
exit(0);
}
else
{
int cnt=10;
//父进程
while(cnt)
{
cout<<"11111111111111111111 pid: "<<getpid()<<"ppid: "<<getppid()<<endl;
sleep(1);
cnt--;
}
pid_t ret= wait(NULL);
if(ret==0)
{
cout<<"0000000000000"<<endl;
}
else if(ret>0)
{
cout<<"1111111111"<<endl;
}
else
{
cout<<"-1"<<endl;
}
}
return 0;
}
运行结果:
现象:刚开始父进程和子进程一起运行,当子进程运行完5秒以后,子进程退出,退出值保存起来,父进程还没有运行完成,父进程运行完5秒后,父进程继续向下走,到了wait,去取取出了子进程结束时的pid。因为wait返回值为大于0的数,所以返回成功!
当子进程运行完以后子进程就成僵尸进程了,等待父进程回收,当父进程运行完wait以后将僵尸进程回收。
五、非阻塞等待
代码
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<sys/wait.h>
using namespace std;
void runchild()
{
int cnt=10;
while(cnt)
{
printf("11111111111111 pid:%d ppid:%d \n",getpid(),getppid());
cnt--;
sleep(1);
}
}
int main()
{
pid_t id=fork();
if(id<0)
{
perror("fork!!!");
sleep(1);
}
else if(id==0)
{
//child
runchild();
exit(1);
}
else
{
//prent
int cnt=3;
while(cnt)
{
printf("22222222222222 pid:%d ppid:%d \n",getpid(),getppid());
cnt--;
sleep(1);
}
//int status=0;
// pid_t ret= waitpid(-1,status,WNOHANG);
while(1)
{
int status=0;
pid_t ret= waitpid(-1,&status,WNOHANG);
if(ret==id)
{
printf("成功返回,返回值为:%d\n ",ret);
sleep(5);
break;
}
else if(ret<0)
{
sleep(1);
perror("wait");
break;
}
else
{
printf("好了吗子进程pid:%d 你爹等你呢pid%d\n",id,getpid());
sleep(1);
}
}
}
return 0;
}
运行结果:
现象:当父进程运行结束后,子进程还没有运行结束,父进程没有闲着,而是一直在问子进程结束没。
waitpid有三个参数,第一个参数填-1代表接收任何子进程,第二参数为status的指针;
第三个参数非常重要,代表着是阻塞等待还是非阻塞等待
非阻塞等待参数为:WNOHANG
阻塞等待参数为:0
进程的创建和进程的基本管理、询问以结束,接下来就是进程的替换!!!
标签:fork,控制,pid,exit,linux,进程,返回值,include From: https://blog.csdn.net/2301_80687320/article/details/143664638