【Linux】进程控制 (万字详解)—— 进程创建 | 进程退出 | 进程等待 | 程序替换 | 实现简易shell(上)

简介: 【Linux】进程控制 (万字详解)—— 进程创建 | 进程退出 | 进程等待 | 程序替换 | 实现简易shell(上)

一. 进程创建


🌍回忆fork


在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进 程,而原进程为父进程。📌fork不懂的可以去这篇博客fork初始看看


#include <unistd.h>
pid_t fork(void);
//返回值:子进程返回0,父进程返回子进程id;创建失败返回-1


⚡面试题:请你描述一下,fork创建子进程,操作系统都做了什么?


1️⃣系统多了一个进程,此进程分配有对应的PCB结构体、地址空间、页表

2️⃣并将自己进程的代码和数据(从父进程中拷贝)加载到内存中,构建映射关系

3️⃣将该进程的PCB放入运行队列里,等待调度

4️⃣一旦开始调度,通过虚拟地址空间➕页表找到相关代码按照顺序语句等执行

0a2653c851af460fa595bd959398a8f1.png

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


fork之后,代码共享是after之后的还是全部代码共享?


虽然子进程是从after之后往后,但全部代码都是共享的

⚡那么为什么子进程是从fork之后开始执行,而不是before开始?


因为进程随时可能被中断,下次回来,还必须从之前位置继续运行,就要求CPU必须随时记录下,当前进程执行的位置,所以CPU内有对应的寄存器EIP,用来记录当前进程的执行位置!

寄存器在CPU内,只有一份,寄存器的数据是可以有多份的 —— 上下文数据


🌍虽然父子进程各自调度,各自会修改EIP,但是因为子进程已经认为自己的EIP起始值就是fork之后的代码!


0a2653c851af460fa595bd959398a8f1.png


所以子进程是从after开始跑,但并不代表之前的代码看不到!


创建子进程,给子进程分配对应的内核结构,必须子进程自己独有,因为进程具有独立性!理论上子进程也要有自己的代码和数据!可是一般而言,我们没有加载的过程,也就是说,子进程没有自己的代码和数据!!所以,子进程只能“使用”父进程的代码和数据!


代码:都不可以被写,只能读取,所以父子共享

数据:可能被修改,必须分离!


🌍为什么OS选用写时拷贝 ?


那么数据在创建进程时候就直接拷贝分离吗?


可能拷贝子进程根本用不上的数据,即便用得上也只是读取 ———— 空间浪费

举个例子:


const char *str = "aaa";
const char *str2 = "aaa";
printf("%p\n", str);
printf("%p\n", str2);


打印出来的是同一块地址!编译器在编译程序时候都知道节省空间,你觉得OS不会吗?


OS为何选择了写时拷贝,来将父子进程的数据进行分离?


一般而言即便是OS,也无法提前知道哪些空间可能会被写入!

用的时候,再给你分配,是一种延时申请技术,可以提高整机内存的使用率

ps:string,深浅拷贝底层也是写实拷贝实现的

0a2653c851af460fa595bd959398a8f1.png


父/子修改数据时,会发生缺页中断:OS再开辟一段空间,把数据拷贝过来(写时拷贝),重新建立映射关系;父子分开,更改读写权限。这时候再进行写操作。这样保证了父子进程的独立性。


🌍fork的用法 & 调用失败的原因


⚡fork用法


父子进程执行不同代码段。例如,父进程等待客户端请求,生成子进程来处理请求。

一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数(下面详说哦)

⚡fork 调用失败的原因


系统中有太多进程时,资源不足

用户创建的进程数超出了限制,为了防止某些用户恶意创建


二. 进程终止


💦进程退出场景

1️⃣代码运行完毕,结果正确

2️⃣代码运行完毕,结果不正确

3️⃣代码异常终止, 崩溃了

思考:为什么main函数总会return 0,意义何在?


并不是总是0, main函数的return的值就是进程退出码,返回给上一级进程,用来评判该进程执行结果


❗查看最近一次进程退出时的退出码 ——来衡量代码跑完对不对的


echo $?  查看最近一个程序的退出码 
————————————————————————————————————————————————————————————————————————————————————————————————
代码运行完毕,结果正确    - 0:   success
代码运行完毕,结果不正确  - !0:  failed 
代码异常终止    - 程序崩溃 → 退出码没有意义,return都不会跑(可以通过某种方式获得原因,进程等待详谈)


0a2653c851af460fa595bd959398a8f1.png


代码运行完毕,结果正确:返回0

代码运行完毕,结果不正确:返回非0

返回非0值,这是因为结果错误有多种可能,通过错误码获得对应错误信息字符串,比如我们可以用strerror来查看 ——


2d65d23f6d4748949b924e4057485923.png


#include<stdio.h>
 #include<string.h>
 int main()
 {
   int i=0;
   for(i=0;i< 150;i++)
   {
     printf("%d:%s\n",i,strerror(i));                                                                  
   }                                                                                  
   return 0;                                                                          
  }


运行结果如下——

0a2653c851af460fa595bd959398a8f1.png

以上的退出码是系统给我们提供的,我们可以使用这些退出码,但是如果想自己定义,也可以自己设计一套退出方案!

2d65d23f6d4748949b924e4057485923.png

这个没有错,自定义设为1了


3️⃣程序崩溃

程序运行出错,崩溃 —— 存在野指针


