第四章:进程管理

第四章:进程管理

4.1 进程概念

4.1.1 进程概念

首先我们来看一个例子:
在这里插入图片描述

上面的结果里面,只有结果1是正确的(程序可能最终在A的第二行停下来或者最终在B的第二行停下来。进而导致了i的值可能为100或者200)。因为上面的两个程序是分属于两个进程。他们互不影响。也就是说,虽然这两个程序是并发运行的,但是:

  • 运行的过程是不确定的(i=?)

  • 结果不可再现(程序运行被干扰,B对A产生干扰,或者A对B产生干扰)

    这样的操作系统我们是不希望的,我们需要改变这种情况。

    解决的方案是:对运行过程施加相互制约,我们要达到这个目的,引入了一个概念:进程

进程的定义:

进程是程序在某个数据集合上的一次 运行活动。(一个程序同时运行两次,就是两个进程)

数据集合:软/硬件环境,多个进程共存/共享的环境。

进程的特征:

  • 动态性

    进程是程序的一次执行过程,动态产生/消亡;

  • 并发性

    进程同其他进程一起向前推进

  • 异步性

    进程按照各自的速度向前推进。

  • 独立性

    进程是系统分配资源和调度CPU的单位。(注:线程是后来的概念了。出现了线程,则后来以线程为调度CPU的单位。)

进程与程序的区别:

  • 动态和静态上:
    • 进程是动态的:程序的一次执行过程。
    • 程序是静态的:一组指令的有序集合。
  • 从时间上看
    • 进程是暂存的:在内存驻留
    • 进程是长存的:

进程的类型:

  • 按照使用资源的权限:
    • 系统进程:指系统内核相关的进程
    • 用户进程:运行于用户态的进行
  • 按照对CPU的依赖性
    • 偏CPU的进程:计算型进程
    • 偏I/O的进程:则重于I/O的进程
  • 其他的分类标准

下面我们来了解一下Linux的进程分类

  • 按照进程特点来分:

    • 1、交互进程:是由shell启动的进程,它既可以在前台运行,也可以在后台运行。交互进程在执行过程中,要求与用户进行交互操作。简单来说就是用户需要给出某些参数或者信息,进程才能继续执行。
    • 2、批处理进程:与windows原来的批处理很类似,是一个进程序列。该进程负责按照顺序启动其它进程。
    • 3、守护进程:是是执行特定功能或者执行系统相关任务的后台进程。守护进程只是一个特殊的进程,不是内核的组成部分。许多守护进程在系统启动时启动,直到系统关闭时才停止运行。而某些守护进程只是在需要时才会启动,比如FTP或者Apache服务等,可以在需要的时候才启动该服务。
  • 按照进程状态的不同来分:

    • 1、守护进程:(补充):所有守护进程都可以超级用户(用户ID为0)的优先权运行;守护进程没有控制终端;守护进程的父进程都是init进程(即1号进程)。 但是,并非所有在后台运行的进程都是守护进程,因为我们可以使用符号“&”来使进程在后台运行。比如:./bin/process_test &,执行该条命令后,相应的进程在后台运行。

    • 2、孤儿进程:一个父进程退出后,它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程所收养,并由init进程对它们完成状态收集工作。

    • 3、僵尸进程:一个子进程结束但是没有完全释放内存(在内核中的 task_struct没有释放),该进程就成为僵尸进程。

      当僵尸进程的父进程结束后该僵尸进程就会被init进程所收养,最终被回收。

    僵尸进程会导致资源的浪费,而孤儿进程不会。

4.1.2进程状态

进程的状态

进程在运行时和其他的进程共享CPU,还有其他的资源共享,如I/O等。所以进程可能会暂停。所以进程需要有其状态的定义。

  • 运行状态(Running)

    进程已经占用了CPU,正在运行着;

  • 就绪状态(Ready)

    具备了运行条件,但是因为还没有得到CPU,暂时无法运行;(当正运行的程序被切换,也会进入就绪状态,他们都在一个就绪队列里面等待CPU执行权);

  • 阻塞状态(Block)(等待(wait)状态)

    因为某项服务或者信号还没具备,而需要等待其完成的一种进程状态。比如等待:系统调用,I/O操作,合作进程的信号还没来;ps:我们可以使用java里面写的很多的如socket.getOutputStream.read()来理解,这些函数是阻塞式方法或者生产者消费者等。

注意,不同操作系统定义的状态名可能不同。如:

在这里插入图片描述

