柚子快报激活码778899分享:【Linux】进程控制
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
前言
一、进程创建
fork函数初识
fork 函数返回值
创建进程的时候,是先有内核的数据结构呢?还是先有进程的代码和数据呢?
写时拷贝
fork常规用法
fork调用失败的原因
二、进程终止
终止是在做什么吗
进程终止的三种情况
查看整个系统提供错误码的编号以及对应的含义
我们自定义描述一下退出码的信息
如何终止异常
三、进程等待
进程等待必要性
进程等待的方法
wait方法
waitpid方法
获取子进程status
进程的阻塞等待
进程的非阻塞等待(等待时,允许父进程做一些事情)
总结
前言
世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!
提示:以下是本篇文章正文内容,下面案例可供参考
一、进程创建
fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include
pid_t fork(void);
//返回值:子进程中返回0,父进程返回子进程pid,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
分配新的内存块和内核数据结构给子进程将父进程部分数据结构内容拷贝至子进程添加子进程到系统进程列表当中fork返回,开始调度器调度
当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
int main(void)
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
pid_t rid = fork();
if (rid == -1)
{
perror("fork()"),
exit(1);
}
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运行结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
这里可以看到,Before只输出了一次,而After输出了两次。其中,Before是由父进程打印的,而调用fork函数之后打印的两个After,则分别由父进程和子进程两个进程执行。也就是说,fork之前父进程独立执行,而fork之后父子两个执行流分别执行。
注意,fork之后,谁先执行完全由调度器 决定。
进程:内核的相关管理数据结构(task_struct + mm_struct + 页表) + 代码和数据
task_struct:进程控制块(描述进程)
mm_struct:进程地址空间(虚拟地址)
fork 函数返回值
子进程返回0父进程返回的是子进程的pid
fork()函数为什么会返回两个值?fork()函数最终return的时候,创建子进程的核心数据已经做完了,也就是说在执行fork()函数最终的return时,子进程和父进程都会被调度,return也是语句,往后的代码都会被共享,所以return被执行了两次。return的本质就是对进程当中进行写入。
为什么给父进程返回的是子进程的PID,给子进程返回的是0?给子进程返回的是0,是因为想要知道子进程有没有被创建成功,通过0来返回得知结果。 我们为了让父进程方便对子进程进行标识,进而进行管理。
创建进程的时候,是先有内核的数据结构呢?还是先有进程的代码和数据呢?
你高考完之后,考上报的大学了,那么你的方案袋会提前被录取你的学校拿走,所以, 你还没有报道的时候,你已经是这个学校的学生了,这就是新建进程;当你人去了学校以后,才是代码和数据。 同理:创建一个进程的时候,先创建内核的相关管理数据结构,再有进程的代码和数据。
命令行中启动的所有进程,都是bash的子进程。
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副 本。具体见下图:
fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
系统中有太多的进程实际用户的进程数超过了限制
二、进程终止
终止是在做什么吗
1、释放曾经的代码和数据所占据的空间2、释放内核数据结构
进程终止的三种情况
a、代码跑完,结果正确
b、代码跑完,结果不正确(正确与否:可以通过进程的退出码决定!)
c、代码执行时,出现了异常,提前退出了。
VS编程运行的时候,崩溃了 --- OS发现你的进程做了不该做的事情,OS杀了进程。一旦出现异常,退出码就没有意义了。
为什么进程会出异常?因为进程收到了OS发给进程的信号!
kill -11 pid // 11:SIGSEGV
我们可以看进程退出的时候,退出信号是多少,就可以判断我的进程为什么异常了!!!
我们看下面这一段代码:
#include
#include
int main()
{
printf("I am process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(2);
return 100;
}
echo $?
//能打印出100
在shell当中存在一个变量,这个变量名是?
echo 是内建命令,只会打印bash本身内部的变量数据?:父进程bash获取到的,最近一个子进程退出的退出码(内建命令虽然没有创建子进程,但是是由bash直接执行的,所以也会修改退出码),退出码告诉关心方(父进程),我把任务完成得怎么样了!退出码0:表示成功 退出码!0:表示失败
查看整个系统提供错误码的编号以及对应的含义
man strerror
//把一个错误码转化成所对应错误信息字符串的描述
#include
#include
#include
int main()
{
// 查看整个系统提供错误码的编号以及对应的含义
for (int errcode = 0; errcode <= 255; errcode++)
{
printf("%d: %s\n", errcode, strerror(errcode));
}
printf("I am process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(2);
return 100;
}
我们自定义描述一下退出码的信息
// 退出码可以使用默认,也可以使用自定义
// 我们自定义描述一下退出码的信息
// 自定义枚举常量
enum
{
Success = 0,
Div_Zero,
Mod_Zero,
};
int exit_code = Success;//全局变量
const char *CodeToErrString(int code)
{
switch(code)
{
case Success:
return "Success";
case Div_Zero:
return "div zero!";
case Mod_Zero:
return "mod zero!";
default:
return "unknow error!";
}
}
int Div(int x, int y)
{
if( 0 == y )
{
exit_code = Div_Zero;
return -1;
}
else
{
return x / y;
}
}
int main()
{
int result = Div(10, 100);
printf("result: %d [%s]\n", result, CodeToErrString(exit_code));
result = Div(10, 0);
printf("result: %d [%s]\n", result, CodeToErrString(exit_code));
return exit_code;
}
衡量一个进程退出,我们只需要两个数字:退出码,退出信号!
我们子进程的退出码或退出信号一定要让我们的父进程(bash)知道,子进程执行完自己的代码和数据,最后return或者异常,我们的子进程的task_struct结构体里面有许多属性,包括退出信息编号、退出码等;子进程退出时,会先将自己的代码和数据释放掉,但不能把自己的对应的PCB释放掉,要把自己的PCB维持一段状态,变成Z状态,让父进程来读取;当一个进程退出时,他会把进程执行的最终的退出信号或者退出码写入到子进程的PCB当中。
如何终止异常
a、main函数return,表示进程终止(非main函数,return,函数结束)
b、代码调用exit函数,注意:我们的代码的任意位置调用exit(),都表示进程退出
c、_exit() -- 系统调用接口,代码的任意位置调用_exit(),也可以表示进程退出
exit()和_exit()的区别:exit()会在进程退出的时候,冲刷缓冲区,_exit()不会。
int main()
{
printf("hello 111");
sleep(2);
exit(3);//会刷新缓存区,将内容打印出来
_exit(3);//不会刷新缓存区,即不会让printf()函数里的字符串内容打印到屏幕上
}
缓冲区不在exit、_exit和操作系统里面,exit在底层调用的就是_exit,用户没有权利对操作系统内的各种字段做任何访问,包括终止或释放一个进程,所以exit底层一定要调用_exit(系统调用接口),所以,如果缓冲区在操作系统内部,那么exit肯定能刷新出来,_exit也能刷新出来,因为exit和_exit是调用关系。所以,缓冲区只能在_exit之上,在_exit之上的话,那么_exit是看不到数据,自然也就刷新不出来数据。
杀掉进程/进程退出:本质是释放进程的代码和数据,释放除了PCB之外的其它数据结构,就是对进程做管理的一种方式。
三、进程等待
进程等待必要性
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
1、父进程通过等待,解决子进程退出的僵尸问题,回收系统资源(一定要考虑的);
2、获取子进程的退出信息,知道子进程是因为什么原因退出的(可选的功能)。
进程等待的方法
wait方法
//wait的两个头文件
#include
#include
// wait/waitpid:等待一个子进程的状态发生变化
// 函数的意义:等待父进程中,任意一个子进程的退出
pid_t wait(int* status)
//返回值是等待成功时,子进程的pid;失败返回-1
//参数:输出型参数,获取子进程退出状态,不关心则可以设置为NULL
#include
#include
#include
#include
#include
#include
void ChildRun()
{
int* p = NULL;
int cnt = 5;
while (1)
{
printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
}
}
int main()
{
printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0)
{
// child
ChildRun();
printf("child quit ...\n");
exit(0);
}
sleep(7);//父进程休眠时,子进程呈现僵尸状态
// fahter
pid_t rid = wait(NULL);//等待子进程的pid,僵尸状态消失
if (rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
}
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
当正常返回的时候waitpid返回收集到的是子进程pid;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
pid
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待指定子进程的pid。
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
查看signal位(终止信号),当前的终止信号是不是为0,如果为0,是正常结束的;非0,条件不满足。
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
options默认为0时,是阻塞等待
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。(非阻塞等待)
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。如果不存在该子进程,则立即出错返回。
第二个参数:输出型参数;需要你在你的代码当中定义一块内存空间,把空间的地址传进来,放入status指针变量中,然后在未来操作系统在等待时,它可以帮我们把子进程的退出信息,通过指针,带到你的用户层。子进程的退出信息。
int a;
scanf("%d ",&a);
// 在键盘上输入的数据,通过scanf()函数输出到你的a空间里。
获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特 位):
#include
#include
#include
#include
#include
#include
void ChildRun()
{
int cnt = 5;
while (1)
{
printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
cnt--;
}
}
int main()
{
printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0)
{
// child
ChildRun();
printf("child quit ...\n");
exit(123);// 123是退出码,自己定的
}
sleep(7);
// fahter
//pid_t rid = wait(NULL);
//pid_t rid = waitpid(-1, NULL, 0);
//如果waitpid函数里的参数是这些的话,那么和上面的wait函数作用一样,都是等待任何一个子进程
int status = 0;//初始化一下status
pid_t rid = waitpid(id, &status, 0);//等待指定子进程pid的返回值
if (rid > 0)
{
printf("wait success, rid: %d\n", rid);
}
else
{
printf("wait failed !\n");
}
sleep(3);
printf("father quit, status: %d, child quit code : %d, child quit signal: %d\n", status, (status >> 8) & 0xFF, status & 0x7F);
// 0xFF:16进制,转换成二进制是8个1,按位与一下,可以把退出状态当中唯一的保留为0的直接去掉
// 0x7F:01111111,按位与一下会把底16位中的前7位按位与出来
}
进程的阻塞等待
阻塞等待:我们讲的scanf(),进程在等待键盘资源就绪;操作系统要管理硬件,也要先描述,再组织,所以每种设备在内核当中都有属于自己的结构体描述对象的,描述结构体对象里面包含等待队列的,所以进程等待就是将自己的PCB列入到设备的等待队列里就可以了。
如何理解阻塞等待子进程呢?子进程本身就是软件,父进程本质是在等待某种软件条件就绪;当子进程没有退出,父进程就不被调度了,就是把父进程的状态设为S状态(非运行状态),把父进程的PCB列入到子进程的队列当中,此时,父进程就是在等待子进程。
什么叫进程阻塞呢?把进程的状态由R状态设为非R状态,比如:S状态;把进程的PCB从运行队列移到其它的等待队列当中,不被调度就可以了。
int main()
{
printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0)
{
// child
ChildRun();
printf("child quit ...\n");
exit(123);
}
sleep(7);
//fahter
//pid_t rid = wait(NULL); 等待任意一个子进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);// 阻塞等待指定的子进程
if(rid > 0)
{
// 子进程正常退出,没有异常(退出信号为0),为真
if(WIFEXITED(status))
{
// WEXITSTATUS:获取子进程的退出码
printf("child quit success, child exit code : %d\n", WEXITSTATUS(status));
}
else
{
printf("child quit unnormal!\n");
}
printf("wait success, rid: %d\n", rid);
}
else
{
printf("wait failed !\n");
}
sleep(3);
printf("father quit, status: %d, child quit code : %d, child quit signal: %d\n", status, (status>>8)&0xFF, status & 0x7F);
}
进程的非阻塞等待(等待时,允许父进程做一些事情)
非阻塞等待:
pid_t waitpid(pid_t pid,int *status,int options)
1、等待指定的进程;为-1时,等待任意进程;2、输出型参数;需要你在你的代码当中定义一块内存空间,把空间的地址传进来,放入status指针变量中,然后在未来操作系统在等待时,它可以帮我们把子进程的退出信息,通过指针,带到你的用户层。子进程的退出信息。3、默认为0时,就是阻塞等待; WNOHANG:宏,这个选项,以非阻塞的方式进行子进程的等待了。
返回值pid_t:
pid_t > 0 :等待成功了,子进程退出了,并且父进程回收成功。pid_t < 0 :等待失败了。pid_t == 0 :检测是成功的,只不过子进程还没有退出,需要你下一次进行重复等待。
非阻塞等待的时候 + 循环 = 非阻塞轮询
int main()
{
printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0)
{
// child
ChildRun();
printf("child quit ...\n");
exit(123);
}
LoadTask();
// father
while (1)
{
int status = 0;
pid_t rid = waitpid(id, &status, WNOHANG); // 非阻塞等待:子进程没有退出,直接返回0,进行下一次循环等待
if (rid == 0)
{
usleep(100000);
printf("child is running, father check next time!\n");
DoOtherThing();
}
else if (rid > 0)
{
// 没有异常,正常退出
if (WIFEXITED(status))
{
// 得到退出码
printf("child quit success, child exit code : %d\n", WEXITSTATUS(status));
}
else
{
printf("child quit unnormal!\n");
}
break;
}
else
{
printf("waitpid failed!\n");
break;
}
}
}
总结
好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。不积硅步,无以至千里;不积小流,无以成江海。
柚子快报激活码778899分享:【Linux】进程控制
推荐链接
发表评论