当前位置: 首页 > news >正文

【Linux】进程控制

进程的创建

创建函数:fork( ) 和 vfork( ) 函数,在linux中是非常重要的函数,它从已经存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

#include<unistd>
pid_t fork(void);
返回值: 给父进程返回子进程的pid,给子进程返回0,出错返回-1;

#include<sys/types.h>
#include<unistd.h>
pid_t vfork(void);
唯一与fork的区别就是调用vfork后;父子进程公用一个虚拟地址空间---为了避免调用栈出错,直到子进程执行完或者子进程进行程序替换后,父进程开开始执行;
  • 既然有个知道了创建子进程的函数,那么调用fork()函数后发送什么?
    答: 在前面我们已经知道,进程在其实在内核中就是PCB进程控制块,当一个程序被从磁盘加载到内存的时候,就会成为进程;在这背后其实是操作系统为该程序创建一个Task_struct结构体,页表,地址空间mm_struct等,这是程序也就成为了一个进程,在我们调用了fork()函数后,操作系统会以当前调用fork()函数的进程为模板创建一个新的进程,这个新创建的进程叫子进程 ,而那个作为模板的进程叫父进程;子进程同样具有Task_struct结构体、页表,地址空间mm_struct结构体,而且子进程的这些数据与父进程可以说基本上是一样的,只是修改的部分信息;
  • 将上述过程简化,调用fork()控制转移到内核中:
    答:(1)分配新的内存块和内核数据结构给子进程(2)将父进程部分数据结构内容拷贝至子进程(3)添加子进程到系统进程列表当中(4)fork返回,开始由调度器调度
    在这里插入图片描述
    注意:当调用fork()后,子进程被创建因为子进程是以父进程为模板创建的,其中由于地址空间mm_struct结构体的缘故,子进程和父进程具有相同的二进制代码,而且他们都运行到了相同的地方(这是由于Task_struct(PCB)的程序计数器决定),因为程序计数器:程序中即将被执行的下一条指令的地址;当fork()函数调用过后父子进程都可以作为两个自由独立的执行流运行;
//这个demo体会,fork创建子进程后,父子进程作为两个不同的执行流独立自由执行的特性
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
  pid_t pid = fork();
  if(pid < 0)
  {
    perror("fork error");
    return -1;
  }
 //情况一:
  //if(pid == 0)//child process
  //{
  //  printf("I am child,My pid :%d\n",getpid());
  //}
  //else//parent
  //{
  //  printf("I am parent,My pid :%d\n",getpid());
 // }  
	//情况二
	printf("I am child,My pid :%d\n",getpid());
	printf("I am parent,My pid :%d\n",getpid());
    return 0;
}

情况一:
在这里插入图片描述
情况二:
在这里插入图片描述
注意: 从代码就可以看出来,当父进程执行fork之后,子进程被创建,子进程具有和父进程相同的代码,而且从fork之后,父子进程从相同的地方开始执行执行,而fork给子进程返回0.父进程返回子进程pid,父子进程作为两个不同的执行流,根据这个代码开始分不同分支执行;得到不同结果;情况二在没有判断的情况下,会发现重复输出两次,这更加能说明父子进程代码是共享的,父子进程作为两个执行流同时执行的;但是: fork之后,父子进程执行的顺序,那完全是看调度器调度的;所以情况二中哪一个pid是父进程、哪一个pid是子进程的这就难易确定了;

写时拷贝
上面说到子进程以父进程为模板,父子进程的代码共享的,其实按照代码段的相似那么父子进程的数据段应该也是相同的;

//这个demo体会fork创建子进程后,父子进程代码共享,数据以写时拷贝的形式各自私有的特性
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

int main()
{
  int val = 100;
  pid_t pid = fork();
  if(pid < 0)
  {
    perror("fork error");
  }

  if(pid == 0)
  {
    //child process
	//val = 200;
    printf("child : %d val : %d  address : %p\n",getpid(),val,&val);
  }
  else
  {
    //parent process
    printf("parent : %d val : %d  address : %p\n",getpid(),val,&val);
  }
  return 0;
}