进程状态之间的转换变迁

  • 进程的状态在满足一定的条件时转化。

在这里插入图片描述

一些其他的转变情况:

在这里插入图片描述

Linux的进程状态

  • 可运行态
    • 就绪:TASK_Running
      • 在就绪队列里面等待调度
    • 运行:正在运行
  • 阻塞(等待态)
    • 浅度阻塞:TASK_Interruptible(可中断的)
      • 能够被其他进程的信号或者时钟唤醒。
    • 深度阻塞TASK_Uninterruptible(不可中断)
      • 不能被其他的进程通过信号或者时钟唤醒。
  • 僵死态:TASK_ZOMBIE
  • 挂起态:TASK_STOPPED
    • 进程被挂起(比如调试的程序)

Linux的进程状态转化图:

在这里插入图片描述

Linux使用fork()创建一个进程。

4.1.3 进程控制块(Process Control Block,PCB)

既然要管理进程,必然进程需要包含一些信息。也就是它需要一个数据结构来维护它的信息——进程控制块。

进程控制块(一种数据结构):

  • 描述了进程状态、资源、和与进程相关关系的数据结构

  • PCB是进程的标志,操作系统就是通过感知PCB来控制管理进程。

  • 进程 = 程序+PCB

    进程和程序和PCB的关系图:

在这里插入图片描述

PCB的数据结构

  • PCB中的基本成员
    在这里插入图片描述

    我们具体到Linux里面查看其定义:

在这里插入图片描述

具体的文件定义:打开linux/sched.h

在这里插入图片描述

进程的上下文和进程的切换(进程的切换就是上下文的切换)

  • 什么是进程的上下文?
    • Context,进程的运行环境,CPU环境
  • 进程的切换过程
    • 换入进程的上下文到CPU里面(从堆栈上读取进程上下文)
    • 换出进程的上下文离开CPU(读到堆栈上)

4.2 进程控制

4.2.1 进程的控制

进程控制的概念

  • 概念:在进程生存期间,对其全部行为的控制

  • 四个典型的控制行为:

    • 创建进程

    • 阻塞进程

    • 撤销进程

    • 唤醒进程

1、进程的创建

功能:创建一个具有指定标识(ID)的进程

参数:

  • 进程标识(PID)、优先级、进程起始地址、CPU初始状态、资源清单

创建进程的过程:

  • 第一步:创建一个空白的PCB
  • 获得并赋予进程标识符PID
  • 为进程分配空间
  • 初始化PCB
    • 默认值
  • 插入相应的进程队列
    • 新进程插入就绪队列(Ready进程队列)

创建进程的伪代码:

在这里插入图片描述

这个进程工作完,我们要即使地关闭该进程。

2、进程的撤销

功能:

  • 撤销一个指定的进程
  • 收回进程所占用的资源,撤销该进程的PCB

进程撤销的时机/事件

  • 正常撤销
  • 异常结束
  • 外界干预

参数

  • 被撤销的进程名(ID)

进程撤销的实现步骤

  • (1)在PCB队列中检索出该PCB
  • (2)获取该进程的状态
  • (3)若该进程处于运行态,立即终止该进程
    • 检测是否有子进程,如果有,则需要进行递归撤销子进程先。

只有将一个进程的pcb撤销,才能说真正的把一个进程干掉。)

  • 释放进程占有的资源
  • 将进程从PCB队列中移除

3、进程阻塞

功能:停止进程的执行,变为阻塞。

阻塞的时机/事件

  • 请求系统服务
    • 由于某种原因,OS不能立即满足进程的相关要求而进程进入等待状态(wait或者block);
  • 启动某种操作
    • 进程启动某种操作,阻塞等待该操作完成;
  • 新数据尚未到达
    • A进程要获得B进程的中间结果,A进程等待;
  • 该进程无新的工作可做
    • 进程完成了任务后,自我阻塞,等待新的任务到达;

进程阻塞需要的参数

  • 阻塞原因
  • 不同原因构建,其有不同的阻塞队列。

进程阻塞的实现(阻塞队列,不同原因造成的阻塞进程进入不同的阻塞队列)

  • 第一步,停止运行该进程
  • 第二步,将PCB“运行态”修改为“阻塞态”
  • 第三步,插入相应原因的阻塞队列
  • 第四步,转调度程序

4、进程唤醒

  • 功能:唤醒处于阻塞队列当中的某个进程。

  • 引起唤醒的时机/事件

    • 系统服务由不满足到满足
    • I/O完成
    • 新数据到达
    • 进程提出新请求(服务)
  • 参数:被唤醒的进程的标识,即PID。