#include<stdio.h>
int main()
{
   printf("hello world\n");
   printf("hello world\n");
   printf("hello world\n");
   int *p =NULL;
   *p=1234;//野指针
   printf("hello world2\n");
   printf("hello world2\n");
   printf("hello world2\n");
   return 0;
}


⚡程序崩溃时,退出码是没有意义的,(好比你作弊了,老师还会在意你的分数吗?),一般而言退出码对应的return语句,没有被执行


0a2653c851af460fa595bd959398a8f1.png


💦退出进程方法

🌈return 退出

main函数内的return返回代表进程退出;非main函数return代表函数返回


return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数


🌈exit

📌 exit在任意地方调用,都代表终止进程,参数是退出码。


#include <unistd.h>
void exit(int status);

0a2653c851af460fa595bd959398a8f1.png


🌈_exit

在之前的进度条代码,我们就知道显示器是行刷新的,即\n进行刷新

2d65d23f6d4748949b924e4057485923.png


🌈exit 和 _exit 区别

我们发现_exit是直接终止进程,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程


0a2653c851af460fa595bd959398a8f1.png


🌈进程异常退出

1️⃣向进程发生信号导致进程异常退出:


发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等

2️⃣代码错误导致进程运行时异常退出:


代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。


三. 进程等待


⚡进程等待的必要性


子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏

进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程

对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何

所以父进程需要通过进程等待的方式,回收子进程资源,获取子进程的退出信息


⚡进程等待的方法


➰wait

0a2653c851af460fa595bd959398a8f1.png


#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);


返回值: 等待成功,返回被等待进程pid;等待失败,返回-1

下面写一段代码来验证:回收僵尸进程的问题


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
  pid_t id = fork();
  if(id < 0)
  {
   perror("fork");
   exit(-1);//表示进程运行完毕,结果不正确
  }
  if(id == 0){
  //子进程
  int count = 5;
  while(count--){
    printf("cnt: %d, 我是子进程,pid:%d,ppid:%d\n",cnt, getpid(), getppid());
    sleep(1);
  }
  exit(0);
  }
  //父进程
  sleep(7);
  pid_t ret = wait(NULL);//阻塞式的等待!
    if(ret > 0)
    {
      printf("等待子进程成功,ret:%d\n",ret);
    }
  return 0;
}


我们可以使用以下监控脚本对进程进行实时监控:


while :; do ps ajx | head -1 && ps ajx | grep myproc | grep -v grep;sleep 1; echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"; done


wait回收了僵尸进程


2d65d23f6d4748949b924e4057485923.png


➰waitpid

#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);


返回值: 等待成功,返回被等待进程pid;等待失败,返回-1


pid:待等待子进程的pid,若设置为-1,则等待任意子进程

status:输出型参数,获取子进程的退出状态,不关心可设置为NULL

options:默认为0,表示阻塞等待

0a2653c851af460fa595bd959398a8f1.png


⚡通过status获取子进程退出信息


🥑位操作

status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只研究status低16比特位):


0a2653c851af460fa595bd959398a8f1.png


由此我们可以通过此来对status进行位操作来获取异常信号和退出码


exitCode = (status >> 8) & 0xFF; //退出码
exitSignal = status & 0x7F;      //退出信号


2d65d23f6d4748949b924e4057485923.png6de278e6d6694ce5bb08e7e842b7e74b.png


🔸 对于代码异常终止的:


除0错误异常终止


8ec4f2997fb246878c34ecd6d122b7c6.png


我们给子进程发送2号信号,把子进程提前干掉,此时可以看到退出码是无效的,退出信号即是我们发送的信号 ——


🥑宏


我们也可以通过一组不用进行位操作的宏来获取退出码、判断有无异常信号


WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

0a2653c851af460fa595bd959398a8f1.png


运行结果如下——(正常退出 vs 异常终止)


2d65d23f6d4748949b924e4057485923.png




相关文章
|
6天前
|
存储 Linux Shell
Linux:进程等待 & 进程替换
Linux:进程等待 & 进程替换
30 9
|
6天前
|
存储 Linux C语言
Linux:进程创建 & 进程终止
Linux:进程创建 & 进程终止
29 6
|
1天前
|
运维 Linux Shell
day02-Linux运维-系统介绍与环境搭建_硬件 系统核心 解释器shell 外围操作系统
day02-Linux运维-系统介绍与环境搭建_硬件 系统核心 解释器shell 外围操作系统
|
6天前
|
Shell Linux Perl
Linux|如何允许 awk 使用 Shell 变量
Linux|如何允许 awk 使用 Shell 变量
14 2
|
6天前
|
Unix Linux 调度
linux线程与进程的区别及线程的优势
linux线程与进程的区别及线程的优势
|
6天前
|
网络协议 Shell Linux
LabVIEW 在NI Linux实时设备上访问Shell
LabVIEW 在NI Linux实时设备上访问Shell
12 0
|
6天前
|
存储 安全 Linux
【Linux】详解进程通信中信号量的本质&&同步和互斥的概念&&临界资源和临界区的概念
【Linux】详解进程通信中信号量的本质&&同步和互斥的概念&&临界资源和临界区的概念
|
6天前
|
Linux C++
【Linux】详解进程程序替换
【Linux】详解进程程序替换
|
6天前
|
存储 Linux Shell
Linux:进程概念
Linux:进程概念
20 8