情况一:当子进程没有对数据进行修改
在这里插入图片描述
情况二:子进程对数据进行修改
在这里插入图片描述
注意:此时就有一点迷惑,子进程是以父进程为模板所创建的,那么父子进程的页表是相同的,此时父子进程因为通过页表映射的是同一块内存空间,那么数据是相同的。情况一就可以看出来;那么在情况二中当子进程对数据进行修改时,同一个变量值确实把不同的,这种二义性的情况是不允许在计算内面出现的,尤其是内存这一块;但是他们的地址确实相同的;这着实让人迷惑;来看一下fork后父子进程的内存映射情况:
在这里插入图片描述
这个时候就很容易解释了,在fork后,在子进程并没有对数据进行修改而只读的时候父子进程,父子进程通过相同的页表映射相同物理内存数据是共享的,但是在子进程对数据进行了修改时,那么此时便以写时拷贝的方式各自一份副本,有操作系统为子进程新开一块物理内存将父进程数据段代码拷贝一份过来,然后修改子进程页表数据段映射;然后对数进行修改就OK了;但是发现打印的地址还是一样的,注意:此时打印的地址时地址空间中的虚拟内存的地址,而不是真正的物理地址
结论:父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本,各自独有一份。
fork调用失败的原因
在我们调用fork函数创建子进程的时候,也会有失败的情况,是什么原因造成的?

  • 最重要就是可能当前进程数量超过操作系统所默认的最大进程数量;
  • 操作系统当前资源不足,不能完成创建任务;

查看当前系统允许用户创建最大进程数:ulimit -a命令查看
在这里插入图片描述

进程终止

常见的进程终止的场景:

  • 正常退出,结果预期
  • 正常退出,结果非预期
  • 代码异常终止

常见进程退出方法:
正常退出:

  • 在main函数中return;
  • 调用_exit()退出
  • 调用exit()退出

异常退出:

  • Ctrl + c 、信号终止

进程退出码: 在linux中进程退出之后会有一个退出状态,进程退出码就是来记录进程退出状态的,其范围是 0 ~ 255这是POSIX 1003.1标准规范化了;
echo $? :查看的是最近的一个命令退出时的退出码

//这个demo体会 echo$?命令查看进程退出码

#include<stdio.h>
#include<stdlib.h>

int main()
{
  printf("hello world\n");
  return 0;//正常退出,且结果预期
}

利用echo $?查看进程退出码:
在这里插入图片描述
exit()和_exit()函数

#include<unistd.h>
void _exit(int status);
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
虽然status是int型,但是只有的低8位被父进程所用,
在调用_exit时,并不会做善后工作直接退出进程

#include<stdlib.h>
void exit(int status) (推荐使用)不管在进程的任何地方调用该函数,该进程立马退出
调用exit时,exit会做好善后工作,最后调用清理函数关闭所有流,
所拥有的缓存数据均被写入;最后调用_exit()在退出

exit()_exit()的区别?
答:_exit()执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。
调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,
但不会刷新流(stdin, stdout, stderr ...). exit函数是在_exit函数之上的一个封装,
其会调用_exit,并在调用之前先刷新流。

exit()和_exit()的区别:
在这里插入图片描述

//这个demo体会 exit()和_exit()的区别
#include<stdio.h>
#include<stdlib.h>

int main()
{
  printf("hello world");
  exit(); //冲刷缓冲区
  //_exit(); 
}

调用exit函数:
在这里插入图片描述
调用_exit()函数:
在这里插入图片描述

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

进程等待

为什么要进程等待?

  • 父进程回收子进程资源,避免子进程成为僵尸进程
  • 父进程想要获取子进程的退出状态

进程等待方法

#include<sys/types>
#include<sys/wait.h>

pid_t wait(int* status);
参数:status输出型参数,获取子进程退出状态,如不关系设置为NULL
返回值:成功返回被等待进程的pid,失败返回-1

pid_t waitpid(pid_t pid,int* status,int options);
参数:
pid:
	pid=-1,表示等待任意一个子进程。与wait等效
	pid>0,表示等待进程ID和pid相同的子进程。