5、进程控制原语

原语

* 由若干指令构成的具有特定功能的函数
* 具有原子性,其操作不可分割

原语必须一气呵成地完成,不允许中断。

  • 进程控制原语:
    • 创建原语,进程必须一次创建成功或者失败,不允许中断
    • 撤销原语,进程必须一次撤销成功或者失败,不允许中断
    • 唤醒原语,进程必须一次唤醒成功或者失败,不允许中断
    • 阻塞原语,进程必须一次阻塞成功或者失败,不允许中断

4.2.2 windows的进程控制

windows如何通过编程的方式启动一个进程?

  • 表面上我们不通过编程,而使用了图像化界面的方式来

    找到对应的程序,点击则可以创建一个进程:

在这里插入图片描述

  • 第二种方式通过编程的方式来实现绘制一个矩形的程序并创建该进程

    • system(“C:\DrawRect.exe”)程序
    • WinExec(“C:\DrawRect.ext”, SW_SHOWMAXIMIZED);
    • ShellExecute(NULL,“open”,“C:\DrawRect.exe”,NULL,NULL,FALSE,NULL,NULL)

    上述三个函数原型:

在这里插入图片描述

上述三个函数都是对CreateProcess()的一定程度封装,下面我们来看一下CreateProcess的函数原型:

在这里插入图片描述

在这里插入图片描述

CreateProcess()创建进程,这是一个原语函数,无法拆分。无法中断。其创建进程的具体步骤:

  • 创建进程内核对象,创建虚拟地址空间
  • 装载EXE和/或DLL的代码和数据到地址空间中
  • 创建主线程和线程内核对象
  • 启动主线程,进入主函数(main)

编程练习

1、编写一个main函数,启动记事本notepad.exe,同时打开c:\readme.txt文件

答案:

#include<windows.h>
int main(){
    
    
	STARTUPINFO si = {
    
    sizeof(si)};
	PROCESS_INFORMATION pi;
	
	char* ZW = "C:\\Windows\\System32\\notepad.exe";
	char *szCommandLine = "C:\\Windows\\System32\\notepad.exe E:\\readme.txt";
	//char *szCommandLine = "E:\\readme.txt";
	::CreateProcess(ZW,szCommandLine,NULL,NULL,FALSE,
	NULL,NULL,NULL,&si,&pi);
	return 0;
}

2、练习2

在这里插入图片描述

简单的读取,然后创建相应的进程即可。

windows如何结束进程?

方式有图中的两种:
在这里插入图片描述

  • ExitProcess温和的方式,它结束的时候会告知其他进程或者操作系统,我结束了,方便操作系统或者其他进程去做其他操作。
  • TerminateProcess暴力方式,线程直接消亡,不会通知其他进程或者操作系统做相应的操作处理。

4.2.3 Linux控制进程的实现

创建进程

  • 创建进程pid_t fork(void)函数,pid_t本质上是一个整型;

    新进程是当前进程的子进程

    父进程和子进程

    • 父进程:fork()的调用者
    • 子进程:新建的进程
  • 子进程是父进程的复制(只有pid和与时间不同,其他的资源情况一致。而且可以和父进程并发运行。)

在这里插入图片描述

解释:因为子进程和父进程都是同样的内容,所以输出了两个Hello World!

下面我们再来看一个例子:

在这里插入图片描述

第一次结果:在这里插入图片描述

第二次结果可能不同于第一次的结果。

分析问题,我们来解析一个创建进程函数fork()

fork()函数在子进程里面返回的pid=0,父进程则大于0。
在这里插入图片描述

因而两个if的分支都被执行了。但是输入的顺序是取决于CPU的调度,不确定的。

关于父进程和子进程的执行流程

父进程和子进程是并发的,但是其并发的范围是不一样的,如果一样,则上面的进程则无限创建进程了。所以并发的范围为:

在这里插入图片描述

fork()函数的实现文件:

  • 定义

在这里插入图片描述

  • 实现

在这里插入图片描述

我们看了java并发编程,知道init()是所有进程的父进程。但是这些子进程却不是父进程的拷贝,想想也不可能。所以它是通过什么来创建子进程的呢?那就是exec函数蔟。

在这里插入图片描述

在这里插入图片描述

我们来看一下exec函数蔟有哪些?

在这里插入图片描述

前面的五个函数都是对最后一个函数的不同程度的封装。

