天天看点

[ Linux ] 进程控制(上)----进程创建与进程终止

文章目录

  • ​​一、进程创建​​
  • ​​fork函数初识​​
  • ​​fork之后,操作系统做了什么?​​
  • ​​写时拷贝​​
  • ​​fork的常规用法​​
  • ​​fork调用失败的原因​​
  • ​​二、进程终止​​
  • ​​关于终止的正确认识​​
  • ​​关于终止的常见做法​​
  • ​​_exit函数​​
  • ​​关于终止,内核做了什么?​​

​正文开始!​

一、进程创建

fork函数初识

在Linux中fork函数是非常重要的函数,他从已存在进程中创建一个新进程。新进程称为子进程,而原进程为父进程。

[ Linux ] 进程控制(上)----进程创建与进程终止
[ Linux ] 进程控制(上)----进程创建与进程终止

返回值:子进程中返回0,父进程中返回子进程的id,出错返回-1。

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度
[ Linux ] 进程控制(上)----进程创建与进程终止

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且他们都运行到相同的地方。但每个进程都将可以开始他们自己的执行。

看下面代码理解:

#include<stdio.h>
  2 #include<unistd.h>
  3 
  4 int main()         
  5 {
  6     printf("我是一个进程,pid:%d\n",getpid());
  7     fork();
  8     printf("我依旧是一个进程:pid:%d\n",getpid());
  9     sleep(1);   
      return 0;
    }      
[ Linux ] 进程控制(上)----进程创建与进程终止

这里我们看到三行输出,一行before,两行after。进程5034先打印before消息,然后他又打印after。另一个after消息由5035打印的。注意到进程5035没有打印before,为什么呢?如下图所示

[ Linux ] 进程控制(上)----进程创建与进程终止

所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意fork之后,谁先执行完全由调度器决定。

那么fork之后,是否只有fork之后代码是被父子进程共享的??

一般情况下,fork之后父子进程共享所有的代码!!

子进程执行的后续代码!=共享的所有代码,只不过子进程只能从这里开始执行!!

那么这是为什么呢?

[ Linux ] 进程控制(上)----进程创建与进程终止

因为CPU中有一个eip,会保存正在执行指令的下一条指令!eip程序计数器会拷贝给子进程,子进程便从该eip所指向的代码处开始执行!!!

fork之后,操作系统做了什么?

进程=内核的进程的数据结构+进程的代码和数据

创建子进程的内核数据结构(struct task_struct+struct mm_struct+页表)+代码继承父进程,数据以写时拷贝的方式,来进行共享或者独立!

写时拷贝

通常情况下,父子代码共享,父子在不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自有一份副本。具体如下图:

[ Linux ] 进程控制(上)----进程创建与进程终止

写时拷贝本身就是由OS的内存管理模块完成的!

那么为什么要写时拷贝呢?

创建子进程的时候,就把数据分开不行吗???

1.父进程的数据,子进程不一定全用,即便使用,也不一定全部写入。—会有浪费空间的嫌疑

2.最理想的情况,只有会被父子进程修改的数据,进行分离拷贝。不需要修改的共享即可—但是从技术角度实现复杂

3.如果fork的时候,就无脑拷贝数据给子进程,会增加fork的成本(内存和时间)

所以最终我们采用写时拷贝!!!(只会拷贝父子修改的,变相的就是拷贝数据的最小成本)

写时拷贝本质上是一个延迟拷贝策略。只有真正使用的时候,才给你!

就意味着你想要,但是不立马使用的空间,先不给你,那么也就意味着可以先给别人!

综上,就是变相的提高了内存的使用率!

fork的常规用法

  • 一个父进程希望复制自己,使得父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

fork调用失败的原因

1.系统中有太多的进程了

2.实际用户的进程数超过了限制

二、进程终止

关于终止的正确认识

我们使用C/C++的时候,main使我们的入口函数,函数的结束我们经常会写return 0;语句!

a.return 0,给谁return

根据下面的论述我们可以得出return是给父进程的!

b.为何是0?其他值可以吗?

答案是:进程代码跑完,结果是否正确

0:表示success

非零:表示失败

但是当我们失败的时候,我们最想知道的是失败的原因是什么?

所以我们用非零标识不同的原因!

首先我们先来了解常见的进程退出:

1.代码跑完,结果正确

2.代码跑完,结果不正确

3.代码没跑完,程序异常了

我们将return X;X称为进程退出码!表征进程退出的信息!得出的信息是要让父进程读取的!

先来看代码!

1 #include<stdio.h>
  2 #include<unistd.h>
  3 
  4 int main()
  5 {
  6 
  7     return 123;                                                                                            
  8 
  9 }
 10      

我们可以看到程序只有一行代码,直接运行完成就退出了!

根据我们之前的学习,我们可以直到命令行的父进程就是bash进程

我们可以用当前指令查看退出码

echo $?
[ Linux ] 进程控制(上)----进程创建与进程终止
[ Linux ] 进程控制(上)----进程创建与进程终止

$?代表在bash中,最近依次执行完毕时,对应进程的退出码!

一般而言,失败的非零值我该如何设置呢??以及默认表达的含义?

1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<string.h>
  4 
  5 int main()
  6 {
  7     for(int i=0;i<100;i++)
  8     {
  9         printf("%d:%s\n",i,strerror(i));
 10     }
 11   return 0;
 12}      
[ Linux ] 进程控制(上)----进程创建与进程终止
[ Linux ] 进程控制(上)----进程创建与进程终止
[ Linux ] 进程控制(上)----进程创建与进程终止
[ Linux ] 进程控制(上)----进程创建与进程终止
[ Linux ] 进程控制(上)----进程创建与进程终止

错误码退出码可以对应不同的错误原因,方便定位问题!

关于终止的常见做法

1.在main函数中return。为什么其它函数不行呢?

2.在自己的代码任意地点中,调用exit()函数!

man 3 exit
[ Linux ] 进程控制(上)----进程创建与进程终止
1 #include<stdio.h>
    2 #include<unistd.h>
    3 #include<string.h>
    4 
    5 void func()
    6 {
    7 
    8     printf("hello func\n");
E>  9     exit(111);
   10 
   11 
   12 }
   13 int main()
   14 {
   15 
   16     func();                                                                                              
   17     return 10;
   18 
   19}      
[ Linux ] 进程控制(上)----进程创建与进程终止

_exit函数

[ Linux ] 进程控制(上)----进程创建与进程终止

exit终止进程会刷新缓冲区

_exit直接终止进程,不会有任何刷新操作!

其实exit底层也是在调_exit函数。

[ Linux ] 进程控制(上)----进程创建与进程终止

关于终止,内核做了什么?

进程= 内核数据结构 + 进程代码和数据