status输出型参数:
	WIFEXITED(status):查看子进程是否正常退出
	WEXITSTATUS(status):查看进程退出码
options等待方式:
	阻塞式:options = 0
	非阻塞式:WNOHANG,若pid指定的子进程没有结束,则立即返回0,不进行等待。
	如果成功返回子进程pid
返回值:
	当正常返回的时候waitpid返回收集到的子进程的进程ID;
	如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
	如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
	出错的愿意就是,waitpid等待的子进程不是自己的子进程
	
注意:这里解释一下WNOHANG的意识,在计算机里,HANG是一个术语表示卡住的意思,
W:表示我要等待,NO:但是我不想HANG住,意思就是非阻塞等待;

注意:

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放子进程资源,获取子进程的退出信息
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能被阻塞
  • 如果等待的子进程不存在,wait/waitpid立马报错返回
    在这里插入图片描述
    获取子进程status
  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息.
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
  • status不能简单的当作整形来看待,它的低16位是有效的。

在这里插入图片描述
core dump:核心转储
怎样区分进程是正常退出还是异常退出:
查看低八位(第7位)是否有core dump信号,如果有就是异常退出,如果没有那就是正常退出。然后再看次低八位内面,正常退出的原因;另外还可以利用waitpid提供的宏进行判断 WIFEXITED(status):查看子进程是否正常退出、WEXITSTATUS(status):查看进程退出码;推荐使用宏因为安全

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>

int main()
{
  pid_t pid  = fork();//创建子进程
  if(pid < 0)
  {
    perror("fork error");
    return -1;
  }

  if(pid == 0)//子进程
  {
    //child
    printf("I am child:%d\n",getpid());
    //int ret = 7 / 0;//被除数不能为0,制造异常退出条件验证status退出状态码
    exit(123);
  }
  else//父进程
  {
    //parent
    printf("I am parent:%d\n",getpid());
    printf("I am parent, Wait child process\n");
    int status;
    //此处展示都是阻塞式等待,非阻塞请下来自己实现
    int ret = wait(&status);
    //int ret = waitpid(-1,&status,0);
    if(ret < 0)
    {
      perror("wait error");
      return -1;
    }
    if((status&0x7f)==0)//表示进程正常退出
    {
      printf("exit success:exit status : %d\n",WEXITSTATUS(status));
    }
    else//表示进程异常退出
    {      
      printf("exit faile:exit signal:%d\n",status & 0x7f);
    }
  }
  return 0;
}

正常退出:
在这里插入图片描述
异常退出:
在这里插入图片描述
退出返回的进程信号:
9号:异常退出;—kill -9 pid
8号:程序异常—程序出问题了触发了异常,导致操作系统向它信号 ;除0错误!!!
11号:段错误—野指针触发(野指针指向的是一个随机的物理地址,操作系统未调用前不知道指针内面存的是什么,

野指针: 操作系统解引用每一次对指针的访问,先对指针做虚拟地址到物理地址的转换(页表(知识建立映射)配合mmu,具体执行是mmu),可是在操作系统转换器件发现指向的地址是一个错误的地址, ;我们知道我们程序所出现的错误最终都要在硬件上表现出来硬件出错,进程出错此时触发操作系统知道mmu出错和cpu出错了,所以操作新系统认为该异常,所以就要杀死该进程
向该进程发送异常信号)
程序异常:
其实就是进程内部错误,触发硬件错误,进而导致操作系统向该进程发送信号;— c++内面的异常就是系统内面的信号

	    (野指针)、(除0错误)一个进程异常是因为自己出现错误,操作系统想他发出异常信号;
         cpu中有个运算寄存位(除零错误);	操作系统每一次访问指针时先做虚拟地址到物理地址的转换,在转换期间发现地址错误(野指针)
		 所写的所程序出现错误最后一旦出异常最后都会在硬件上表现出来,进程也会出错。所以操作系统必须清楚硬件上的错误,就需要知道是哪个进程引起来的,知道了以后肯定就要杀死该进程,怎么杀?发送信号;
		 进程错误----硬件错误----操作系统想该进程发送信号杀死该进程----该进程返回杀死自己信号的信息