4.3 线程

4.3.1 线程的概念

线程的概念

先从一个例子开始,一个程序里面同时实现画圆和画方块的效果。如果我们没有线程的话,我们是无法实现同时的,除非有多个cpu。那么什么是线程?

在这里插入图片描述

没有线程的时候,或者操作系统不支持线程时,进程是CPU调度的基本单位和分配资源的基本单位。但是有了线程,则线程成为了CPU分配资源和CPU调度的基本单位,而且它比进程更细化,线程可以直接由CPU运行,它就是一条执行路径。

那么用线程如何实现画圆和画方呢?

在这里插入图片描述

也就是我们需要先创建线程。使用Windows底层的创建线程函数为:

//MSDN中CreateThread原型:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
SIZE_T dwStackSize,//initialstacksize
LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction
LPVOID lpParameter,//threadargument
DWORD dwCreationFlags,//creationoption
LPDWORD lpThreadId//threadidentifier
);
    
//processthreadsapi.h中CreateThread原型:
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateThread(
_In_opt_LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_SIZE_T dwStackSize,
_In_LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt___drv_aliasesMemLPVOID lpParameter,
_In_DWORD dwCreationFlags,
_Out_opt_LPDWORD lpThreadId
);

实现如下:

在这里插入图片描述

单线程程序和多线程程序

就像java程序,运行的时候一般函数入口为main线程。

在这里插入图片描述

4.3.2 线程的典型使用场景

  • 程序的多功能运行

    • 暴风影音(在线看电影)
      • 并发功能:视频解码,音频解码,网络接受字节信息等等。每个功能都要创建一个线程从而实现多功能并发。
  • 需要改善窗口交互性的地方

    • 后台计算程序十分耗时的情况,如图:在这里插入图片描述

      这上面的后台计算和输入框的输入是串行的话,则单线程导致交互性变得十分的差,解决的办法就是:把后台计算函数创建为——线程

    • 微软的文件拷贝程序就是一个多线程程序,我们拷贝到一半点击取消键是可以的。

      在这里插入图片描述

  • 需要改善程序结构的地方

  • 多核CPU需要发挥其性能,要应用多线程技术

4.3.3 创建线程和使用线程存在的一些问题

创建线程

在这里插入图片描述

使用线程存在的一些问题

  • 线程安全问题;
  • 线程使得程序难以调试;
  • 并发过程难以控制(同步和互斥问题)。

在这里插入图片描述

4.4 临界区和锁

4.4.1 临界资源和临界区

例子引入

在这里插入图片描述

如果是A先运行然后是B运行,则当A运行到第三行停下来,CPU切换到B,则B会修改i的值,使得A运行得不到期待的结果(i=100)。同理如果是B先运行,也有可能B得不到期待的结果(i=200)。造成这个现象的原因是线程的并发运行导致了对共同资源的访问。要想解决这个问题,办法是:在这里插入图片描述

上面的这个区域就叫做临界区。对应的共享资源就是临界资源。

临界区和临界资源的定义

  • 临界资源(critical Resource)

    • 一次只允许一个进程单独访问(使用)的资源

      比如上面的i

  • 临界区(Critical Section)

    • 进程中访问临界资源的程序段。
  • 临界区和临界资源的访问特点

    • 具有排他性(联想java里面的多线程,使用了线程安全,同一个时刻只有一个线程能进入临界区,所以当一个线程已经进入临界区,其他的线程都不能进入。)
    • 并发线程不能同时进入临界区(多个线程同时抢夺锁,最后只有一个线程能抢到,即只有一个线程能进入临界区)

设计临界区访问机制的四个原则

  • 忙则等待
    在这里插入图片描述

  • 空闲让进

    • 当没有进程处于临界区,任何有权进程可以进入临界区
  • 有限等待

    • 进程进入临界区的请求应在有限的时间里得到满足(思考临界区设置得大一些好还是小一些好?随意扩大会使得另外的进程等待时间变长,设置太小,会无法实现临界区的目的)在这里插入图片描述
  • 让权等待

    • 等待的进程需要放弃CPU,如果它一直在占用CPU,在询问CPU是否可以进入临界区,则其他的进程无法得到CPU,也就无法知道其是否能够进入临界区。

4.4.2 锁机制

要想让多个进程(线程)能够实现我们期望的访问临界区的原则,我们需要一个机制来实现——锁机制。(联想我们java里面讲的锁机制即可。)

锁机制的定义和原理

在这里插入图片描述

锁机制中的锁操作

  • 上锁操作

    • 第一步,检测锁S的状态(0或者1?)

    • 第二步,如果S=0,则返回第一步

    • 第三步,如果S=1,则置其为0

    • 相关伪代码为:(操作系统里使用原语来实现,成为:上锁原语)

    在这里插入图片描述

  • 解锁操作

    • 只需要一步,就是修改S的值为1.

    • 实际上看起来步骤少,但是实现是需要额外的其他步骤。操作系统里面一样是使用原语来实现的。这个叫做开锁原语。

在这里插入图片描述

锁机制访问临界区

  • 步骤

    • 初始化锁的状态S=1(可用)

    • 进入临界区前执行上锁操作Lock(s)

    • 离开临界区之后执行开锁unlock(s)操作

在这里插入图片描述

在这里插入图片描述

4.5 同步和P-V操作(重要)

4.5.1 同步和互斥概念

进程的互斥概念

例子:在这里插入图片描述

定义:

* 多个进程由于共享了独立性资源,必须协调各进程对资源的存取顺序:确保没有任何两个或以上的进程同时进行了存取操作。
* 互斥和资源共享
* 资源:临界资源
* 存取操作区域:临界区

进程的同步关系

  • 概念:合作的进程某些操作之间需要满足某种先后关系或者某个操作是否能进行需要满足某个前提条件,否则只能等待
    在这里插入图片描述

  • 互斥关系属于特殊的同步关系

4.5.2 P-V操作概念

引入P-V操作

什么是信号灯概念?借用了红绿灯概念,实现了进程同步机制。

在这里插入图片描述

信号灯操作用于进程同步的基本思想:

进程在运行过程里面受信号灯状态的控制,并且能改变信号灯的状态。

* 进程受控制:信号灯的状态可以阻塞或者唤醒进程
* 改变信号灯:信号灯的状态可以被进程改变

信号灯的数据结构

在这里插入图片描述

信号灯的操作:

  • P操作(函数或者过程,P(S,q))在这里插入图片描述

  • V操作(函数或者过程,V(S,q)),只要该进程调用了V操作,当前进程都会向前进行。在这里插入图片描述

    P,V是荷兰语,Passeren通过,Vrijgeven释放。

4.5.3 P-V操作

P-V操作解决互斥问题

实质:实现对临界区的互斥访问

* 允许最多一个进程处于临界区

应用过程:

  • 进入临界区前先执行P操作;(可能阻塞当前进程)
  • 离开临界区后再执行V操作;(可能唤醒某个进程)

在这里插入图片描述

我们来看一个例子:3个进程Pa,Pb,Pc。CSa,b,c是临界区

在这里插入图片描述

1.设Pa最开始运行,则P(mutex)操作使得mutex-=1——0,Pa进入临界区,此时如果Pb开始P(mutex),mutex-=1——-1,阻塞,同理Pc阻塞,mutex=-2;

2.然后到Pa的V(mutex),使得mutex++——-1,同时唤醒一个进程,假设是Pb,这个时候,Pb又运行到了V(mutex),使得mutex++——0,再唤醒Pc,最后执行Pc的V(mutex)操作。进而实现了三个进程的互斥。(特殊的同步)
在这里插入图片描述

4.5.4 P-V操作解决同步问题

同步机制的实质

  • 运行条件不满足的时候,能让进程及时的暂停
  • 运行条件满足时,能让进程立即继续

P-V操作应用于进程同步的基本思路

  • 暂停当前进程:在关键操作之前执行P操作

    • 必要时可以暂停
  • 继续进程:在关键操作之后执行V操作

    • 必要时可以唤醒合作进程
  • 定义有意义的信号量S,并设置好合适的初值

    • 信号量S能够明确地表示“运行条件”。

    例子:
    在这里插入图片描述

在这里插入图片描述

当执行P(S1),则S1-=1——-1,司机阻塞。售票员关门了,到V(S1),因为S1=-1<=0,则唤醒一个进程司机进程。S1++——0。此时司机起步,驾驶。售票员售票。两者并发。当P(S2),S2-=1——-1,售票员进程阻塞。当司机进程开始停车。S2++——0,同时唤醒售票员进程,此时P(S2)满足,售票员开门。此时S2=0,S1=0,回到初始条件。

4.5.5 P-V经典同步操作问题

经典问题1: 多生产者和多消费者问题(联想java多线程并发中讲的生产者消费者问题)

在这里插入图片描述