进程阻塞:
该进程所等待的条件不满足,则操作系统将该等待进程的状态从S态转换为非S态,从运行队列中拿走放入等待队列;当操作对象发现条件复合了,将该进程从等待队列拿出将其唤醒(将PCB转换为R态)放入运行队列;

站在操作系统的角度理解进程阻塞:
	直观的感受:进程阻塞就是进程卡住了
	操作系统角度:父进程在等待子进程,发送了阻塞的原因就是子进程退出这个事件没有发送;所以父进程在以阻塞式的方式在哪里等待;
	阻塞式等待:在阻塞式等待的进程的运行状态不再是运行态R而是处于S状态,调出内存,当等待条件满足时再有操作系统将等待的进程唤醒(其实就是将PCB状态设置为R)
	调入内存;加入到运行调度队列当中。;
	总结:该进程所等待的条件不满足,则操作系统将该等待进程的状态设置为非R状态,从运行队列中拿走,发入到等待队列当中,当等待条件满足时,再有操作系统
	将该等待队列中的等待进程唤醒(其实就是将PCB状态设置为R状态),从等待队列中拿出在放入到运行队列当中,接下来有调度器调度;

相关文章:

  • 【Linux】进程程序替换——exec函数簇
  • 【Linux】入门基础命令(2)
  • 【Linux】权限管理和粘滞位理解
  • linux下inode节点理解
  • C语言函数
  • C语言数组
  • C语言表达式
  • C语言初识指针
  • C语言结构体
  • C语言深度剖析数据在内存中的存储
  • 深入了解指针
  • 字符串函数(认识 + 实现)
  • C语言内存函数(认识 + 实现)
  • 内存对齐和位段
  • 枚举和联合
  • 【402天】跃迁之路——程序员高效学习方法论探索系列(实验阶段159-2018.03.14)...
  • leetcode98. Validate Binary Search Tree
  • Linux快速复制或删除大量小文件
  • mongodb--安装和初步使用教程
  • PV统计优化设计
  • spring cloud gateway 源码解析(4)跨域问题处理
  • Travix是如何部署应用程序到Kubernetes上的
  • tweak 支持第三方库
  • VUE es6技巧写法(持续更新中~~~)
  • 每天10道Java面试题,跟我走,offer有!
  • 前端技术周刊 2018-12-10:前端自动化测试
  • 思考 CSS 架构
  • 提升用户体验的利器——使用Vue-Occupy实现占位效果
  • 用element的upload组件实现多图片上传和压缩
  • 我们雇佣了一只大猴子...
  • ​ 轻量应用服务器:亚马逊云科技打造全球领先的云计算解决方案
  • ​flutter 代码混淆
  • ​如何在iOS手机上查看应用日志
  • (pytorch进阶之路)CLIP模型 实现图像多模态检索任务
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (Redis使用系列) SpringBoot中Redis的RedisConfig 二
  • (ZT)薛涌:谈贫说富
  • (第9篇)大数据的的超级应用——数据挖掘-推荐系统
  • (一)基于IDEA的JAVA基础12
  • (转)jQuery 基础
  • *Django中的Ajax 纯js的书写样式1
  • .NET CF命令行调试器MDbg入门(一)
  • .Net MVC + EF搭建学生管理系统
  • .Net 转战 Android 4.4 日常笔记(4)--按钮事件和国际化
  • .NET企业级应用架构设计系列之应用服务器
  • .Net中间语言BeforeFieldInit
  • [ 隧道技术 ] 反弹shell的集中常见方式(四)python反弹shell
  • [BPU部署教程] 教你搞定YOLOV5部署 (版本: 6.2)
  • [BUUCTF 2018]Online Tool(特详解)
  • [java]删除数组中的某一个元素
  • [LeetBook]【学习日记】数组内乘积
  • [Pytorch]:PyTorch中张量乘法大全
  • [SAP ABAP开发技术总结]面向对象OO
  • [SWPUCTF 2021 新生赛]ez_unserialize
  • [UE4]动画蓝图的编辑全流程(Animation Blueprint)