使用P-V操作来解决的:

在这里插入图片描述

经典问题2:读者(Reader)和编者(Editor)问题

在这里插入图片描述

我们需要分析信号是什么?约束如何实现?要一个一个约束的实现。

  • 编者之间的互斥
  • 编者和读者之间的互斥
  • 读者之间的不互斥
  • 考虑读者的个数

实现:
在这里插入图片描述

小结

  • 信号灯机制P-V操作解决同步问题

    • 关键操作之前P操作
    • 关键操作之后V操作
    • 区分关键操作或运行条件或影响
  • 生成者和消费者问题

    • 同步和互斥混合
  • 读者和编者问题

    • 典型的互斥问题

    P操作就是理解为上锁操作,V操作就是理解为开锁操作。

4.6 Windows和Linux同步机制

4.6.1 windows的同步机制

windows进程的同步机制提供了什么?

在这里插入图片描述

1、临界区机制(CRITICAL_SECTION类)

  • 进程内使用,保证仅一个线程可以申请到该对象

  • 临界区内是临界资源的访问

  • 相关的API

在这里插入图片描述

实际例子:使用三个线程使得sum从0累加到240,每个线程负责一个累加80。

代码:在这里插入图片描述

首先我们先了解一下相关函数WaitForMultipleObjects:

在这里插入图片描述

所以上面的那一行的意思就是:等待hThread数组里的线程都结束才返回。

上面程序的运行结果:运行无法实现目标,三次运行的结果都不唯一,而且无法得到240。

分析原因:因为上面的代码存在访问临界资源,而三个线程没有实现互斥机制,进而导致了不确定的结果。

我的运行结果:

在这里插入图片描述

我添加了延时函数以后,延时10ms:在这里插入图片描述

修改代码:

在这里插入图片描述

实战代码及运行结果:

在这里插入图片描述

2、互斥量

  • 互斥量概念

在这里插入图片描述

  • 用在互斥量上的API函数

在这里插入图片描述

3、信号量机制(Semaphore)

  • 概念:

  • 可以控制同一个临界区支持多个线程/进程同时访问

  • 信号量的值可以通过相应的函数进行增或者减

    • WaitForSingleObject将信号量减1
    • ReleaseSemaphore将信号量增1
  • 信号状态

    • 信号量的值大于0时,有信号状态
    • 信号量的值小于等于0,为无信号状态
  • 使用信号量同步的例子(AfxBeginThread是MFC中创建线程的全局函数,想要导入对应的文件需要先配置MFC)

在这里插入图片描述

再来看一下访问数据库的函数:

在这里插入图片描述

注意上面使用了信号量,支持多个线程同时进入临界区的两个函数。

4、windows的其他机制

  • 事件机制

在这里插入图片描述

5、小结Windows的同步机制
在这里插入图片描述

4.6.2 Linux的父子进程之间同步

前沿知识

  • wait(),这个函数,如果是父进程调用,则子进程不结束,则父进程一直等待。子进程结束则wait返回子进程的pid,失败则返回-1

在这里插入图片描述

  • sleep(),函数让线程睡眠多少秒。在这里插入图片描述

  • exit(),结束进程。销毁进程并报告父进程。

在这里插入图片描述

然后我们来看一个例子:

在这里插入图片描述

实战代码:

在这里插入图片描述

运行结果:子进程的pid和父进程打印的pid一致。

在这里插入图片描述

父子进程之前共享普通变量的问题

在这里插入图片描述

各自操作普通变量的副本,并不会共享变量的值。

实战代码:

在这里插入图片描述
运行结果:说明父子进程对普通的共享变量i,操作的是变量i的副本。

在这里插入图片描述

共享文件资源问题

在这里插入图片描述

对于文件,父子进程共享同一文件和文件指针。

实战代码:
在这里插入图片描述

运行结果:
在这里插入图片描述

4.7 进程之间的通信

4.7.1 Windows的匿名管道通信

  • 管道通信机制

    • 1.管道定义pipe,管道是进程之间的一种通信机制。一个进程(A)可通过管道把数据传输到另外一个进程(B)。前者向管道传输数据,后者从管道读取数据。

在这里插入图片描述

  • 管道的工作原理

在这里插入图片描述

Handle W;写句柄 Handle R;读句柄

  • 匿名管道使用注意事项——仅能用于父子或者兄弟进程间的通信。

在这里插入图片描述

  • 双向匿名管道通信注意事项在这里插入图片描述

原来的一个例子:

将命令行程序算命大师修改为一个窗口程序:

题目要求:利用之前的算命大师的代码,不重新编写程序,而是修改程序,使其成为一个窗口程序。

设计思路:

在这里插入图片描述

关键代码:在这里插入图片描述

基于上面的原理,自己实现的一个简单的命令程序:

父进程程序:

//算命大师前台程序 
#include <Windows.h>
#include <iostream>
#include <string.h>
#include<stdlib.h>
 
using namespace std;
void invoke(string exe);
 
int main(int argc, char* argv[])
{
    
    

	string exe = "code4_Pipe.exe";
	invoke(exe);
	return 0;
}
 
void invoke(string exe)
{
    
    

	  		//第一条管道 
	SECURITY_ATTRIBUTES saPipe; 
        saPipe.nLength = sizeof(SECURITY_ATTRIBUTES);
        saPipe.lpSecurityDescriptor = NULL;
        saPipe.bInheritHandle = TRUE;
			
	HANDLE hRead1Pipe, hWrite1Pipe;//两个句柄 
	//建立两个句柄匿名通信管道saPipe 
	BOOL bSuccess = CreatePipe(&hRead1Pipe, 
		&hWrite1Pipe, 
		&saPipe, 
		0);  
	if(!bSuccess) //创建匿名管道不成功,则退出 
		return ;

	//创建第二条管道 
	
	HANDLE hRead2Pipe, hWrite2Pipe;//另外的句柄 
	//创建管道2,匿名管道 
	BOOL bSuccess2 = CreatePipe(&hRead2Pipe, 
		&hWrite2Pipe, 
		&saPipe, 
		0); 
		if(!bSuccess2) //创建匿名管道不成功,则退出 
		return ;	 
  		//si指算命大师的输入和输出 
	PROCESS_INFORMATION pi;//pricessinfo 
	STARTUPINFO si; //startinfo 
        memset(&si,0,sizeof(si)); //分配内存
        //界面窗口 
		si.wShowWindow = SW_HIDE;
		//隐藏窗口的输入是父进程的输入,所以读管道 
		//输入是父进程窗口,所以是写入 
		 
        si.hStdInput=hRead1Pipe;//输入重定向到读管道 
        si.hStdOutput=hWrite1Pipe;//输出重定向到写管道 
        si.dwFlags=STARTF_USESTDHANDLES;
        si.cb=sizeof(si);

     const int max = 1024;  

	//创建子进程 
	if(CreateProcess(NULL,(char*)exe.c_str(),NULL,NULL,TRUE,0,NULL,NULL,&si,&pi))
	{
    
    	
	

		char subuf[max] = {
    
    0};
		DWORD subwriteBytes;
		WriteFile(hWrite2Pipe,"算命大师为您测算属相和星座 ver2.0\n请输入日期YYYY/MM/DD:",1023,&subwriteBytes,NULL);
	
	
			//创建缓冲区 
		char csDate[max] = {
    
    0};
		scanf("%s",csDate);	
		 
		DWORD writeBytes;
		//发送给子进程
	   	WriteFile(hWrite1Pipe,csDate,1023,&writeBytes,NULL);
	
		//创建缓冲区 
		char buf[max] = {
    
    0};
		DWORD dw;
		//子进程从这个地方读取 前台输入的数据 
		// ReadFile (HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped);
		while(ReadFile(hRead1Pipe,buf,max-1,&dw,NULL))
		{
    
    //使用字符串切割函数,切割出数据。
			//使用算命大师处理,得到的数据写入管道 
			
			cout<<buf<<endl;
//			int y,m,d;
//			y = atoi(strtok(buf,"/"));
//			cout<<y;
//			m =  atoi(strtok(buf,"/"));
//			cout<<m;
//			d = atoi(strtok(buf,"/"));
//			cout<<d;
			//处理 
			
			//将处理的数据发送给父进程 
			WriteFile(hWrite2Pipe,subuf,1023,&subwriteBytes,NULL); 
			break;
		}
		//关闭 
		CloseHandle(pi.hProcess);   
	}
		// 父线程先睡眠,让子进程先运行
		//Sleep(1000);// //父进程将数据写入管道
		DWORD readBytes;
		char READDate[max]={
    
    0};
		//读取子进程的数据 
		ReadFile(hRead2Pipe,READDate,1023,&readBytes,NULL);
		cout<<READDate<<endl;
		CloseHandle(pi.hThread);  
	//关闭管道1 
	CloseHandle(hRead1Pipe);
	CloseHandle(hWrite1Pipe);
	CloseHandle(hRead2Pipe);
	CloseHandle(hWrite2Pipe);
}

原来的算命大师程序:

//算命大师后台程序 
#include<stdio.h>
int main(){
    
    
	printf("算命大师为您测算属相和星座 ver2.0\n");
	printf("请输入日期YYYY/MM/DD:\n");
	int y,m,d;
	scanf("%d/%d/%d",&y,&m,&d);
	const char* animals[12] = {
    
    "你属猴","你属鸡",
	"你属狗","你属猪","你属鼠","你属牛"," 你属虎","你属兔","你属龙","你属蛇","你属马","你属羊"};
	int animal_index = y%12;
	const char* animal = animals[animal_index];
	
	int xing_index;
	if(m<=1&&d<=19) 
		xing_index = 0;
	else if(m<=2&&d<=18)
		xing_index = 1;
	else if(m<=3&&d<20) 
		xing_index = 2;
	else if(m<=4&&d<=19)
		xing_index = 3;
	else if(m<=5&&d<=20)
		xing_index = 4;
	else if(m<=6&&d<=21)
		xing_index = 5;
	else if(m<=7&&d<=22)
		xing_index = 6;
	else if(m<=8&&d<=22)
		xing_index = 7;
	else if(m<=9&&d<=22)
		xing_index = 8;
	else if(m<=10&&d<=23)
		xing_index = 9;
	else if(m<=11&&d<=22)
		xing_index = 10;
	else if(m<=12&&d<=21)
		xing_index = 11;
	else xing_index = 0;
	const char* xingzuo[12] = {
    
    "魔蝎座","水瓶座","双鱼座","白羊座",
	"金牛座","双子座","巨蟹座","狮子座","处女座","天秤座","天蝎座","白羊座"};
	//添加const可去除[Warning] deprecated conversion from string constant to 'char*' [-Wwrite-strings] 
	const char* xing = xingzuo[xing_index];

	printf("%d/%d/%d\n",y,m,d);
	printf("%s,%s,每天好运气!",animal,xing);

	return 0; 
}

运行父进程的程序,输入,将信息作为字符串传送给子进程,子进程处理完数据,输出。

在这里插入图片描述

4.7.2 Linux的信号通道

信号的概念

在这里插入图片描述

终端上使用信号机制的例子

在这里插入图片描述

信号产生的方式
在这里插入图片描述

信号的定义

在这里插入图片描述

信号机制编程

  • 例子1:编写一个死循环的程序,当输入按下的键盘组合为"Ctrl+C",则输出Bye,退出程序。[

在这里插入图片描述

在这里插入图片描述

实战演示:

代码:

  #include <stdio.h>
  #include<signal.h>
  //#include<Linux/sys.c>
  void int_handler(int signum);//发送SIGINT信号的对应的响应函数
  {
    
    
      printf("\nBye Bye!\n");
      exit(-1);
  }
  
  int main()
  {
    
    
      signal(SIGINT,int_handler);
      printf("int_handler set for SIGINT\n");
      //cout << "Hello World!" << endl;
      while(true){
    
    
          printf("go to sleep.\n");
          //延时60ms
          sleep(60);
      }
      return 0;
  }
  • 例子2:

在这里插入图片描述

在这里插入图片描述

测试代码:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>

void sayBye(){
    
    
    printf("Bye Bye!\n");
}
//处理函数
void int_handler(){
    
    
    sayBye();
    exit(-1);
}
int main()
{
    
    
    int pid;
    while((pid=fork())==-1);//创建子进程, 直到创建成功
    //SIGUSR1是内置的信号
    signal(SIGUSR1,int_handler);//注册自定义的信号处理函数
    //父进程向子进程发送信号告知结束
    //kill(pid,SIGUSER1);
    if(pid>0)
    {
    
    
        while(1){
    
    //无线死循环
            printf(" child process is looped!\n");
        }
    }
    else if(pid==0)
    {
    
    
        int pidchild = getpid();
        //父进程向子进程发送信号告知结束
        kill(pid,SIGUSR1);
        //父进程等待
        if(wait()!=-1)//子进程真正的结束,没有异常结束
            exit(0);
    }
    return 0;

}


在这里插入图片描述

至于Linux如何注册信号?用户自定义信号?修改注册信号的响应函数?如何向一个进程发送信号?

signal.h—— kill();

在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_44861675/article/details/111604217