Download as pdf or txt
Download as pdf or txt
You are on page 1of 103

第一章 进程概述

1.1 进程的概念
我们所开发的应用程序如果需要运行起来,需要被操作系统加载到内存上进行运行。操作系统用进程来
描述这些被运行起来的程序。

为什么要有进程呢?

随着计算机的发展,计算机能够提供给我的资源例如CPU、内存等越来越多,越来越强,我们的操
作系统如果只运行一个程序显然是对计算机资源的一种浪费,所以系统允许运行多个程序,并且同
一个程序可以同时运行多次,这样多个程序的实例在内存中该如何表示就成了问题。现在操作系统
提出了进程的概念,可以更好的表示程序在内存中的执行过程。
程序是一些二进制指令的集合,是静态的。
进程是表示程序的执行过程,是动态的。
进程的定义:

一个具有独立功能的程序在一个数据集合上的一次动态执行过程。

数据集合:进程所运行的特定的软硬件环境

1.2 进程的特点
1、动态性:可动态的创建、结束进程

2、并发性:进程可以被独立调度并占用处理器运行,但是在某一个时间段内可以运行有多个进程占用
处理器运行(注意并行的概念:在某个时间点有多个进程在占用处理器运行,在单核CPU上是无法实现
并行的。在单个cpu运行队列上各个进程宏观并行微观串行执行)

3、独立性:进程具有自己独立的地址空间,进程是系统进行资源分配的的最小单位

4、异步性:进程按各自的速度向前推进

1.3 进程的状态
1、进程的三种状态

进程在运行的过程中会与其他的进程共享CPU,所以进程其实是“断断续续地在CPU上运行”。

进程在生命结束前处于以下三种基本状态之一:

运行状态(Running):进程占用CPU,在CPU上运行
就绪状态(Ready):具备运行地条件,但是因为没有获取CPU,暂时不能运行
阻塞状态(Block):也叫做等待状态,因为等待某项服务完成或信号不能运行的状态,如等待∶
系统调用,I/O操作,合作进程信号...

在Linux中我们可以使用 top 命令动态地查看系统中各个进程地状态:


也可以使用 ps -aux 命令查看当前时刻各个进程的状态:

2、进程状态的变化

进程的状态可以依据一定的条件相互转化:

就绪->运行:

进程的调度
运行->就绪:
时间片花完
被抢占
运行->阻塞:

请求服务
等待信号
阻塞->就绪:
服务完成
信号到来
当计算机系统是多道程序设计系统时,通常就会有多个进程或线程同时竞争CPU。只要有两个或
更多的进程处于就绪状态,这种情形就会发生。如果只有一个CPU可用,那么就必须选择下一个
要运行的进程。在操作系统中,完成选择工作的这一部分称为调度程序(scheduler),该程序
使用的算法称为调度算法(scheduling algorithm)。

3、Linux系统对进程状态的定义

可运行态
就绪:TASK_RUNNING(在就绪队列中等待调度)
运行:正在运行
阻塞(等待)态

浅度阻塞:TASK_INTERRUPTIBLE(可中断),能被其他进程的信号或时钟唤醒。
深度阻塞;TASK_UNINTERRUPTIBLE(不可中断),不能被其他进程通过信号和时钟唤醒。深度
阻塞一般是请求系统服务、IO服务,这些服务没有满足前不能被唤醒
僵死态:TASK_ZOMBIE,进程终止执行,释放大部分资源
挂起态:TASK_STOPPED,进程被挂起

1.4 进程的挂起
1、挂起的概念

当系统负载较高时,系统会主动将进行某些进程“挂起”。被挂起的进程的内存空间中的数据会被拷贝到
硬盘上(一般时swap分区),这样被挂起的进程将不再占用内存空间。

挂起(Suspend):把一个进程从内存转到外存。

2、挂起状态

就绪挂起状态(Ready-suspend):进程处于就绪或者运行状态时被挂起
阻塞挂起状态(Blocked-suspend):进程处于阻塞状态时被挂起,当某个事件或者信号发生
时,系统会把阻塞挂起转换为就绪挂起(注意:不是转换为就绪态!)

3、挂起的几种情况

阻塞到阻塞挂起:没有进程处于就绪状态或就绪进程要求更多内存资源时,会进行这种转换,以提
交新进程或运行就绪进程;
就绪到就绪挂起:当有高优先级阻塞(系统认为会很快就绪的)进程和低优先就绪进程时,系统会
选择挂起低优先级就绪进程;
运行到就绪挂起:对抢先式分时系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,
系统可能会把运行进程转到就绪挂起状态;

1.5 进程控制块PCB

1.5.1 进程控制块概述
操作系统使用一个数据结构描述进程的状态、资源、运行变化的过程等。这个数据结构我们称之为进程
控制块(Process Control Block,PCB)。

PCB是系统在创建进程时创建的,当进程退出后PCB会被释放。

PCB包含以下三大类信息:

进程标识信息:本进程的标识,父进程标识,所属的用户的标识
处理及状态信息保存区:用来保存进程运行现场的信息

用户可见寄存器:用户程序可以使用的数据,地址等寄存器(通用寄存器:
AX,BX,CX,DX,SP,数据寄存器,地址寄存器,条件码寄存器)
控制和状态寄存器(CPU寄存器):如程序计数器(PC),程序状态字(PSW)
栈指针:过程调用/系统调用/中断处理和返回时需要用到它
进程控制信息:调度和状态信息,用于操作系统调度进程并占用处理机使用。
进程间通信信息:为支持进程间的与通信相关的各种标识、信号、信件等,这些信息存在接
收方的进程控制块中
存储管理信息:包含有指向本进程映像存储空间的数据结构
进程所用资源:说明由进程打开、使用的系统资源,如打开的文件等
有关数据结构连接信息:进程可以连接到一个进程队列中,或连接到相关的其他进程的PCB

在ubuntu系统中,我们可以使用 sudo apt-get install linux-source 命令下载Linux内核源代码,然


后打开/usr/src/linux-headers-5.11.0-43-generic/include/linux目录下的sched.h头文件就可以查看
struct task_struct中的内容了。

下面我们对struct task_struct结构体中的某些成员变量进行简单的说明:

1.5.2 和进程标识相关的成员变量
PID:进程ID
PPID:父进程的ID
UID:进程所属的用户ID

我们可以在Linux系统下使用 ps -ef 命令查看进程的相关信息

1.5.3 Linux对task_struct的管理
在Linux内核中对task_struct使用以下三种数据结构进行存储,在不同的时机和需求下可以选择合适的
数据结构访问进程的task_struct:

链表:需要遍历系统中的进程时可以使用链表
二叉树:Linux系统中的进程之间是一种树状的关系(每个进程拥有自己的一个父进程和多个字进
程(>=0)),我们可以使用 pstree命令查看这种树状关系。如果将这些进程的PCB保存在二叉树
上,可以为我们访问某个进程的父进程或者子进程提供便利

哈希表:将PCB(task_struct)存储在哈希表中,当我们需要访问任意一个进程时,查找时间复杂
度为O(1)

1.5.4 进程的地址空间
1、进程由PCB和程序所组成。

2、当一个程序被加载到内存上后,会被分配4G的独立的虚拟地址空间。进程的虚拟地址空间布局如
下:
3、进程地址空间各个部分存储内容

4、变量在进程地址空间中的分布实验

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

//未初始化的全部变量
int g_x; //.bss
//初始化的全局变量
int g_y = 100; //.data
//未初始化的const全局变量
const int const_g_x; //.bss
//初始化的const全局变量
const int const_g_y = 100; //.rodata
int main()
{
//局部变量
int x; //栈
//未初始化的static变量
static int static_m; //.bss
//初始化的static变量
static int static_n = 100; //.data

//字符串常量
char *str = "hello"; //str在栈上,"hello"在.rodata
printf("hello: %p\n", &("hello"));
//堆上分配空间
char *p = (char*)malloc(10); //p保存在栈上,p的值是分配的堆空间的首地址
strcpy(p, "yes");

printf("int g_x: %p\n", &g_x);


printf("int g_y=100 : %p\n", &g_y);
printf("const int const_g_x: %p\n", &const_g_x);
printf("const int const_g_y=100: %p\n", &const_g_y);
printf("int x: %p\n", &x);
printf("static int static_m: %p\n", &static_m);
printf("static int static_n=100: %p\n", &static_n);
printf("char *str: %p\n", &str);
printf("char *p: %p\n", &p);
printf("char *p: %p\n", p);

while (1);

return 0;
}

首先我们对程序进行编译:gcc 1.c -o 1 -static

运行程序,查看各个变量在进程地址空间的内存地址

使用objdum 命令查看可执行程序各个段的地址: objdum -h 1


通过查看可执行程序的进程ID,查看该进程的maps文件,查找堆空间和栈空间的首地址

1.6 进程的切换
1.6 1 进程上下文切换的概念
我们知道在单个CPU的情况下,操作系统中运行的各个进程之间是共享CPU资源的,在不同的时候进程
需要进行切换让其他进程能够占用CPU执行指令,这个切换的过程我们称之为进程上下文的切换
(Context Switch)。

被调度走的进程我们可以称之为下降进程,被调度到的进程我们则称之为上升进程。

一旦发生上下文的切换,那我们必须要把被调度走的下降进程的现场保护起来(为什么需要保存现场
呢?)。那什么是现场呢?就是我们前面所讲过的“用户可见寄存器”、“控制和状态寄存器”等。例如:
当一个进程被调度走以后,当再次被调度到CPU中运行时,他是如何知道需要执行哪条指令呢?其实是
靠PC寄存器保存了当前执行指令的下一条指令来实现的。PC寄存器的值所指向的那条指令也叫做断
点。

进程的上下文除了保存在PCB中,还有部分数据保存在内核栈(中断栈)中。

在task_strcut中使用成员变量 void *stack 指向系统分配的内核栈

其实所谓的上下文切换其实就是将下降进程的上下文保存起来,将上升进程曾经被保存的上下文恢复到
执行环境中。
1.6.2 进程上下文的切换过程
1、进程上下文的组成:进程的物理实体(代码和数据等)和支持进程运行的环境(PCB、内核栈、寄
存器等)合称为进程的上下文

由进程的程序块、数据块、运行时的堆和用户栈(两者通称为用户堆栈)等组成的用户空间信息被
称为用户级上下文
由进程标识信息、进程现场信息、进程控制信息(PCB)和系统内核栈等组成的内核空间信息被称
为系统级上下文
处理器中各寄存器的内容被称为寄存器上下文(也称硬件上下文),即进程的现场信息

2、上下文切换流程

进程地址空间切换:将上升进程的虚拟地址还原到用户空间虚拟地址寄存器ttbr0_el1中
硬件上下文切换:下降进程的现场信息保存到下降进程的PCB中,上升进程PCB中保存的现场信息
恢复到相应寄存器中

1.7 中断上下文

1.8 进程调度(CPU调度)

1.8.1 进程的调度时机
当前进程主动放弃处理机(CPU):非抢占式调度算法
进程正常终止
运行过程中发生异常而终止
进程主动请求阻塞(如等待I/O)
当前进程被动放弃处理机(CPU):抢占式调度算法

分给进程的时间片用完
有更紧急的事需要处理( 如I/O中断)
有更高优先级的进程进入就绪队列

1.8.2进程行为
计算密集型:进程的大部分在做计算、逻辑判断、循环导致cpu占用率很高的情况,称之为计算密
集型,也叫做CPU密集型,这类任务的特点是要进行大量的计算,消耗CPU资源,CPU占用率高。
例如matlab,加密解密软件、视频解码软件等

I/O密集型:进程频繁进行网络传输、读取硬盘及其他IO设备,称之为I/O密集型,这类任务的特
点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存
的速度)。例如office软件,web应用等

1.8.3 CPU调度准则
1、进程的运行其实是"CPU执行"和“I/O执行”的交替序列
2、从计算机系统层面来看,进程运行时CPU执行时间的直方图可能时下面这样的,CPU的执行时间呈
波动状态。I/O 密集型进程通常具有大量短 CPU 执行,CPU 密集型进程可能只有少量长 CPU 执行

3、CPU调度时机满足时,调度程序需要选择另外一个进程进行执行,为了完成进程的调度从而达到某
种目的,操作系统的设计者往往需要设计各种CPU调度算法。为了比较 各种CPU 调度算法,可以采用
许多比较准则。选择哪些特征来比较,对于确定哪种算法是最好的有本质上的区别。这些准则包括:
CPU 使用率:CPU处于忙状态所占时间比。为了充分利用CPU而不浪费任何CPU周期,CPU大部
分时间都处于工作状态(理想情况下为100%的时间)。 考虑到实际系统,CPU使用率应介于40%(轻
载)到90%(重载)之间。

吞吐量:在一个时间单元内进程完成的数量。

周转时间:进程提交到进程完成的时间段称为周转时间。周转时间为所有时间段之和,包括等待进
入内存、在就绪队列中等待、在 CPU 上执行和 I/O 执行。

进程在系统中停留的时间越短则说明进程响应速度越快
注意:周转时间不等于运行时间,运行时间指的是在CPU上运行时间的总和
等待时间: 进程在就绪队列中等待所花时间之和,等待时间越长,用户满意度越低

响应时间:对于计算机用户来说,会希望自己的提交的请求(比如通过键盘输入了一个调试命令)
尽早地开始被系统服务、回应。响应时间是从提交请求到产生第一响应所花费的时间。

4、进程调度的目标:

响应速度尽可能快:进程的交互性要尽可能地强,要求进程地调度尽可能地频繁
进程处理的时间尽可能短:进程从提交到最后执行完毕所花地时间
系统吞吐量尽可能大:单位时间内尽可能地完成更多地进程
资源利用率尽可能高:CPU尽可能地忙,CPU使用率尽可能地高
对所有进程要公平:所有地进程有均等地机会获得CPU
避免饥饿:避免某些进程长时间无法获得CPU
避免死锁:死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成
的一种阻塞的现象,若无外力作用,它们都将无法推进下去

5、进程调度目标的矛盾性

事实上在具体设计某个操作系统时很难兼顾以上的所有目标,有些目标之间时互相矛盾的,所以我们只
能根据操作系统所处的环境,用户对操作系统的特殊要求等等来适当的提高某些目标而降低或者忽略其
他的目标。

响应速度尽可能快和进程处理时间尽可能短之间的矛盾:频繁调度虽然可以使得进程响应速度尽可
能快,但是调度会带来一定的CPU的开销,这样CPU在调度上所损耗的时间变长从而导致进程的处
理时间变长
响应速度尽可能快和系统吞吐量尽可能大之间的矛盾:频繁调度导致进程处理时间变长,导致系统
吞吐量降低
系统吞吐量尽可能高和进程处理时间尽可能短(周转时间短)的矛盾:当系统中存在多个运行时间
短和运行时间长的进程时,如果总是调度运行时间短的进程运行可以提高系统的吞吐量,但是进程
的周转时间变长了
进程处理时间尽可能短与对所有进程要公平之间的矛盾:为了让某些进程的处理时间尽可能短

6、进程调度目标的两个量化的衡量指标

周转时间/平均周转时间

周转时间:进程提交给计算机到最终完成所花费的时间( t )

进程在系统中停留的时间越短则说明进程响应速度越快
单个进程的周转时间是无法体现整个系统的进程调度效率的,只有整个系统所有进程的平均
周转时间才能够体现
平均周转时间:进程中所有进程周转时间的平均值

平均周转时间越短,意味着这些进程在系统内停留的时间越短,因而系统吞吐量也就越
大,资源利用率也越高。
带权周转时间/平均带权周转时间

带权周转时间:作业(进程)的周转时间与系统为它提供服务的时间之比

带权周转时间越大,作业越短;带权周转时间越小,作业越长
平均带权周转时间:用于衡量调度算法对不同作业流调度性能

1.8.4 典型的调度算法
1、先到先服务调度算法

先到先服务调度(first-come,first-served (FCFS) )算法是最简单的CPU调度算法


调度规则:按照作业进入系统的时间先后来挑选作业,先进入系统的作业优先被运行

FCFS调度算法是非抢占的,只有当前面的作业执行完毕或者阻塞,后面的作业才能获得CPU运行

举例:3个进程P1、P2、P3,计算时间分别为12,3,3

任务到达顺序:P1,P2,P3

任务到达顺序:P2,P3, P1

FCFS调度算法的特点:
容易实现,但是效率不高
没有考虑作业运行时间的长短。因此一个晚来但是很短的作业可能需要等待很长时间才能被
运行,因而本算法不利于短作业
如果队列的前面是运行时间长的作业,后面是运行时间短的作业会导致平均周转时间变长,
系统响应速度变慢,系统吞吐量小

2、短作业(进程)优先(Short Job First, SJF)调度算法

非抢占调度

调度规则:参考运行时间,选取运行时间最短的作业投入运行

抢占式调度
最短剩余时间优先(SRTN):SJF抢占式版本,即当一个新就绪的进程比当前运行进程具有
更短完成时间时,系统抢占当前进程,选择新就绪的进程执行
SJF调度算法的特点:

减少平均周转时间
SRTN调度算法资源利用率和吞吐率较高,周转率较快,响应时间较短
如果是SRTN,忽视了作业等待时间,一个早来但是很长的作业将会在很长时间得不到调度,
易出现资源“饥饿”的现象(比如超市中买东西少的总是插队可能会导致买东西多的人无法结
账)
SRTN调度算法教难实现:预测一个进程的CPU时间较难,但是可以由用户提供运行时间(可
能会出现用户欺骗,长进程欺骗系统提供一个短运行时间)

3、响应比高者优先调度算法(Highest Response Ratio Next)


最高响应比优先法HRRN是介于FCFS(先来先服务算法)与SJF(短作业优先算法)之间的折中算
法,既考虑作业等待时间又考虑作业运行时间,既照顾短作业又不使长作业等待时间过长,改进了
调度性能,是非抢占调度算法

调度规则:在每次调度时先计算各个作业/进程的响应比,选择响应比最高的作业/进程为其服务

HRRN调度算法的特点:

如果作业等待时间相同,则运行时间越短的作业,其响应比越高,因此越容易被调度。因而
有利于短作业
如果作业运行时间相同,则等待时间越长的作业,其响应比越高,因此越容易被调度。因而
有利于等待时间长的作业
对于运行时间长的作业,其优先级可以随等待时间的增加而提高,当其等待足够久的时侯,
也有可能获得CPU,避免“饥饿”

4、优先数(优先级/优先权)调度算法

调度规则:根据进程优先数,把CPU分配给最高的进程
非抢占式优先级算法:在这种调度方式下,系统一旦把处理机分配给就绪队列中优先级最高的进程
后,该进程就能一直执行下去,直至完成;或因等待某事件的发生使该进程不得不放弃处理机时,
系统才能将处理机分配给另一个优先级高的就绪队列

抢占式优先级调度算法:在这种调度方式下,进程调度程序把处理机分配给当时优先级最高的就绪
进程,使之执行。一旦出现了另一个优先级更高的就绪进程时,进程调度程序就停止正在执行的进
程,将处理机分配给新出现的优先级最高的就绪进程。常用于实时要求比较严格的实时系统中,以
及对实时性能要求高的分时系统。
进程优先数分为:静态优先数和动态优先数

静态优先数:进程创建时确定,在整个进程运行期间不再改变

优先数确定规则:

基于进程所需的资源多少基于程序运行时间的长短
基于进程的类型[IO/CPU,前台/后台,核心/用户]
静态优先数调度算法不太灵活,很可能出现低优先级的作业,长期得不到调度而等待的
情况(进程“饥饿”)

动态优先数:动态优先数在进程运行期间可以改变

优先级数确定规则:

当使用CPU超过一定时长时
一当进行I/O操作后
当进程等待超过一定时长时
动态优先数使相应的优先级调度算法比较灵活、科学,可防止有些进程一直得不到调
度,也可防止有些进程长期占用处理机

5、循环轮转调度法(ROUND-ROBIN )

调度规则:把所有就绪进程按先进先出的原则排成队列,新来进程加到队列末尾
进程以时间片q为单位轮流使用CPU吗,刚刚运行一个时间片的进程排到队列末尾,等候下一轮运

队列逻辑上是环形的

循环轮转算法的优点:

公平性:每个就绪进程有平等机会获得CPU
交互性:每个进程等待(N-1)*q的时间就可以重新获得CPU
循环轮转算法的缺点:

如果时间片q太大,交互性差,甚至可能退化为FCFS调度算法
如果时间片q太下,进程切换频繁,系统开销增加
循环轮转算法的改进

时间片的大小可变
组织多个就绪队列

6、多级反馈队列调度算法(MLFQ)
不需要事先知道各种进程所需要的执行时间,还可以较好地满足各种类型进程的需要,是目前公认
的一种较好的进程调度算法。

调度规则:

设有N个队列(Q1,Q2....QN),其中各个队列对于处理机的优先级是不一样的。一般来说,
优先级Priority(Q1) > Priority(Q2) > ... > Priority(QN)。怎么讲,位于Q1中的任何一个作业
(进程)都要比Q2中的任何一个作业(进程)相对于CPU的优先级要高(也就是说,Q1中的作业
一定要比Q2中的作业先被处理机调度),依次类推其它的队列。
同一个队列中的进程按照FCFS规则放到就绪队列中。
每一个队列分配一定的时间片(队列中的的每个进程执行一个时间片的时间),若时间片运
行完时进程未结束,则进入下一优先级队列的末尾。
多级反馈队列调度算法是抢占式的算法。在k 级队列的进程运行过程中,若更上级的队列中
进入了一个新进程,则由于新进程处于优先级更高的队列中,因此新进程会抢占处理机,原
来运行的进程放回k级队列队尾。
只有第k级队列为空时,才会为k+1级队头的进程分配时间片被抢占处理机的进程重新放回原
队列队尾
各个队列的时间片是一样的吗?不一样,这就是该算法设计的精妙之处。各个队列的时间片
是随着优先级的增加而减少的,也就是说,优先级越高的队列中它的时间片就越短。同时,
为了便于那些超大作业的完成,最后一个队列QN(优先级最低的队列)的时间片一般比较大。

例题:

多级反馈队列调度算法的优缺点:

优点:相较其他调度算法相对公平(FCFS的优点),每个新到达的进程都可以很快得到响应
(ROUND-ROBIN的优点),短进程只用较少的时间就可完成(SJB的优点),不必实现估计
进程的运行时间(避免用户作假),可灵活地调整对各类进程的偏好程度,比如CPU密集型进
程、I/O密集型进程(拓展:可以将因I/O而阻塞的进程重新放回原队列,这样I/O密集型进程就
可以保持较高优先级)
缺点:如果由源源不断地短进程进来地话,第一级队列不会为空,可能会导致低优先级地进
程饥饿

7、各种调度算法的比较

第二章 进程的控制
2.1 命令行参数
在运行一个可执行程序时,系统会将我们在命令行终端输入一些参数传递给main函数,比如像下图一
样:

如上如图所示,命令行参数为./a.out ,hello,123,world,所有的命令行参数都以字符串的形式被保
存在main函数的第二个形参 字符指针数组char *argv[]中。

我们可以在程序中将所有的命令行参数进行输出:

#include <stdio.h>
int main(int argc, char *argv[]) //int main(int argc, char **argv)
{
//argc 记录命令行参数的个数
int i;
for (i = 0; i < argc; i++)
printf("%s ", argv[i]);
printf("\n");
return 0;
}

2.2 环境表
在Linux中每个进程都会接收到一张环境表,存储当前进程的环境变量,环境表是一个字符指针数组,
数组中的元素由“name=value”这样的字符串组成,该数组的最后一个元素为空指针NULL,每个进程有
一个全局变量environ包含了该指针数组的首地址。

extern char **environ;

我们可以在程序中将该进程的所有环境变量进行输出:

#include <stdio.h>
extern char **environ;

int main(int argc, char *argv[]) //int main(int argc, char **argv)
{
int i = 0;
while (environ[i] != NULL)
{
printf("%s\n", environ[i]);
i++;
}

return 0;
}

2.3 环境变量
环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数。

2.3.1 获取环境变量的值:getenv

#include <stdlib.h>

char *getenv(const char *name);

形参列表:
name:需要获取的环境变量的名字,Unix规范中定义了如下环境变量

返回值:

成功返回环境变量的值
失败返回NULL(一般是没有找到对应的环境变量的名字)
示例代码:getenv.c

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

int main(int argc, char *argv[])


{
char *value;
value = getenv("PATH");
if (value)
printf("%s\n", value);
else
printf("can not get PATH\n");

return 0;
}

2.3.2 新增或更改环境变量 -- putenv、setenv


1、putenv函数

#include <stdlib.h>

int putenv(char *string);

功能说明:添加或者修改形式为“name=value”的字符串,将其放到环境变量表中,如果name已
经存在则修改,如果name不存在则添加

形参列表:

string:“name=value”格式
返回值:
成功返回0
失败返回-1,并且errno被设置
示例代码:putenv.c

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

int main(int argc, char *argv[])


{
int ret;

ret = putenv("env=test");
if (ret)
{
perror("putenv error");
return 0;
}

printf("env: %s\n", getenv("env"));

return 0;
}

2、setenv函数

#include <stdlib.h>

int setenv(const char *name, const char *value, int overwrite);

功能说明:添加或修改指定的环境变量的值

形参列表:

name:环境变量的名字
value:环境变量的值
overwrite:如果环境变量存在,并且overwrite的值为非0,则修改,如果overwrite为0,则
不修改
返回值:

成功返回0
失败返回-1,并且errno被设置
示例代码:setenv.c

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

int main(int argc, char *argv[])


{
int ret;

ret = putenv("env=test");
if (ret)
{
perror("putenv error");
return 0;
}
printf("env: %s\n", getenv("env"));

ret = setenv("env", "TEST", 0);


printf("env: %s\n", getenv("env"));

ret = setenv("env", "TEST", 1);


printf("env: %s\n", getenv("env"));

return 0;
}

2.4 进程标识

2.4.1 进程标识的相关概念
1、每个进程都有一个非负整数表示唯一的进程ID,虽然进程ID是唯一的,但是可以重用。当一个进程
终止后,其进程ID就可以再次使用了。

2、在Linux中进程ID为0的进程,是系统创建的第一个进程,也是唯一一个没有通过fork或者
kernel_thread产生的进程。完成加载系统后,演变为进程调度、交换进程(swapper)。

3、进程ID为1通常是init进程,由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进
程。 Linux中的所有进程都是有init进程创建并运行的。在系统启动完成完成后,init将变为守护进程监
视系统其他进程。在某些Linux中init进程还是所有孤儿进程的托管进程,

2.4.2 进程标识的相关函数
1、getpid函数

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

pid_t getpid(void);

功能说明:获取当前进程的ID

2、getppid函数

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

pid_t getppid(void);

功能说明:获取当前进程的父进程的ID

3、getuid函数

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

uid_t getuid(void);

功能说明:获取当前进程所属用户的ID

示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[])


{
pid_t pid, ppid;

pid = getpid();
ppid = getppid();

printf("pid: %d, ppid: %d\n", pid, ppid);

uid_t uid;
uid = getuid();
printf("uid: %d\n", uid);

while (1);

return 0;
}

编译运行后查看程序的输出结果如下:

我们使用 ps -ef 命令对比输出的pid和ppid是否正确:

输入 id msb 命令查看msb用户的id:

2.4 进程的创建

2.4.1 fork函数的基本使用
一个现有的进程可以调用fork函数创建一个新的进程。

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

pid_t fork(void);

功能:fork()函数用于从一个已存在的进程中创建一个新进程,新进程称为子进程(child
process),原进程称为父进程(parent process)
返回值:fork函数调用一次返回两次:在父进程中返回子进程的ID,在子进程中返回0,如果失败
返回-1,将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函
数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只
会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID(进程ID 0总是由内
核交换进程使用,所以一个子进程的进程ID不可能为0)。
示例代码:fork1.c

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

int main(int argc, char *argv[])


{
pid_t pid;
pid=fork();
if(pid<0)
perror("fork");
if(0 == pid) //子进程返回
{
while(1)
{
printf("son process\n");
sleep(1);
}
}
else if (pid > 0) //父进程返回
{
while(1)
{
printf("father process\n");
sleep(1);
}
}
return 0;
}

注意:一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使
用的调度算法。

2.4.2 子进程对父进程的复制
我们先来看看manpage中对新创建的子进程的介绍:

父进程通过复制自己本身创建了一个新的进程,子进程和父进程运行在格子独立的内存地址空间中,当
fork结束后父进程和子进程的地址空间中拥有相同的内容,其中一个进程中的写内存操作、文件映射、
解除映射操作不会影响另外一个进程。
我们可以通过下面的代码来验证父子进程是否拥有相同的进程地址空间,示例代码 fork2.c:

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

int cnt2; //没有初始化的全局变量在.bss段,而且值为0


static int s_int = 100; //.data段 数据段
static int s_int2; //没有初始化的全局变量在.bss段,而且值为0

void func()
{
static int x; //.bss
static int y = 10; //.data
}

int main(int argc, char *argv[])


{
int a = 100; //栈
char buf[] = "hello"; //buf: 栈 "hello":栈上
char *p = "hello"; //p: 栈, "hello": .rodata 只读数据段

pid_t pid;
pid = fork();
if (pid > 0)
{
printf("I am your father\n");
//在父进程中修改a的值
a = 1000;
printf("parent: %d\n", a); //1000
}
else if (0 == pid)
{
printf("I am your child\n");
//在子进程中打印a的值
printf("child: %d\n", a); //100 因为子进程和父进程的地址空间
// 是互相独立的,因为子进程拷贝了父进程的数据,所以在子进程中也有a,但是a的值是
//父进程修改前的值
}

return 0;
}

实时上多数的系统在实现fork函数时,并不会马上将父进程地址空间中的数据段、栈和堆、代码段等拷
贝到子进程的地址空间中,因为这样做fork函数的效率很低,而是采用了写时拷贝(Copy-On-Write)
技术,这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读的。如果父、子进程中的任
一个试图修改这些区域,则内核只将修改区域的那块内存拷贝到子进程的地址空间中。

2.4.3 关于fork函数的一些误区
我们知道fork函数在父进程中返回子进程的ID,子进程中返回0,所以在进行多进程编程时,我们将需
要在父进程中执行的代码写在 if (pid > 0) 逻辑中,将需要在子进程中执行的代码写在 if (0 == pid) 这
个逻辑中。正因如此,很多人误以为 if (pid > 0) 逻辑中的代码属于父进程中,if (0 == pid) 逻辑中的代
码是属于子进程的。这种理解是错误的!事实上子进程和父进程地址空间中的代码段是相同的,只是在
各自进程中fork函数的返回值不一样所以才执行不同的逻辑分支而已。a

2.4.4 fork函数的使用场景
当我们需要和父进程并发执行的某些代码是计算量比较大(子进程是CPU密集型)时,可以优先考
虑使用fork创建子进程。使用多进程可以提供CPU的利用率,提升程序的运行速度。
一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后
立即调用exec执行指定的程序。例如我们设计一个智能家居系统,如果需要播放音乐或者视频
时,父进程会创建一个子进程去执行音乐播放器或者视频播放器程序。

2.4.5 vfork函数
vfork函数的调用序列和返回值与for相同。

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

pid_t vfork(void);

vfork创建新进程的主要目的在于调用exec函数执行另外的一个新程序,在没调用exec或exit之
前,子进程的运行是与父进程共享数据段的
vfork调用中,子进程先运行,父进程阻塞,直到子进程调用exec或者exit,在这以后,父子进程
的执行顺序不再被限制
实例代码:vfork.c

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

int main(int argc, char *argv[])


{

pid_t pid;
pid = vfork();
if (pid > 0)
{
printf("I am your father\n");

}
else if (0 == pid)
{
printf("I am your child\n");
sleep(3);

execl("/bin/ls", "ls", "-l", NULL);


}

return 0;
}

编译并且运行可执行程序后,我们发现首先调度的是子进程,当子进程调用execl函数后,父进程被调
度到。
由于某些系统并不支持vfork函数,所有在考虑到程序的可移植性上,不推荐大家使用vfork函数,以下
说明是摘抄自《Unix环境高级编程 第三版》

2.4.6 fork函数的笔试题
1、下面的程序在运行时会打印几个a?

2、以下两段代码在运行时有何区别?

2.5 进程的终止

2.5.1 进程的终止方式
有8种方式使进程终止(termination),其中5种为正常终止,它们是:

从main返回
调用exit
调用_exit或_Exit
最后一个线程从其启动例程返回
最后一个线程调用pthread_exit
异常终止有3种方式,它们是:

调用abort
接到一个信号并终止
最后一个线程对取消请求作出响应

2.5.2 exit函数
有三个函数用于正常终止一个程序:_exit和_Exit立即进入内核,exit则先执行一些清理处理(包括调用
执行各终止处理程序,关闭所有标准I/O流等),然后进入内核。

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

exit函数总是执行一个标准I/O库的清理关闭操作:为所有打开流调用fclose函数。这会造成所有缓冲
的输出数据都被冲洗(写到文件上)。而_exit和_Exit则不会。

示例代码:exit.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main(int argc, char *argv[])


{
printf("hello");
// exit(0);
//_exit(0);
_Exit(0);
return 0;
}

三个exit函数都带一个整型参数,称为终止状态(或退出状态,exit status),被终止的进程会将
status&0377 的结果给父进程 。

2.6 进程的等待

2.6.1 僵尸进程
当进程终止后,系统会把这个进程标记为zombie状态,如果该进程的父进程不回收退出进程的资源,
那么这个进程将会变成僵尸进程,僵尸进程并没有真正的退出,该进程的ID会一直被占用。如果系统中
产生大量的僵尸进程,系统将因为没有可用的进程号而导致系统不能产生新的进程.,此即为僵尸进程的
危害,应当避免。

僵尸进程的产生:子进程先于父进程退出,并且父进程没有回收子进程的资源。

僵尸进程的演示:zombie.c

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

int main(int argc, char *argv[])


{

pid_t pid;
pid = fork();
if (pid > 0)
{
printf("I am your father\n");

while (1);

}
else if (0 == pid)
{
printf("I am your child\n");
}

return 0;
}
因为子进程退出后,父进程没有回收子进程的资源,所以子进程就变成了僵尸进程,执行完程序后,我
们可以使用top命令查看僵尸进程的个数

2.6.2 孤儿进程
如果父进程先于子进程退出,该父进程的子进程将成为孤儿进程。孤儿进程将被系统的某个进程所托管
(以往的Linux系统中由init进程托管,现在ubuntu系统中由 /lib/systemd/systemd --user进程所托
管),并由该进程对它们完成状态收集工作。

孤儿进程演示:orphan.c

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

int main(int argc, char *argv[])


{

pid_t pid;
pid = fork();
if (pid > 0)
{
printf("I am your father\n");
}
else if (0 == pid)
{
while (1);

printf("I am your child\n");


}

return 0;
}

编译后执行程序我们发现,子进程的父进程为906

我们发现进程ID为906的进程为 /lib/systemd/systemd --user

2.6.3 进程的等待
1、当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事
件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以
选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号的系
统默认动作是忽略它。

2、linux 下提供了两个等待函数 wait()、waitpid(),阻塞等待子进程的退出,并且回收子进程的资


源。 当调用这两个函数时可能会发生情况:
如果其所有子进程都还在运行,则阻塞
如果一个子进程已终止,等待的父进程获取其终止状态,立即返回
如果它没有任何子进程,则立即出错返回

3、wait函数

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

//阻塞等待子进程的状态发生变化
pid_t wait(int *wstatus);

通过wstatus获取子进程的退出信息:
WIFEXITED(wstatus):如果子进程是正常退出,调用了exit(),_exit(),在main函数中调用
return
WEXITSTATUS(wstatus):返回子进程的退出值exit,_exit的参数,前提是
WIFEXITED(wstatus)的返回值为true

WIFSIGNALED(wstatus): 如果子进程是被信号终止返回true
WTERMSIG(wstatus):返回终止子进程的信号的编号,前提:WIFSIGNALED的返回值是true

返回值:成功终止进程的ID,失败返回-1

示例代码:wait.c

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

int main(int argc, char *argv[])


{

pid_t pid;
pid = fork();
if (pid > 0) //pid的值是创建的新的子进程的PID
{
printf("I am your father,My PID:%d, your PID: %d, My parent: %d\n",
getpid(), pid, getppid());

int wstatus;
wait(&wstatus); //阻塞等待子进程的退出,白发人送黑发人
if (WIFEXITED(wstatus))
{
printf("exit status:%d\n", WEXITSTATUS(wstatus));
}
else if (WIFSIGNALED(wstatus))
{
printf("exit by signal: %d\n", WTERMSIG(wstatus));
}
}
else if (0 == pid)
{
printf("I am your child, Parent PID: %d, My PID: %d\n", getppid(),
getpid());

//while (1);
}
}

4、waitpid函数

waitpid等待指定的子进程状态的变化(退出)

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

pid_t waitpid(pid_t pid, int *wstatus, int options);

形参列表:

pid:
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出
了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一
样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,
waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
wstatus:保存子进程的退出状态

options:options提供了一些额外的选项来控制waitpid,目前在Linux中支持WNOHANG和
WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,比如:
waitpid(-1, NULL, WNOHANG | WUNTRACED ),如果我们不想使用它们,也可以把
options设为0,比如:waitpid(-1, NULL, 0 )

WNOHANG:如果没有子进程退出则立即返回(非阻塞模式)
WUNTRACED:如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会(跟
踪调试,很少用到)
返回值:

成功返回状态发生变化的进程ID
如果设置了WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
如果调用中出错,则返回-1,并且errno被设置为ECHILD

示例代码:waitpid.c

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

int main(int argc, char *argv[])


{

pid_t pid;
pid = fork();
if (pid > 0) //pid的值是创建的新的子进程的PID
{
printf("I am your father,My PID:%d, your PID: %d, My parent: %d\n",
getpid(), pid, getppid());

int wstatus;
waitpid(pid, &wstatus, 0); //阻塞等待子进程的退出,白发人送黑发人
if (WIFEXITED(wstatus))
{
printf("exit status: %d\n", WEXITSTATUS(wstatus));
}
else if (WIFSIGNALED(wstatus))
{
printf("exit by signal:%d\n", WTERMSIG(wstatus));
}
}
else if (0 == pid)
{
printf("I am your child, Parent PID: %d, My PID: %d\n", getppid(),
getpid());

//while (1);
}
}

2.7 exec 函数族


exec 函数族,是由六个 exec 函数组成的。

#include <unistd.h>

extern char **environ;

int execl(const char *path, const char *arg, ... /* (char *) NULL */);
l(list):参数地址列表,以空指针结尾
path:必须指定命令的路径,如果不指定则在当前目录下查找命令

int execlp(const char *file, const char *arg, .../* (char *) NULL */);
p(path) 按 PATH 环境变量指定的目录搜索可执行文件。
以 p 结尾的 exec 函数取文件名做为参数。当指定 filename 作为参数时,若 filename 中
包含/,则将其视为 路径名,并直接到指定的路径中执行程序。
int execle(const char *path, const char *arg, .../*, (char *) NULL, char * const
envp[] */);
e(environment): 存有环境变量字符串地址的指针数组的地址。execle 和 execve 改变的
是 exec 启动的程序的环境变量(新的 环境变量完全由
environment 指定),其他四个函数启动的程序则使用默认系统环境变量。

int execv(const char *path, char *const argv[]);


int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);

注意:exec函数簇会用新的进程的镜像替换掉原来的进程镜像(将新的进程的代码段替换掉原来进程的代码
段,会执行新的进程的代码)
在实际工作中如果需要使用exec函数簇我们一般fork一个子进程,在子进程中调用execl函数簇
示例代码:exec.c

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

int main(int argc, char *argv[])


{
//execl的第一个参数要填写可执行程序的路径
int ret;
//ret = execl("/bin/ls", "ls", "-a", "-l", "-h", NULL);

ret = execlp("ls", "ls", "-a", "-l", "-h", NULL);

char *env[]={"USER=ME", "pwd=123456", NULL};


execle("./test", "test", NULL, env);
//test程序的源代码如下
/*
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf("USER = %s\n", getenv("USER");
printf("pwd = %s\n", getenv("pwd");
return 0;
}
*/

char *arg[]={"ls", "-a", "-l", "-h", NULL};


ret = execv("/bin/ls", arg);

ret = execve("./test", arg, env);

ret = execvp("ls", arg);

if (-1 == ret)
perror("execl");
return 0;
}

什么时候用execl,什么时候用execlp呢?

如果执行的命令是用户的可执行程序使用execl
如果执行的命令是系统的命令可以使用execlp

2.8 system 函数

#include <stdlib.h>

int system(const char *command);

功能说明:

system 会调用 fork 函数产生子进程,子进程调用 exec 启动/bin/sh -c command来执行参


数 command字符串所

代表的命令,此命令执行完后返回原调用进程。如果使用system执行的程序没有退出,则
system函数阻塞。

形参:
command:要执行的命令的字符串
返回值:

如果 command 为 NULL,则 system()函数返回非 0,一般为 1

如果 system()在调用/bin/sh 时失败则返回 127,其它失败原因返回-1

注意:system 调用成功后会返回执行 shell 命令后的返回值。其返回值可能为 1、127 也可


能为-1,故最好应再

用WIFEXITED来确认执行是否成功。

示例代码:system.c

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

int main(int argc, char *argv[])


{
int status;
status = system("ls -l");
if(WIFEXITED(status))
printf("the exit status is %d \n", WEXITSTATUS(status));
else
printf("abnornamal exit\n");

status = system("./system_test"); //程序system_test的源代码如下


/*
#include <stdio.h>
int main()
{
printf("system test...\n");
while (1);
return 0;
}
*/
if(WIFEXITED(status))
printf("the exit status is %d \n", WEXITSTATUS(status));
else if (WIFSIGNALED(status))
{
printf("exit by signal: %d\n", WTERMSIG(status));
}

return 0;
}

第三章 进程间通信
3.1 进程间通信概述
因为进程间的地址空间是互相独立的,一般情况下是不能互相访问的(共享内存除外),当多个进程间
需要交换信息时,可以通过磁盘上的普通文件交换信息,或者注册表、数据库等方式进行信息的交换,
但是这些方式都需要进行磁盘的IO操作,效率非常低,所以一般不采用这样的方式,广义上这些也是进
程间的通信方式,但是一般不把这算作“进程间通信”。

我们常说的进程间通信(IPC,Inter-process communication)是一组编程接口,通过在内核中开辟
一块缓冲区,需要通信的进程通过对缓冲区的读/写操作实现信息的交换。根据通信的实现方式的不
同,在Linux中常用的进程间通信有如下几种:

进程间通信目的:

数据传输:一个进程需要将它的数据发送给另一个进程。
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件。
进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够
拦截另一个进程 的所有操作,并能够及时知道它的状态改变。
3.2 信号

3.2.1 信号的概念
1、信号是Linux系统中用于进程间互相通信或者操作的一种机制,信号可以在任何时候发给某一进程,
而无需知道该进程的状态。信号是软中断,是软件层次上对中断机制的一种模拟,是一种异步通信方
式,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪
些系统事件,信号事件主要有两个来源:

硬件来源:用户按键输入 Ctrl+C 退出、硬件异常如无效的存储访问等。


软件终止:终止进程信号、其他进程调用 kill 函数、软件异常产生信号。

2、在Linux中每个信号的名字都以字符 SIG 开头,每个信号都可以用一个宏名以及一个无符号的正整数


表示,我们可以在终端输入 kill -l 命令查看Linux系统中的信号

需要注意的几点:

没有0号信号
没有32,33号信号
从31号信号分开,前面的是普通信号,后面的是实时信号,本篇介绍的是普通信号
信号和前面的标号是一致的,可以使用标号,也可以使用宏名称

我们也可以在终端输入 man 7 signal 查看Linux中常用的信号:

Signal Value Action Comment


──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard ctrl + c
SIGQUIT 3 Core Quit from keyboard ctrl+\
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating-point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no
readers; see pipe(7)
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal ctrl+z
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background process

注意:进程在接受到这些信号后都有一个默认的AVCTION所指定的处理方式。绝大的部分的信号的默
认处理方式都是终止进程。

3、Linux内核对信号的处理有如下几种方式:

忽略此信号:接收到此信号后没有任何动作
执行系统默认动作:对大多数信号来说,系统默认动作是用来终止该进程
执行用户自定义信号处理函数:使用用户定义的信号处理函数处理该信号

注意:SIGKILL 和 SIGSTOP 不能更改信号的处理方式

4、信号处理流程:

应用程序运行或者阻塞时,假如产生了某个信号,系统会将该信号在内核中进行注册。
信号注册方法:在进程表的表项中有一个软中断信号域,该域中每一位对应一个信号。内核给一个
进程发送软中断信号的方法,是在进程所在的进程表项的信号域设置对应于该信号的位。如果信号
发送给一个正在睡眠的进程,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程
表中信号域相应的位,而不唤醒进程。如果发送给一个处于可运行状态的进程,则只置相应的域即
可。进程的task_struct结构中有关于本进程中未决信号的数据成员: struct sigpending
pending:

另外一个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链
(称之为"未决信号链")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指
向下一个sigqueue结构:

信号在进程中注册指的就是信号值加入到进程的未决信号集sigset_t signal(每个信号占用一位)中,
并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集
中,表明进程已经知道这些信号的存在,但还没来得及处理

当进程执行了系统调用或者产生了中断、异常而进入内核态,并且从内核态返回用户态之前会检查
未决信号链,处理未决的信号,然后将该信号从链上删除,同时将未决信号集合中对应的位置置
0。如果用户注册了信号的处理函数,则返回用户态执行对应的信号处理函数,执行完信号处理函
数后会执行系统调用sigreturn再次进入内核态,最后在内核态返回用户态时继续从上次被中断的
地方往下执行。
需要注意 的是:如果进程处于“浅度睡眠(可被中断阻塞)”状态,当一个信号到达时,可以唤醒该
进程。

3.2.1 signal函数
1、signal函数的基本使用

如果进程需要捕获某个信号,或者需要处理某个信号,需要在进程中安装该信号(注册信号的处理函
数)。linux主要有两个函数实现信号的安装:signal()、sigaction()。

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

功能说明:注册一个信号处理函数,当指定的信号产生时,系统会自动调用handler所指定的函数

形参列表:
signum:需要注册的信号的编号或者宏名

handler:

忽略该信号:SIG_IGN
执行系统默认动作:SIG_DFL
自定义信号处理函数:信号处理函数名
返回值:

成功:返回函数地址,该地址为此信号上一次注册的信号处理函数的地址
失败:返回 SIG_ERR

示例代码:signal.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

void quit_handler(int signum) //信号处理函数


{
switch (signum)
{
case SIGINT:
printf("get A signal: SIGINT\n");
break;
case SIGQUIT:
printf("get A signal: SIGQUIT\n");
break;
}

int main(int argc, char *argv[])


{
//ctrl + c
signal(SIGINT, quit_handler);
signal(SIGQUIT, quit_handler);
//SIGKILL信号的处理方式是不能被改变的
signal(SIGKILL, quit_handler);
while (1);

return 0;
}

我们可以使用signal注册一个SIGCHLD信号的处理函数,通过该信号在父进程中可以获知子进程的退
出。示例代码:sigchld.c

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

pid_t pid;

void quit_handler(int signum) //信号处理函数


{
printf("child process exit \n");
int status;
wait(&status);
}

int main(int argc, char *argv[])


{
pid = fork();

if (pid > 0)
{
printf("parent process\n");
signal(SIGCHLD, quit_handler);
}
else if (0 == pid)
{
printf("child process\n");
return 0;
}

while (1);

return 0;
}

2、signal函数的使用注意事项

signal有两种实现方式:

第一种作为Linux系统调用实现,运行结果与signal.c程序的运行结果一致:当我们使用signal函数
注册完信号处理函数后,每次接受到指定的信号都会自动执行注册的信号处理函数
第二种作为标准C库中的库函数实现,编译时加编译选项 -std=c99 ,作为库函数实现的signal函
数,第一次接收到指定的信号后,在执行handler函数指针所指向的函数之前,会讲信号的
handler指针恢复,这样当下次接受到信号后,会执行默认的处理动作,所以不能重复处理同一信
号。

如果需要重复处理该信号怎么办呢?在信号处理函数中再次调用signal函数进行信号处理函
数的注册即可。

3.2.2 sigaction函数
sigaction函数的功能是检查或修改与指定信号相关联的处理动作(可同时两种操作)。

sigaction是POSIX的信号接口,而signal()可以是标准C的信号接口(如果程序必须在非POSIX系统上运
行,那么就应该使用这个接口)。

#include <signal.h>

int sigaction(int signo, const struct sigaction *restrict act, struct sigaction
*restrict oact);

结构sigaction定义如下:
struct sigaction{
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flag;
void (*sa_sigaction)(int,siginfo_t *,void *);
};

形参列表:

signo:需要捕获的信号编号
act:

sa_handler 信号处理函数
sa_mask字段说明了一个信号集,在调用该信号捕捉函数之前,这一信号集要加进进程的信
号屏蔽字中。仅当从信号捕捉函数返回时再将进程的信号屏蔽字复位为原先值。
sa_flag是一个选项,主要可以设置为:

SA_RESTART:使被信号打断的系统调用自动重新发起
SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号
SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程
如果退出也不会成为僵尸进程
SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号
SA_RESETHAND:信号处理之后重新设置为默认的处理方式
SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数
sa_sigaction:最后一个参数是一个替代的信号处理程序,当设置SA_SIGINFO时才会用他。不做
重点讲解

示例代码:sigaction.c

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

void quit_handler(int signum) //信号处理函数


{
switch (signum)
{
case SIGINT:
printf("get A signal: SIGINT\n");
break;
case SIGQUIT:
printf("get A signal: SIGQUIT\n");
break;
}

int i;
for (i = 0; i < 5; i++)
{
printf("sleeping ...\n");
sleep(1);
}
}

int main(int argc, char *argv[])


{
int i = 0;
struct sigaction act, oldact;
act.sa_handler = quit_handler;

sigaddset(&act.sa_mask, SIGQUIT);

act.sa_flags = SA_RESETHAND | SA_NODEFER;


//act.sa_flags = 0; //在处理信号的过程中如果重新接受到了该信号则阻塞

sigaction(SIGINT, &act, &oldact);

while (1);

return 0;
}
3.2.3 可重入函数
函数的重入:在多个执行流程中,同时进入一个函数运行
函数可重入:函数重入之后,不会造成数据二义或者逻辑混乱
函数不可重入:函数重入之后,有可能会造成数据二义或者逻辑混乱

我们来思考下面的这个程序在运行时可能会出现什么问题? reentrant.c

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

int a = 1, b = 1;

int test() {
a++;
sleep(3);
b++;
return a+b;
}

void sigcb(int no) {


printf("signal sum:%d\n", test());
}
int main()
{
signal(SIGINT, sigcb);
printf("main sum:%d\n", test());
return 0;
}

运行结果如下:

我们发现主流程执行函数和执行中断处理函数虽然是同一个函数,但是函数的执行结果不一样。那么这
个函数就是不可重入的。

编写可重入函数:

不使用(返回)静态的数据、全局变量(除非用信号量互斥)
不调用动态内存分配、释放的函数
不调用任何不可重入的函数(如标准 I/O 函数)

注意:在后面的课程中我们还会进一步学习函数的可重入,所以该知识点目前只需了解即可。

3.2.4 Linux信号集
在Linux系统的实际应用中,常常需要将多个信号组合起来使用,这种用来表示多个信号的数据类型被
称为信号集(signal set),其定义格式为sigset_t。
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t

常用于操作信号集的函数有如下5个:

#include <signal.h>

int sigemptyset(sigset_t *set) 初始化由set指定的信号集,信号集里面的所有信号被清空;


int sigfillset(sigset_t *set) sigfillset初始化set所指向的信号集,使其中所有信号的对应
bit置位,表示该信号集的有效信号包括系统支持的所有信号。
int sigaddset(sigset_t *set, int signum) 在set指向的信号集中加入signum信号;
int sigdelset(sigset_t *set, int signum) 在set指向的信号集中删除signum信号;
int sigismember(const sigset_t *set, int signum) 判定信号signum是否在set指向的信号集
中。
int sigpending(sigset_t *set) 获取候被阻塞挂起的信号集合

3.2.5 信号的阻塞
在实际应用中,有时候既不希望进程在接收到信号时立刻中断,也不希望该信号完全被忽略,而是希望
进程延迟处理。这可以通过阻塞信号的方法来实现。每个进程都有一个用来描述哪些信号递送到进程时
将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。Linux提供了sigprocmask函
数用于信号的阻塞。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset));

形参列表:

how:
SIG_BLOCK 在进程当前阻塞信号集中添加set指向信号集中的信号
SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的
阻塞
SIG_SETMASK 更新进程阻塞信号集为set指向的信号集
set:指向信号集的指针,在此专指新设的信号集,如果仅想读取现在的屏蔽值,可将其置为
NULL

oldset:也是指向信号集的指针,在此存放原来的信号集
返回值:成功返回0。失败返回-1,errno被设为EINVAL

sigprocmask函数一般都配合信号集使用:sigprocmask.c

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

void quit_handler(int signum) //信号处理函数


{
switch (signum)
{
case SIGINT:
printf("get A signal: SIGINT\n");
break;
case SIGQUIT:
printf("get A signal: SIGQUIT\n");
break;
}
}

int main(int argc, char *argv[])


{
//注册两个信号的处理函数
signal(SIGINT, quit_handler);
signal(SIGQUIT, quit_handler);

sigset_t newmask,pendmask;

sigemptyset(&newmask);

//将SIGQUIT SIGINT信号添加到信号集中
sigaddset(&newmask , SIGQUIT);
sigaddset(&newmask , SIGINT);

//将信号集中的信号阻塞
sigprocmask(SIG_BLOCK , &newmask , NULL);

sleep(5);

//获取被阻塞的信号集合
sigpending(&pendmask);

//检查SIGINT信号是否被阻塞
if(sigismember(&pendmask , SIGINT))
{
printf("SIGINT pending\n");
}

//检查SIGQUIT信号是否被阻塞
if(sigismember(&pendmask , SIGQUIT))
{
printf("SIGQUIT pending\n");
}
//将信号集中的信号解除阻塞
//sigprocmask(SIG_UNBLOCK , &newmask , NULL);
while (1);

return 0;
}

3.2.5 信号的发送
可以在进程中使用kill函数向另外一个进程发送指定的信号

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

int kill(pid_t pid, int signum);


功能:给指定进程发送信号。

形参列表:

pid:

pid>0: 将信号传送给进程 ID 为 pid 的进程


pid=0: 将信号传送给当前进程所在进程组中的所有进程
pid=-1: 将信号传送给系统内所有的进程
pid<-1: 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值
signum:信号的编号

返回值:

成功返回 0
失败返回 -1

示例代码:父进程给子进程发送信号 kill.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
pid_t pid; pid = fork();
if(pid<0)
perror("fork");
if(pid==0)
{
int i = 0;
for(i=0; i<5; i++)
{
printf("in son process\n");
sleep(1);
}
}
else
{
printf("in father process\n");
sleep(2);
printf("kill sub process now \n");
kill(pid, SIGINT);
}
return 0;
}

3.2.6 SIGALRM信号
在Linux中可以利用SIGALRM信号进行定时,Linux系统提供了alarm()函数,可以定时发送一个
SIGALRM信号
#include <unistd.h>

unsigned int alarm(unsigned int seconds);


在seconds秒内产生一个SIGALRM信号
注意:调用一次alarm函数在seconds秒内有且只有一个SIGALRM信号
alarm函数的返回值:如果调用此alarm()前,进程已经设置了闹钟时间,则返回上一个闹钟时间的
剩余时间,否则返回0

示例代码:alarm.c

void func(int signu)


{
alarm(1);
printf("alarm ...\n");
}

void main()
{
alarm(1);
signal(SIGALRM, func);
while (1)
{
}
}

笔试题:

int n = alarm(alarm(3));
printf("%d\n", n); // 3 但是alarm(3)的返回值0, alarm(alarm(3)) == alarm(0)

3.2.7 定时器的使用
在Linux应用程序开发过程中,我们经常需要在程序中设置一些定时器,当到达某个时间节点时需要完
成特定的功能(例如网络服务器定时1分钟给客户端发送心跳包,客户端在每个小时的整点向服务端发
送数据等等)。

在Linux中可以使用settimer函数设置定时器:

#include <sys/time.h>

int setitimer(int which, const struct itimerval *new_value,


struct itimerval *old_value);

设置一个定时器
参数:
which:
ITIMER_REAL:根据系统时间设置定时器
ITIMER_VIRTUAL:根据进程中所花的时间

newval是函数特有的struct itimeval,it_value储存初始间隔,it_interval储存重复间隔,
定义如下:
struct itimerval {
struct timeval it_interval; //it_interval指定间隔时间
struct timeval it_value; //it_value指定初始定时时间,如果只指定it_value,就是实现
一次定时;如果同时指定 it_interval,则超时后,系统会重新初始化it_value为it_interval,实现
重复定时
};
struct timeval {
time_t tv_sec;
suseconds_t tv_usec;
};
其中timeval结构体是提高精度体现,suseconds_t储存毫秒值。setitimer同alarm的作用类似,同样时
通过计时向进程发送信号,只是前者具有循环间隔发送的功能。

示例代码:settimer.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>

typedef void (*FUNC)();

void func(int signum)


{
printf("alarm ...\n");
}

void main()
{
signal(SIGALRM, func);

struct itimerval value;


/*设定初始时间计数为1秒0微秒*/
value.it_value.tv_sec=1;
value.it_value.tv_usec=0;
/*设定执行任务的时间间隔也为1秒0微秒*/
value.it_interval= value.it_value;
/*设置计时器ITIMER_REAL*/
setitimer(ITIMER_REAL,&value, NULL);

while (1);
}

实际应用示例代码:C语言版本:settimer_exp.c

//设计一个定时器,实现每隔5s打印hello,每到整10分钟打印world
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <time.h>
typedef void (*FUNC)();

enum en
{
FIVE,
TEN
};
int seconds[] = {5, 600};

//设计一个链表,节点描述定时的时间和对应的处理函数
typedef struct Node
{
time_t t;
FUNC f;
int flag;
struct Node *next;
}Node;

typedef struct List


{
Node *head;
Node *tail;
}List;

List *list = NULL;

void func1()
{
printf("hello\n");
}

void func2()
{
printf("world\n");
}

void tail_insert(List *list, int flag, FUNC f)


{
if (NULL == list)
return ;

time_t t = time(NULL);
//添加节点
Node *node = (Node *)malloc(sizeof(Node));
if (flag == TEN)
{
//获取当前的分钟数
struct tm *tm;
tm = localtime(&t);
tm->tm_min = tm->tm_min/10 * 10;
tm->tm_sec = 0;

t = mktime(tm);
}
node->t = t+seconds[flag];
node->f = f;
node->flag = flag;
node->next = NULL;
//判断链表是否为空
if (list->head == NULL)
{
list->head = node;
list->tail = node;
return ;
}

list->tail->next = node;
list->tail = node;
}

List *list_init()
{
List *list = (List *)malloc(sizeof(List));
list->head = NULL;
list->tail = NULL;

tail_insert(list, FIVE, func1);


tail_insert(list, TEN, func2);

return list;
}

void func(int signum)


{
time_t t = time(NULL);

//遍历链表
Node *tmp;
tmp = list->head;
while (tmp)
{
if (tmp->t == t)
{
tmp->f();
tmp->t += seconds[tmp->flag];
}
tmp = tmp->next;
}
}

void timer_init()
{
signal(SIGALRM, func);

struct itimerval value;


/*设定初始时间计数为1秒0微秒*/
value.it_value.tv_sec=1;
value.it_value.tv_usec=0;
/*设定执行任务的时间间隔也为1秒0微秒*/
value.it_interval= value.it_value;
/*设置计时器ITIMER_REAL*/
setitimer(ITIMER_REAL,&value, NULL);
}

int main()
{
list = list_init();

timer_init();

while (1);
return 0;
}

实际应用示例代码:C++版本:settimer_exp.cpp

//设计一个定时器,实现每隔5s打印hello,每到整10分钟打印world
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include <time.h>
#include <list>

using namespace std;

enum en
{
FIVE,
TEN
};

//函数指针
typedef void (*FUNC)();

//定义一个任务结构体
typedef struct task
{
time_t t; //任务触发的时间点
FUNC f; //任务对应的处理函数
int type; //任务类型
}Task;

//任务链表
list<Task> tList;

int seconds[] = {5, 600};

void func5()
{
printf("hello\n");
}

void func10()
{
printf("world\n");
}

void handler(int signum)


{
time_t t = time(NULL);
//遍历任务链表
list<Task>::iterator it;

for (it = tList.begin(); it != tList.end(); it++)


{
if (it->t == t)
{
it->f();
it->t += seconds[it->type];
}
}
}

void timer_init()
{
signal(SIGALRM, handler);

struct itimerval value;


/*设定初始时间计数为1秒0微秒*/
value.it_value.tv_sec=1;
value.it_value.tv_usec=0;
/*设定执行任务的时间间隔也为1秒0微秒*/
value.it_interval= value.it_value;
/*设置计时器ITIMER_REAL*/
setitimer(ITIMER_REAL,&value, NULL);
}

void task_init(list<Task> &tList)


{
Task node;

//添加5s结点
time_t t = time(NULL);

node.t = t+seconds[FIVE];
node.f = func5;
node.type = FIVE;

tList.push_back(node);

//添加10分钟结点
struct tm *tm;
tm = localtime(&t);
tm->tm_min = tm->tm_min/10 * 10; //获取整10分
tm->tm_sec = 0;

t = mktime(tm);
node.t = t+seconds[TEN];
node.f = func10;
node.type = TEN;

tList.push_back(node);
}

int main()
{
task_init(tList);
timer_init();

while (1);

return 0;
}

3.2.8 用户自定义信号
Linux系统提供了两个信号,用户可以对这两个信号进行自定义,这两个信号分别为:

SIGUSR1
SIGUSR2

用户可以利用这两个自定义的信号,在进程间通信时进行自定义的约定,例如A进程完成了某件事情后
向B进程发送SIGUSR1或者SIGUSR1信号,在后面我们会做一个简单的播放器,在该项目中我们会使用
到这两个信号。

示例代码:sigusr.c

void func(int signum)


{
printf("hello\n");
}
void main()
{
pid_t pid;

pid = fork();
if (pid > 0)
{
sleep(1);
//向子进程发送SIGUSR1信号
kill(pid, SIGUSR1);
}
else if (pid == 0)
{
signal(SIGUSR1, func);
}
while (1);
}

3.3 管道

3.3.1 管道的概念
管道的本质:伪文件(操作时当做文件,其实他不是文件),其实是内核中的一段缓冲区,管道有两个
文件描述符:读文件描述符、写文件描述符。
管道在内核中的实现原理:用一个环形队列,数据一旦被读走,在管道中不存在了,数据不可以反复读
写。并且管道时半双工工作模式,数据只能在一个方向上传输。

Linux中的管道分为如下两类:

1. 无名管道:在内核中没有名字,只能用于具有“血缘关系”(具有相同的祖宗进程)的进程间通信
2. 有名管道:在内核中有名字,用于任意进程间通信

3.3.2 无名管道
Linux系统中可以使用pipe函数创建无名管道,调用pipe函数时在内核中开辟一块缓冲区(称为管道)
用于通信,它有一个读端一个写端,然后通过pipefd参数传出给用户程序两个文件描述符,pipefd[0]指
向管道的读端,pipefd[1]指向管道的写端。所以管道在用户程序看起来就像一个打开的文件,通过
read(pipefd[0]);或者write(pipefd[1]);向这个文件读写数据其实是在读写内核缓冲区

#include <unistd.h>

int pipe(int pipefd[2]);

形参列表:

pipefd:pipefd保存两个文件描述符

pipefd[0]: 读端
pipefd[1]:写端
返回值:

成功返回0
失败返回-1,errno被设置
注意事项:当我们向管道的写端写入数据时一般需要将读端关闭,同样的如果需要从读端读取数据
时需要将写端关闭
示例代码:pipe.c

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>

void func(int signo)


{
printf("broken pipe ...\n");
}

int main()
{
int fd[2]; //保存管道的两个文件描述符
int ret;
ret = pipe(fd); //创建一个无名管道
if (ret == -1)
{
printf("error\n");
return 0;
}

pid_t pid;
pid = fork();

#if 0
if (pid > 0)
{
sleep(5);
//关闭读端
close(fd[0]);
write(fd[1], "hello", 5);
write(fd[1], "world", 5);
printf("parent write data\n");
}
else if (pid == 0)
{
//关闭写端
close(fd[1]);
printf("child read data\n");
char buf[10];
int n;
while (1)
{
memset(buf, 0, sizeof(buf));
//默认阻塞读 具体的实现
n = read(fd[0], buf, sizeof(buf));
printf("n: %d\n", n);
if (n > 0)
printf("%d: %s\n", n, buf);
}
}
#endif
//模拟一种情况:子进程将管道的读端关闭,测试父进程像写端写入数据后会发生什么情况
//此种情况,父进程在像一个被子进程关闭了读端的管道中写入数据时会产生一个 SIGPIPE,会将父进
程终止
//当进程向一个broken的管道中写入数据时,会产生一个 SIGPIPE
if (pid > 0)
{
signal(SIGPIPE,func);
sleep(5);

//关闭读端
close(fd[0]);

write(fd[1], "hello", 5);

while (1);
}
else if (0 == pid)
{
//关闭读端
close(fd[0]);
}

return 0;
}

注意:A进程在向一个被B进程关闭了读端的管道中写入数据时会产生一个 SIGPIPE,会将A进程
终止

3.3.3 有名管道
Linux系统中可以使用mkfifo函数创建有名管道,有名管道可以用于任意进程间通信。

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

int mkfifo(const char *pathname, mode_t mode);

exp:mkfifo("/tmp/1.fifo", 0644)

形参列表:

pathname:根据pathname创建一个fifo文件
mode:指定文件的权限
返回值:
成功返回0
失败返回-1,errno被设置

示例代码:mkfifo_w.c

//写端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO "/tmp/1.fifo"

void main()
{
int ret;
//unlink(FIFO);
if (access(FIFO, F_OK))
{
//创建一个管道文件
ret = mkfifo(FIFO, 0644);
if (-1 == ret)
{
printf("error\n");
return ;
}
}
//打开管道文件
int fd;
fd = open(FIFO, O_RDWR);

write(fd, "hello", 6);

while (1);
}

示例代码:mkfifo_r.c

//读端代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

#define FIFO "/tmp/1.fifo"

void main()
{
int ret;
if (access(FIFO, F_OK))
{
//创建一个管道文件
ret = mkfifo(FIFO, 0644);
if (-1 == ret)
{
printf("error\n");
return ;
}
}
//打开管道文件
int fd;
fd = open(FIFO, O_RDWR);

char buf[10];
memset(buf, 0, sizeof(buf));
read(fd, buf, sizeof(buf));
printf("%s\n", buf);

while (1);
}

3.4 System V和POSIX

3.4.1 System V
1、System V的基本概念

System V, 曾经也被称为 AT&T System V,是Unix操作系统众多版本中的一支。它最初由 AT&T


开发,在1983年第一次发布。一共发行了4个 System V 的主要版本:版本1、2、3 和 4。System V
Release 4,或者称为SVR4,是最成功的版本,成为一些UNIX共同特性的源头,例如 ”SysV 初始化脚本
“ (/etc/init.d),用来控制系统启动和关闭,System V Interface Definition (SVID) 是一个System V 如何
工作的标准定义。

我们可以将System V理解为一种应用于系统的接口协议或者规范。

2、Linux与System V

Linux不是Unix,而是 Unix Like。目前很多的基于Linux的操作系统都实现了System V。例如:使用


System V的启动方式,实现了System V IPC等。

3.4.2 POSIX
POSIX也是一种应用于系统的接口协议或者规范。POSIX(Portable Operating System Interface
for Computing Systems)是由IEEE 和ISO/IEC 开发的一簇标准。该标准是基于现有的UNIX 实践和经
验,描述了操作系统的调用服务接口,用于保证编制的应用程序可以在源代码一级上在多种操作系统上
移植运行。

3.5 消息队列

3.5.1 消息队列概述
1、消息队列是内核中维护的消息链表,消息队列的特点如下:

消息队列中的消息是有类型的。
消息队列中的消息是有格式的。
消息队列可以实现消息的随机查询。消息不一定要以先进先出的次序读取,编程时可以按消息的类
型读取。
消息队列允许一个或多个进程向它写入或者读取消息。
与无名管道、命名管道一样,从消息队列中读出消息,消息队列中对应的数据都会被删除。
每个消息队列都有消息队列标识符,消息队列的标识符在整个系统中是唯一的。
只有内核重启或人工删除消息队列时,该消息队列才会被删除。若不人工删除消息队列,消息队列
会一直存在于系统中。
2、在 ubuntu 中消息队列限制值如下:

每个消息内容最多为 8K 字节
每个消息队列容量最多为 16K 字节
系统中消息队列个数最多为 1609 个
系统中消息个数最多为 16384 个

3.5.2 System V消息队列


System V 提供的 IPC 通信机制需要一个标识符,在程序中若要使用消息队列,必须要能知道消息
队列标识符,因为应用进程无法直接访问内核消息队列中的数据结构,因此需要一个消息队列的标识,
让应用进程知道当前操作的是哪个消息队列,同时也要保证每个消息队列标识符值的唯一性。

对于每个消息队列内核都会维护如下一个结构体:

// /usr/include/linux/msg.h
struct msqid_ds{
struct ipc_perm msg_perm; //权限
struct msg *msg_first; //指向消息头
struct msg *msg_last; //指向消息尾
__kernel_tiem_t msg_stime; //last msgsnd time 最近发送消息时间
__kernel_tiem_t msg_rtime; //lsat msgrcv time 最近接受消息时间
__kernel_tiem_t msg_ctime; //last change time
unsigned long msg_lcbytes; //Reuse junk fields for 32 bit
unsigned long msg_lqbytes; //ditto
unsigned short msg_qnum; //current number of bytes on queue 当前队列大

unsigned short msg_qbytes; //max number of bytes on queue 队列最大值
__kernel_ipc_pid_t msg_lspid; //最近msgsnd 的pid
__kernel_ipc_pid_t msg_lrpid; //最近receive 的pid
};

struct msqid_ds结构体与消息队列的关系:
System V消息队列编程流程:

ftok:获取IPC键值
msgget:创建或者获得消息队列
msgctl:操作消息队列
msgsnd:将消息发送到消息队列
msgrcv:从消息队列接收消息

1、ftok函数

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

key_t ftok(const char *pathname, int proj_id);

功能说明:获得IPC相关的唯一的 IPC 键值
参数列表:

pathname:为文件路径,或目录,一般是当前目录
proj_id:为一个整形变量,是子序号,参与构成ftok()函数的返回值。虽然是int类型,但是
只使用8bits(1-255)
返回值:

成功返回key值
失败返回-1
注意:通信双方 pathname proj_id 必须一致

2、创建消息队列:msgget()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget(key_t key, int msgflg);

功能说明:创建一个新的或打开一个已经存在的消息队列。不同的进程调用此函数,只要用相同的
key 值就能得到同一 个消息队列的标识符

形参列表:
key:IPC键值,ftok的返回值

msgflg:标识函数的行为及消息队列的权限

IPC_CREAT:创建消息队列
IPC_EXCL:检测消息队列是否存在、
返回值:

成功返回消息队列的标识符
失败返回-1,errno被设置

3、发送消息:msgsnd()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

功能说明:将新消息添加到消息队列

形参列表:

msgid:消息队列标识符,msgget的返回值

msgp:待发送消息结构体的地址,消息结构体一般为一下形式:

typedef struct _msg

long mtype; //消息类型


char mtext[100]; //消息正文

}MSG;
消息类型必须是长整型的,而且必须是结构体类型的第一个成员,类型下面是消
息正文,正文可以有多个成

员(正文成员可以是任意数据类型的)。

msgsz:消息正文的字节数

msgflg:函数的控制属性
0:msgsnd 调用阻塞直到条件满足为止
IPC_NOWAIT: 若消息没有立即发送则调用该函数的进程会立即返回
返回值:

成功返回0
失败返回-1,errno被设置

4、接收消息:msgrcv()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

功能说明:从标识符为 msqid 的消息队列中接收一个消息。一旦接收消息成功,则消息在消息队


列中被删除

形参列表:
msgid:消息队列标识符,msgget的返回值

msgp:存放消息结构体的地址
msgsz:消息正文的字节数

msgtyp:消息的类型、可以有以下几种类型 :

msgtyp = 0:返回队列中的第一个消息
msgtyp > 0:返回队列中消息类型为 msgtyp 的消息
msgtyp < 0:返回队列中消息类型值小于或等于 msgtyp 绝对值的消息,如果这种消息
有若干个,则取类型值最小的消息
msgflg:函数的控制属性

0:msgrcv 调用阻塞直到接收消息成功为止

MSG_NOERROR:若返回的消息字节数比 nbytes 字节数多,则消息就会截短到 nbytes 字节,且


不通知消息发 送进程

IPC_NOWAIT:调用进程会立即返回。若没有收到消息则立即返回-1
返回值:

成功返回读取消息的长度
失败返回-1,errno被设置
注意:
若消息队列中有多种类型的消息,msgrcv 获取消息的时候按消息类型获取,不是先进先出

在获取某类型消息的时候,若队列中有多条此类型的消息,则获取最先添加的消息,即先进
先出原则

5、消息队列的控制:msgctl()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

功能说明:对消息队列进行各种控制,如修改消息队列的属性,或删除消息消息队列

形参列表:

msgid:消息队列标识符,msgget的返回值

cmd:函数功能的控制

IPC_RMID:删除由 msqid 指示的消息队列,将它从系统中删除并破坏相关数据结构。


IPC_STAT:将 msqid 相关的数据结构中各个元素的当前值存入到由 buf 指向的结构
中。
IPC_SET:将 msqid 相关的数据结构中的元素设置为由 buf 指向的结构中的对应值
buf:msqid_ds 数据类型的地址,用来存放或更改消息队列的属性

返回值:
成功返回0
失败返回-1, errno被设置

6、示例代码

接收进程代码:msg_rcv.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

#define PATH "/tmp"


#define PROJ_ID 250

struct msgbuf {
long mtype;
char mtext[100];
};

int main()
{
key_t key;
key = ftok(PATH, PROJ_ID);
if (-1 == key)
{
perror("ftok error");
return 0;
}

int id;
id = msgget(key, 0);
if (-1 == id)
{
perror("msgget error");
return 0;
}

struct msgbuf msg;


msg.mtype = 100;
memset(msg.mtext, 0, sizeof(msg.mtext));
//接收消息
while (1)
{
int ret;
ret = msgrcv(id, &msg, sizeof(msg.mtext), msg.mtype, 0);
if (ret == -1)
{
perror("msgsnd error");
return 0;
}
printf("%s\n", msg.mtext);
}

return 0;
}

发送进程代码:msg_snd.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define PATH "/tmp"
#define PROJ_ID 250

struct msgbuf {
long mtype;
char mtext[100];
};

int main()
{
key_t key;
key = ftok(PATH, PROJ_ID);
if (-1 == key)
{
perror("ftok error");
return 0;
}

int id;
id = msgget(key, IPC_CREAT | 0644);
if (-1 == id)
{
perror("msgget error");
return 0;
}

struct msgbuf msg;


msg.mtype = 100;
memset(msg.mtext, 0, sizeof(msg.mtext));
strcpy(msg.mtext, "hello world");
//发送消息
int ret;
int i;
for (i = 0; i < 10; i++)
{
ret = msgsnd(id, &msg, sizeof(msg.mtext), 0);
if (ret == -1)
{
perror("msgsnd error");
return 0;
}
}

return 0;
}

3.5.3 POSIX消息队列
1、创建消息队列:mq_open()
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <mqueue.h>

mqd_t mq_open(const char *name, int oflag);


mqd_t mq_open(const char *name, int oflag, mode_t mode,
struct mq_attr *attr);

Link with -lrt.

功能说明:创建一个新的POSIX消息队列或者打开一个已经存在的消息队列

形参列表:

name:消息队列的名称(必须以/开头,且后面不能再有/)

oflag: 标志
O_RDONLY 只读
O_RDWR 读写
O_WRONLY 只写
O_CREAT 没有该对象则创建
O_EXCL 如果O_CREAT指定,但name不存在,就返回错误
O_NONBLOCK 以非阻塞方式打开消息队列
mode:权限

S_IWUSR 用户/属主写
S_IRUSR 用户/属主读
S_IWGRP 组成员写
S_IRGRP 组成员读
S_IWOTH 其他用户写
S_IROTH 其他用户读
attr:给新队列指定某些属性,如果为空就使用默认属性

struct mq_attr{
long mq_flags;//标志,其值为0表示阻塞,O_NONBlOCK表示非阻塞。可用
mq_getatter和mq_setattr获取 和 设置
long mq_maxmsg;//消息队列的消息个数的最大值,只能在创建消息队列的时候设

long mq_msgsize;//每个消息大小的最大限制数
long mq_curmsgs://当前队列的消息个数,可用mq_getatter获取
}

返回值:

成功返回消息队列的描述符
失败返回-1,errno被设置

2、关闭消息队列:mq_close()

#include <mqueue.h>

int mq_close(mqd_t mqdes);

Link with -lrt.

功能说明:关闭一个消息队列,调用之后表示进程不在使用该描述符并不代表消息队列会从系统中
删除
形参列表:

mqdes:消息队列描述符,mq_open的返回值
返回值:

成功返回0
失败返回 -1,errno被设置

3、获取消息队列属性:mq_getattr()

#include <mqueue.h>

int mq_getattr(mqd_t mqdes, struct mq_attr *attr);

Link with -lrt.

功能说明:获取当前消息队列的属性

形参列表:
mqdes:消息队列描述符,mq_open的返回值
attr:存储消息队列属性的结构体指针
返回值:

成功返回0
失败返回 -1,errno被设置

4、设置消息队列属性:mq_setattr()

#include <mqueue.h>

int mq_setattr(mqd_t mqdes, const struct mq_attr *newattr, struct mq_attr


*oldattr);

功能说明:设置消息队列的属性,可以设置的属性只有mq_flags,用来设置或清除消息队列的非
阻塞标志,其他属性被忽略。mq_maxmsg和mq_msgsize属性只能在创建消息队列时通过
mq_open来设置。

形参列表:

mqdes:消息队列描述符,mq_open的返回值
newattr:指向新设置属性的结构体
oldattr:指向返回旧的属性的结构体
返回值:

成功返回0
失败返回 -1,errno被设置

5、发送消息:mq_send()

#include <mqueue.h>

int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int
msg_prio);

#include <time.h>
#include <mqueue.h>

int mq_timedsend(mqd_t mqdes, const char *msg_ptr,


size_t msg_len, unsigned int msg_prio,
const struct timespec *abs_timeout);

struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};

Link with -lrt.

功能说明: 往消息队列中放置一个消息,如果O_NONBLOCK被指定,满队时mq_send()将不会阻
塞,而是返回EAGAIN错误,如果没有设置O_NONBLOCK,满队时会产生阻塞

形参列表:

mqdes:消息队列描述符,mq_open的返回值
msg_ptr:要发送消息的指针
msg_len:消息长度(不能大于属性值mq_msgsize的值)
msg_prio:优先级(消息在队列中将按照优先级从大到小的顺序排列消息;数字越大优先级
越高)
返回值:

成功返回0
失败返回 -1, errno被设置

6、接收消息:mq_receive()

#include <mqueue.h>

ssize_t mq_receive(mqd_t mqdes, char *msg_ptr,size_t msg_len, unsigned int


*msg_prio);

#include <time.h>
#include <mqueue.h>

ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr,size_t msg_len, unsigned int


*msg_prio,const struct timespec *abs_timeout);

Link with -lrt.

功能说明:按优先级从高到低进行接收,即优先级值大的先接收。如果队列为空,
mq_receive()函数将阻塞,直到消息队列中有新的消息,如果O_NONBLOCK被指定,
mq_receive()将不会阻塞,而是返回EAGAIN错误
形参列表:

mqdes:消息队列描述符,mq_open的返回值
msg_ptr:指向接收消息缓冲区
msg_len: ptr指向缓冲区的大小(必须大于等于属性值mq_msgsize的值,否则返回
EMSGSIZE错误)
msg_prio:如果prio不为NULL,那么接收到的消息优先级会被复制到prio指向的位置
返回值:
成功返回接收到的消息的长度
失败返回-1,errno被设置

7、示例代码:posix_msg_queque.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <mqueue.h>
#include <errno.h>
#include <unistd.h>

int main()
{
mqd_t mq_id;

mq_id = mq_open("/myqueue", O_RDWR | O_CREAT | O_EXCL, 0666, NULL);


if (mq_id < 0) //如果创建消息队列失败
{
if (errno == EEXIST) //如果出错原因是队列存在
{
mq_unlink("/myqueue"); //删除原来的队列
mq_id = mq_open("/myqueue", O_RDWR | O_CREAT, 0666, NULL);
if (mq_id < 0)
{
printf("open message queue error...\n");
return 0;
}
}
else
{
printf("open message queue error...\n");
return 0;
}
}
struct mq_attr mqAttr;
if (mq_getattr(mq_id, &mqAttr) < 0)
{
printf("get the message queue attribute error");
return 0;
}
printf("mqAttr.mq_flags:%ld\n", mqAttr.mq_flags);
printf("mqAttr.mq_maxmsg:%ld\n", mqAttr.mq_maxmsg);
printf("mqAttr.mq_msgsize:%ld\n", mqAttr.mq_msgsize);
printf("mqAttr.mq_curmsgs:%ld\n", mqAttr.mq_curmsgs);

pid_t pid;
pid = fork();
if (pid == 0)
{
int i = 0;
char buf[100];

while (1)
{
memset(buf, 0, sizeof(buf));
sprintf(buf, "The %dst msg ...\n", i+1);
i++;
int ret = mq_send(mq_id, buf, sizeof(buf), i);
printf("ret: %d\n", ret);
sleep(1);
}
}
else if (pid > 0)
{
char buf[mqAttr.mq_msgsize]; //注意:必须大于等于属性值mq_msgsize的值,否则返
回EMSGSIZE错误
int ret;
while (1)
{
//阻塞读取消息
ret = mq_receive(mq_id, buf, sizeof(buf), NULL);
printf("%d: %s\n", ret, buf);
}
}

return 0;
}

3.5.4 消息队列和管道的区别
IPC的持续性不同。管道和FIFO是随进程的持续性,当管道和FIFO最后一次关闭发生时,仍在管道和
FIFO中的数据会被丢弃。消息队列是随内核的持续性,即一个进程向消息队列写入消息后,然后终止,
另外一个进程可以在以后某个时刻打开该队列读取消息。只要内核没有重新自举,消息队列没有被删除

3.5.5 System V消息队列编程流程

3.6 共享内存
3.6.1 共享内存概述
共享内存允许两个或更多进程访问同一块物理内存,是进程间通信中最简单的方式之一。进程将物理
内存上的一块空间进行共享,然后每个进程都将这块共享内存映射到自己的虚拟地址空间中。当一个进
程在自己的虚拟地址空间中改变了映射的共享内存时,其它进程都会察觉到这个更改。

共享内存的特点:

1. 所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域
和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核(避免了用户态
和内核态上下文切换带来的开销)的过程来完成。同时它也避免了对数据的各种不必要的复制。
(管道和消息队列数据在内核中被拷贝了两次)
2. 使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥。在数据被写入之前不允许进
程从共享内存中读取信息、不允许两个进程同时向同一个共享内存地址写入数据等。解决这些问题
的常用方法是通过使用信号量进行同步

3.6.2 System V共享内存


1、共享内存结构体

/* Obsolete, used only for backwards compatibility and libc5 compiles */


struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};

2、与System V 消息队列一样,需要利用ftok函数生成key标识符
3、创建共享内存段:shmget()

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

功能说明:使用shmget函数,创建一个共享内存块

形参列表:

key:IPC键值,ftok的返回值

size:需要申请的共享内存的大小,需要注意的是,操作系统为你提供的大小的时候是按页
来提供,所以size为4k的整数倍

shmflg:

IPC_CREAT,如果共享内存不存在就创建一个新的信号量,如果存在则返回该共享内存
的标识
IPC_EXCL,如果存在则以错误返回errno为EEXIST
IPC_CREAT | IPC_EXCL,如果共享内存不存在就创建一个新的共享内存,如果存在则以
错误返回errno为EEXIST
返回值:
成功返回共享内存IPC的标识符
失败返回-1,errno被设置

4、共享内存映射:shmat()

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

void *shmat(int shmid, const void *shmaddr, int shmflg);

功能说明:将共享的物理内存映射到进程的虚拟地址空间

形参列表:
shmid:共享内存IPC的标识符,shmget函数的返回值
shmaddr:指定共享内存连接到当前进程中的地址位置,通常为空指针,表示让系统来选择
共享内存的地址
shmflg:共享标志,通常为0
返回值:

成功返回映射到虚拟地址空间的首地址
失败返回 (void*)-1, errno被设置

5、解除映射:shmdt()

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

int shmdt(const void *shmaddr);

功能说明:将原来映射到进程虚拟地址的那段空间解除映射

形参列表:
shmaddr:上次映射的虚拟地址空间的首地址
返回值:

成功返回0
失败返回-1,errno被设置

6、共享内存的控制:shmctl()

#include <sys/ipc.h>
#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

功能说明:对共享内存进行控制
形参列表:

shmid:共享内存IPC的标识符,shmget函数的返回值

cmd:需要对共享内存采取的操作

IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的
当前关联值覆盖shmid_ds的值
IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中
给出的值
IPC_RMID:删除共享内存段
buf:是一个结构指针,它指向共享内存模式和访问权限的结构,shmid_ds结构至少包括以
下成员

struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};

7、示例代码:shm_systemV.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define PATH "/tmp"


#define ID 100

#define page_size 4096

void main()
{
key_t key;
//key:代表一个system V IPC
key = ftok(PATH, ID);
if (-1 == key)
{
perror("ftok error");
return ;
}

int id;
//创建了个system v IPC中的共享内存,而且共享了page_size大小的内存
id = shmget(key, page_size, IPC_CREAT | 0644);
if (-1 == id)
{
perror("shmget error");
return ;
}

//将共享的内存映射到进程的地址空间中
int *p = (int *)shmat(id, NULL, 0);
if (p == (void *)-1)
{
perror("shmat error");
return ;
}

*p = 0;
pid_t pid;
pid = fork();

if (pid > 0)
{
int i;
for (i = 0; i < 100000000; i++)
(*p)++;
printf("%d\n", *p);
}
else if (pid == 0)
{
int i;
for (i = 0; i < 100000000; i++)
(*p)++;
printf("%d\n", *p);
}

while (1);
}

8、思考:为什么最后*p的值不是20000000?

因为(*p)++不是个原子操作(原子操作是指不会被调度机制打断的操作)。我们编写一段简单的代
码来查看变量自增在编译时生成的汇编指令。代码如下:1.c
#include <stdio.h>

int main()
{
int a = 1;
a++;
return 0;
}

我们将1.c 编译生成汇编文件 1.s: arm-linux-gcc -S 1.c,编译完后,打开1.s汇编文件:

我们发现a++这条语句被编译生成了三条汇编指令,只有当最后一条str指令执行完毕,将w0寄存器
中的值写入到栈中才真正完成自增操作。假如多个进程对共享内存上的一个变量,其中某个进程对变量
进行自增操作在执行str指令前,该进程被从CPU上调度走,调度程序调度其他进程也对该变量进行自增
操作,那么其他进程拿到该变量的值依然是自增之前的值,这样就会导致虽然n个进程对变量进行了自
增操作,但该变量最终的结果却没有加n,导致该变量的值没有得到同步。

9、System V共享内存编程流程

3.6.3 POSIX 共享内存


1、创建共享内存段:shm_open()

#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */

int shm_open(const char *name, int oflag, mode_t mode);

int shm_unlink(const char *name); //删除一个共享内存区对象的名字

Link with -lrt.

功能说明:创建一个共享内存段

形参列表:
name:指定一个共享内存的名字,格式如下: /something
oflag:与open函数的flags参数一样
mode:与open函数的mode参数一样,指定权限位
返回值:

成功返回文件描述符
失败返回-1, errno被设置

2、内存映射:mmap()

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t
offset);
int munmap(void *addr, size_t length);

功能说明:将一个指定的文件映射到进程的虚拟地址空间中

形参列表:

addr:映射的虚拟内存空间的首地址,一般设置为NULL,由系统自动指定
length:映射的空间大小,一般为一页的整数倍

prot:映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取
flags:影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或
MAP_PRIVATE

MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进
程共享
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复
制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容
fd:需要映射的文件的文件描述符
offset:文件的偏移位置,如果需要将整个文件映射在offset置为0

返回值:
返回映射区的内存起始地址
失败返回MAP_FAILED , (void *)-1,errno被设置

3、内容同步:msync()

#include <sys/mman.h>

int msync(void *addr, size_t length, int flags);

功能说明:对映射内存的内容的更改并不会立即更新到文件中,而是有一段时间的延迟,你可以调
用msync()来显式同步一下, 这样你内存的更新就能立即保存到文件里

形参列表:

addr:要进行同步的映射的内存区域的起始地址

length:要同步的内存区域的大小

flags:可以为以下三个值之一

MS_ASYNC : 请Kernel马上将资料写入
MS_SYNC : 在msync结束返回前,将资料写入
MS_INVALIDATE : 内核自行决定是否写入,仅在特殊状况下使用

4、示例代码:shm_posix.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/wait.h>

void error_out(const char *msg)


{
perror(msg);
exit(EXIT_FAILURE);
}
int main (int argc, char *argv[])
{
int r;
const char *memname = "/mymem";
int fd = shm_open(memname, O_CREAT|O_TRUNC|O_RDWR, 0666);
void *ptr = mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
error_out("MMAP");

pid_t pid = fork();


if (pid == 0)
{
u_long *d = (u_long *)ptr;
*d = 0xdeadbeef;
exit(0);
}
else
{
int status;
waitpid(pid, &status, 0);
printf("child wrote %#lx\n", *(u_long *)ptr);
}
sleep(5);
r = munmap(ptr, region_size);
if (r != 0)
error_out("munmap");
r = shm_unlink(memname);
if (r != 0)
error_out("shm_unlink");
return 0;
}

5、POSIX信号量编程流程
3.7 进程的同步和互斥

3.7.1 进程的同步
在多道程序系统中,多个进程是可以并发执行的,但由于系统的资源有限,进程的执行不是一贯到
底的, 而是走走停停,以不可预知的速度向前推进,这就是进程的异步性。

那么,进程的异步性会带来什么问题呢?举个例子,如果有 A、B 两个进程/线程分别负责读和写数


据的操作,这两个进程是相互合作、相互依赖的。那么写数据应该发生在读数据之前。而实际上,由于
异步性的存在,可能会发生先读后写的情况,而此时由于缓冲区还没有被写入数据,读进程 A 没有数据
可读,因此读进程 A 被阻塞。

进程同步(synchronization)就是用来解决这个问题的。从上面的例子我们能看出,一个进程的执行
可能影响到另一个进程的执行,所谓进程同步就是指协调这些完成某个共同任务的并发进程/线程,在
某些位置上指定线程的先后执行次序、传递信号或消息。

3.7.2 进程的互斥
1、临界资源与临界区
在操作系统中,多个线程或者多个进程需要同时访问某些资源/数据,有些资源在同一时间只能被
一个线程或者进程所占用,这些一次只能被一个线程或者进程所占用的资源就是所谓的临界资源。

对于临界资源的访问,必须是互斥进行。也就是当临界资源被占用时,另一个申请临界资源的进程
会被阻塞,直到其所申请的临界资源被释放。而线程或者进程内访问临界资源的代码被成为临界区。

2、进程的互斥

在操作系统中,当某个线程或者进程在访问临界资源时,不允许其他的线程或者进程访问该临界资
源,进程之间的这种相互制约的关系成为进程互斥。

通过上图两个线程或者进程对临界资源的访问,我们发现,进程互斥其实是一种特殊的进程同步,
即逐次使用临界资源,也是对进程使用资源的先后执行次序的一种协调。

举个例子:打印机在打印某个文档的过程中,如果有新增了第二个打印任务,那么第二个打印任务
必须等待第一个打印任务结束才能开始。

Linux中常见的进程同步与互斥机制为:信号量与PV操作。

3.8 Linux 信号量

3.8.1 信号量概述
信号量(Semaphore)的概念最早由荷兰计算机科学家 Dijkstra(迪杰斯特拉)提出,有时又称
“信号灯”,是用来解决进程同步与互斥问题的一种机制。

信号量本质上是一个整数计数器,它被用来控制对公共资源的访问,程序开始需要给它一个初值。
信号量的类型:

1、二进制信号量:信号量的值只能是0和1,一般用来表示临界资源能否访问

2、一般/计数信号量:信号量的值与相应资源的使用情况有关,当它的值大于0时,表示当前可用
资源的数量,当它的值等于0时,表示当前资源不能访问, 值为负数时,其绝对值表示等待资源的进
程或者线程个数。

信号量是一个被保护的变量,一旦被初始化,改变信号量的值的唯一办法就是通过PV操作。

3.8.2 PV操作
1、PV操作概述

PV操作是一种实现进程互斥与同步的有效方法。PV操作由P操作原语和V操作原语组成(原语是不可
中断的过程):

P操作:荷兰语Proberen,理解为申请资源,P操作将信号量S的值减1,即S=S-1,如果 S>=0,
则该进程继续执行,否则该进程置为阻塞状态,排入阻塞队列
V操作:荷兰语Verhogen,理解为释放资源,V操作将信号量S的值加1,如果信号量的值为负
数,说明有其他进程阻塞在P操作(等待获取共享资源),唤醒其中的一个进程获取资源
P操作可以阻塞
V操作不会阻塞

2、System V 信号量共享资源获取流程

Systen V信号量在实现时,信号量的最小值为0。

多个进程或者线程为了获得共享资源,一般需要执行下列操作:

(1)测试控制该资源的信号量

(2)如果信号量的值为正,则进程可以使用该资源,进程将信号量的值减-1,表示它使用了一个
资源单位,并且接入临界区(执行操作共享资源的代码)

(3)如果信号量的值为0,则进入阻塞状态,直到信号量的值大于1,当它被其他进程唤醒后,返
回值步骤(1)

为了正确的实现信号量,信号量值的测试及减1操作应当是原子操作,因此信号量通常是在内核中
实现得。

声明:后面的学习中我们将“信号量值的测试及减1操作”当作一个P操作,将“信号量值的测试及加1
操作”当作一个V操作。

在信号量的实际应用中,基本思想是:一个共享资源与一个信号量联系起来,然后用P操作和V操作
将临界区包起来:
3、PV操作练习

1)PV操作解决同步问题:公家车司机VS售票员

同步条件:

售票员关门后,司机才能起步
司机停车后,售票员才能开门
2)PV操作解决异步问题:单生产者和单消费者

问题描述:

有两个进程:一组生产者进程和一组消费者进程共享一个初始为空、固定大小为n的缓存(缓冲
区)。生产者的工作是制造一段数据,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则
必须等待,如此反复; 同时,只有缓冲区不空时,消费者才能从中取出消息,一次消费一段数据
(即将其从缓存中移出),否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消
息,或者一个消费者从中取出消息。

问题的核心:

要保证不让生产者在缓存还是满的时候仍然要向内写数据
不让消费者试图从空的缓存中取出数据

生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生
产者生产之后,消费者才能消费,他们也是同步关系。

解决思路:

对于生产者,如果缓存是满的就阻塞,消费者从缓存中取走数据后就唤醒生产者,让它再次将缓存
填满
若消费者发现缓存是空的,就阻塞。下一轮中生产者将数据写入后就唤醒消费者
不完善的解决方案会造成“死锁”,即两个进程都在阻塞等着对方来“唤醒”

实现方法:

我们使用了两个信号量:full 和 empty 。

信号量 full 用于记录当前缓冲池中“满”缓冲区数,初值为0


信号量 empty 用于记录当前缓冲池中“空”缓冲区数,初值为n
新的数据添加到缓存中后,full 在增加,而 empty 则减少。如果生产者试图在 empty 为0时
减少其值,生产者就会被阻塞。下一轮中有数据被消费掉时,empty就会增加,生产者就会
被“唤醒”
信号量mutex作为互斥信号量,它用于控制互斥访问缓冲池,互斥信号量初值为 1
思考:为什么对缓冲区的操作还需要使用一个信号量mutex?

伪代码:

semaphore mutex=1; //临界区互斥信号量


semaphore empty=n; //空闲缓冲区
semaphore full=0; //缓冲区初始化为空

producer ()//生产者进程
{
while(1)
{
produce an item in nextp; //生产数据
P(empty); //获取空缓冲区单元
P(mutex); //进入临界区.
add nextp to buffer; //将数据放入缓冲区
V(mutex); //离开临界区,释放互斥信号量
V(full); //满缓冲区数加1
}
}

consumer ()//消费者进程
{
while(1)
{
P(full); //获取满缓冲区单元
P(mutex); // 进入临界区
remove an item from buffer; //从缓冲区中取出数据
V (mutex); //离开临界区,释放互斥信号量
V (empty) ; //空缓冲区数加1
consume the item; //消费数据
}
}

3.8.4 哲学家就餐问题
1、问题描述

哲学家就餐问题是由迪杰斯特拉提出并解决的,是一个经典的同步问题:假设有 5 个哲学家,他们
的生活只是思考和吃饭。这些哲学家共用一个圆桌,每位都有一把椅子。在桌子中央有一碗米饭,在桌
子上放着 5 根筷子。
当一位哲学家思考时,他与其他同事不交流。时而,他会感到饥饿,并试图拿起与他相近的两根筷
子(筷子在他和他的左或右邻居之间)。一个哲学家一次只能拿起一根筷子。显然,他不能从其他哲学
家手里拿走筷子。当一个饥饿的哲学家同时拥有两根筷子时,他就能吃。在吃完后,他会放下两根筷
子,并开始思考。

2、问题分析

一共有五个哲学家,也就是五个进程,五只筷子,也就是五个临界资源;因为哲学家想要进餐,必
须要同时获得左边和右边的筷子,这就是要同时进入两个临界区(使用临界资源),才可以进餐。因为
是五只筷子为临界资源,因此设置五个信号量即可。

3、错误的解决方法

semaphore mutex[5] = {1,1,1,1,1}; //初始化信号量

void philosopher(int i){


do {
//thinking //思考
P(mutex[i]);//判断哲学家左边的筷子是否可用
P(mutex[(i+1)%5]);//判断哲学家右边的筷子是否可用
//...
//eat //进餐
//...
V(mutex[i]);//退出临界区,允许别的进程操作缓冲池
V(mutex[(i+1)%5]);//缓冲池中非空的缓冲区数量加1,可以唤醒等待的进程
}while(true);
}
我们来分析下上面的代码,首先我们从一个哲学家的角度来看问题,程序似乎是没有问题的,申请
到左右两支筷子后,然后开始进餐。但是如果考虑到并发问题,五个哲学家同时拿起了左边的筷子,此
时,五只筷子立刻都被占用了,没有可用的筷子了,当所有的哲学家再想拿起右边筷子的时候,因为临
界资源不足,只能将自身阻塞,而所有的哲学家全部都会阻塞,并且不会释放自己手中拿着的左边的筷
子,因此就会一直处于阻塞状态,无法进行进餐并思考。

因为,为了解决五个哲学家争用的资源的问题,我们可以采用以下几种解决方法:

1. 至多只允许有四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并在用餐
完毕后能释放他占用的筷子,从而使别的哲学家能够进餐

semaphore mutex[5] = {1,1,1,1,1}; //初始化信号量


semaphore count = 4; //控制最多允许四位哲学家同时进餐

void philosopher(int i){


do {
//thinking //思考
p(count); //判断是否超过四人准备进餐
P(mutex[i]); //判断缓冲池中是否仍有空闲的缓冲区
P(mutex[(i+1)%5]);//判断是否可以进入临界区(操作缓冲池)
//...
//eat //进餐
//...
V(mutex[i]);//退出临界区,允许别的进程操作缓冲池
V(mutex[(i+1)%5]);//缓冲池中非空的缓冲区数量加1,可以唤醒等待的进程
V(count);//用餐完毕,别的哲学家可以开始进餐
}while(true);
}

2. 仅当哲学家的左、右两支筷子可用时,才允许他拿起筷子:使用AND型信号量,同时对哲学家左
右两边的筷子同时申请

semaphore mutex[5] = {1,1,1,1,1}; //初始化信号量

void philosopher(int i){


do {
//thinking //思考
Swait(mutex[i], mutex[(i+1)%5]);//P操作,判断哲学家左边和右边的筷子是否同时可用
//...
//eat
//...
Ssignal(mutex[i], mutex[(i+1)%5]);//V操作,进餐完毕,释放哲学家占有的筷子
}while(true);
}

3.规定奇数号哲学家先拿他左边的筷子,然后再去拿右边的筷子;而偶数号哲学家则相反

semaphore mutex[5] = {1,1,1,1,1}; //初始化信号量

void philosopher(int i){ //i 代表哲学家的编号


do {
//thinking
if(i%2 == 1){
P(mutex[i]);//判断哲学家左边的筷子是否可用
P(mutex[(i+1)%5]);//判断哲学家右边的筷子是否可用
}else{
P(mutex[(i+1)%5]);//判断哲学家右边的筷子是否可用
P(mutex[i]);//判断哲学家左边的筷子是否可用
}
//...
//eat
//...
V(mutex[i]);//退出临界区,允许别的进程操作缓冲池
V(mutex[(i+1)%5]);//缓冲池中非空的缓冲区数量加1,可以唤醒等待的进程
}while(true);
}

3.8.5 System V信号量


1、System V是一个计数信号量集,其实就是把信号量放入数组中

2、在Linux的/usr/include/linux/sem.h头文件中描述了信号量的相关信息:

struct semid_ds {
struct ipc_perm sem_perm; /* permissions .. see ipc.h */
__kernel_time_t sem_otime; /* last semop time */
__kernel_time_t sem_ctime; /* create/last semctl() time */
struct sem *sem_base; /* ptr to first semaphore in array */
struct sem_queue *sem_pending; /* pending operations to be processed */
struct sem_queue **sem_pending_last; /* last pending operation */
struct sem_undo *undo; /* undo requests on this array */
unsigned short sem_nsems; /* no. of semaphores in array */
};

struct sem 表示封装的信号量结构:

struct sem
{
unsigned short semval; /* semaphore value */
unsigned short semzcnt; /* # waiting for zero */
unsigned short semncnt; /* # waiting for increase */
pid_t sempid; /* ID of process that did last op */
};
3、与System V 消息队列一样,需要利用ftok函数生成key标识符,System V信号量编程流程

4、创建和打开System V信号量:semget()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

功能说明:创建或者打开一个System V的信号量,并且获取信号量标识符

形参列表:

key:ftok的返回值。如果要创建的信号量集合中需要有多个信号量,该参数必须设置为
IPC_PRIVATE

nsems:表示信号量集中信号量数(可以理解为数组的大小)
semflg:

IPC_CREAT,如果信号量不存在就创建一个新的信号量,如果存在则返回该信号量的标

IPC_EXCL,如果存在则以错误返回errno为EEXIST
IPC_CREAT | IPC_EXCL,如果信号量不存在就创建一个新的信号量,如果存在则以错误
返回errno为EEXIST
如果只是打开一个已经创建好的信号量可以设置为0
返回值:
成功返回System V 信号量IPC的标识符
失败返回-1,errno被设置
注意:semget()创建一个新的信号量集并没有对其进行初始化,需要调用后面要讲的semctl()函数
进行初始化

5、信号量的控制操作:semctl()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl(int semid, int semnum, int cmd, ...);

功能说明:对信号量集的一系列控制操作,根据操作命令cmd的不同,执行不同的操作

形参列表:

semid:System V信号量的标识符,semget函数的返回值

semnum:表示信号量集中的第semnum个信号量。它的取值范围:0~ nsems-1

cmd:操作命令,semctl可以有三个参数或者四个参数,如果使用四个参数最后一个参数的
类型为 union semun,并且需要用户自定义该类型(对信号量的值进行设置时需要使用第四
个参数):

union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT,
IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL
*/
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};

cmd命令有以下10种
返回值:
如果失败返回-1, errno被设置是

如果cmd设置为如下命令,返回值是一个非负值

GETNCNT the value of semncnt

GETPID the value of sempid.

GETVAL the value of semval.

GETZCNT the value of semzcnt.

IPC_INFO the index of the highest used entry in the kernel's


internal array recording information about all
semaphore sets. (This information can be used with repeated
SEM_STAT or SEM_STAT_ANY operations to
obtain information about all semaphore sets on the system.)

SEM_INFO as for IPC_INFO.

SEM_STAT the identifier of the semaphore set whose index was given
in semid.

SEM_STAT_ANY
as for SEM_STAT.

如果cmd设置为其他命令,成功返回0

6、信号量的操作:semop()

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semop(int semid, struct sembuf *sops, size_t nsops);

int semtimedop(int semid, struct sembuf *sops, size_t nsops, const struct
timespec *timeout);
功能说明:对信号量的值进行操作

形参列表:

semid:System V信号量的标识符,semget函数的返回值

sops:信号量操作结构体数组,struct sembuf结构如下:

struct sembuf
{
short sem_num;//信号量编号,从0开始
short sem_op;//信号量操作类型,自减为P操作,自增为V操作,如果为负数那么从
信号量的值semval中减去这个数的绝对值,正值将值添加到semval上,对应与释放某个资
源,希望等待到semval值变为0,如果已经是0,则立即返回,否则semzcnt+1,并线程阻

short sem_flg;//信号量行为,可以设置为IPC_WAIT,SEMUNDO
//IPC_WAIT,信号量的操作非阻塞的,调用进程在信号量的值不满足条件的情
况下不会被阻塞,直接返回-1,并将errno设置为EAGAIN
//SEMUNDO, 程序结束时,不论是不是正常返回,都会把信号量的值设定成调用
semop()前的值,保证资源的释放(否则会造成资源的永远锁定)。
};

注意:信号量相关的值在前面已经讲过的struct sem结构体中:

struct sem
{
unsigned short semval; /*信号量的当前值 */
unsigned short semzcnt; /* # 等待semval变为0的线程数 */
unsigned short semncnt; /* # 等待semval变为大于当前值的线程数 */
pid_t sempid; /* ID of process that did last op */
};

nsops:一次操作信号量的个数

返回值:

成功返回0
失败返回-1,errno被设置

7、示例代码:sem_systemV.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#define PATH "/tmp"


#define ID 100
#define PATH1 "/tmp"
#define ID1 101

#define page_size 4096

union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};

//初始化一个System V的共享内存
int *shm_init()
{
key_t key;
//key:代表一个system V IPC
key = ftok(PATH, ID);
if (-1 == key)
{
perror("ftok error");
return NULL;
}

int id;
//创建了个system v IPC中的共享内存,而且共享了page_size大小的内存
id = shmget(key, page_size, IPC_CREAT | 0644);
if (-1 == id)
{
perror("shmget error");
return NULL;
}

//将共享的内存映射到进程的地址空间中
int *p = (int *)shmat(id, NULL, 0);
if (p == (void *)-1)
{
perror("shmat error");
return NULL;
}
return p;
}

int main()
{
int *p = shm_init();
*p = 0;

//使用 system V信号量保障共享资源*p能够数据同步


key_t key1;
//key:代表一个system V IPC
key1 = ftok(PATH1, ID1);
if (-1 == key1)
{
perror("ftok error");
return 0;
}
int id1;
//通过key1创建一个system V IPC中的信号量
id1 = semget(key1,1, IPC_CREAT | 0644); //1: 信号量集合中信号量的个数
if (-1 == id1)
{
perror("semget error");
return 0;
}
//设置信号量的初始值
union semun semun;
semun.val = 1;
semctl(id1, 0, SETVAL, semun);

struct sembuf buf;

pid_t pid;
pid = fork();

if (pid > 0)
{
int i;
for (i = 0; i < 5000000; i++)
{

//P操作
buf.sem_num = 0;
buf.sem_op = -1;
buf.sem_flg = SEM_UNDO;
semop(id1, &buf, 1);

(*p)++; //临界区代码

//V操作
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = SEM_UNDO;
semop(id1, &buf, 1);
}
printf("%d\n", *p);
}
else if (pid == 0)
{
int i;
for (i = 0; i < 5000000; i++)
{
//P操作
buf.sem_num = 0;
buf.sem_op = -1;
buf.sem_flg = SEM_UNDO;
semop(id1, &buf, 1);

(*p)++; //临界区代码

//P操作
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = SEM_UNDO;
semop(id1, &buf, 1);
}
printf("%d\n", *p);
}

semctl(id1, 0, IPC_RMID, NULL);


while (1);
}

3.8.4 POSIX信号量
1、POSIX信号量与System V信号量的对比:

POSIX信号量不是信号量的集合而是单个信号量,System V 信号量其本质是信号量集
POSIX 信号量一次只能将信号量的值加 1 或减 1,而System V 信号量能够加上或减去一个大于 1
的值
POSIX 信号量并没有提供一个等待信号量变为 0 的接口, System V 信号量中, semop 函数则提供
了这样的接口
对于 System V 信号量而言,每次操作信号量,必然会从用户态陷入内核态,进程上下文的切换导
致系统的开销大,Posix信号量有基于内存实现的,即信号量值是放在共享内存中的,它是由可能
与文件系统中的路径名对应的名字来标识的。

2、POSIX信号量分为有名信号量(命名信号量)和无名信号量

有名信号量:可以通过名字访问,因此可用于知道他们名字的任意进程和线程使用

无名信号量:只存在于内存中,并要求使用信号量的进程可以访问内存,这意味着他们只能应用在
同一进程中的线程,或者不同进程的共享内存上

3、POSIX信号量的编程流程:需要包含头文件<semaphore.h>
4、信号量编程接口

sem_open:初始化或者打开一个有名信号量

#include <fcntl.h> /* For O_* constants */


#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>

sem_t *sem_open(const char *name, int oflag);


sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);

形参列表:
1、name:名字的首字符必须是斜杠(/),除首字符外,名字中不能再包含其他斜杠(/),名字的最长
长度由实现定义,不应超过_POSIX_NAME_MAX个字符
2、oflg:
如果使用一个现存的有名信号量,我们只需指定两个参数:信号量名和oflag。把oflag设置为
O_CREAT标志时,如果指定的信号量不存在则新建一个有名信号量;如果指定的信号量已经存在,那么打开使
用,无其他额外操作发生。
3、mode:如果我们指定O_CREAT标志,那么就需要提供另外两个参数:mode和value。mode用来指
定谁可以访问该信号量。它可以取打开文件时所用的权限位的取值
4、value:指定信号量的初始值

返回值:
成功返回一个信号量的指针,该指针可以用于其他对信号量操作的函数
失败返回SEM_FAILED,errno被设置

Link with -pthread.


注意:编译的时候要加 -lpthread 选项
sem_init:初始化一个无名信号量

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);

形参列表:
1、sem:无名信号量指针,通过sem_init函数进行赋值
2、pshared:如果是在多个线程中使用设置为0,如果是在多个进程中使用则为非零值,多进程间使用
无名信号量时该信号量必须在多个进程都能够访问到的共享内存中
3、value:信号量的初始值

返回值:成功返回0, 失败返回-1, errno被设置

Link with -pthread.


注意:编译的时候要加 -lpthread 选项

sem_wait:请求一个信号量(对信号量值执行减1操作, P操作)

#include <semaphore.h>

int sem_wait(sem_t *sem);


阻塞等待信号量的值变为大于0,如果信号量的值为0,则一直阻塞
返回值:成功返回0, 失败返回-1, errno被设置

int sem_trywait(sem_t *sem);


非阻塞等待,当信号量为0时,该函数返回-1并且将errno置为EAGAIN

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);


阻塞等待一段时间信号量的值变为大于0,如果超时到期并且信号量计数没能减1,sem_timedwait将返回-1
且将errno设置为ETIMEDOUT

Link with -pthread.


注意:编译的时候要加 -lpthread 选项

sem_post:信号量加1(V操作)

#include <semaphore.h>

int sem_post(sem_t *sem);

Link with -pthread.


返回值:成功返回0, 失败返回-1, errno被设置
注意:编译的时候要加 -lpthread 选项

sem_getvalue:获取信号量的值
#include <semaphore.h>

int sem_getvalue(sem_t *sem, int *sval);

形参列表:
1、sem:信号量指针
2、sval:保存信号量值得指针
返回值:成功返回0, 失败返回-1, errno被设置

Link with -pthread.


注意:编译的时候要加 -lpthread 选项

sem_close:关闭一个有名信号量

#include <semaphore.h>

int sem_close(sem_t *sem);

Link with -pthread


返回值:成功返回0, 失败返回-1, errno被设置
注意:编译的时候要加 -lpthread 选项

sem_unlink:销毁一个有名信号量

#include <semaphore.h>

int sem_unlink(const char *name);

Link with -pthread


返回值:成功返回0, 失败返回-1, errno被设置
注意:编译的时候要加 -lpthread 选项

sem_destroy:销毁一个无名信号量

#include <semaphore.h>

int sem_destroy(sem_t *sem);

Link with -pthread.

返回值:成功返回0, 失败返回-1, errno被设置


注意:编译的时候要加 -lpthread 选项

示例代码:sem_posix.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>

#define PATH "/tmp"


#define ID 100

#define page_size 4096

void main()
{
key_t key;
//key:代表一个system V IPC
key = ftok(PATH, ID);
if (-1 == key)
{
perror("ftok error");
return ;
}

int id;
//创建了个system v IPC中的共享内存,而且共享了page_size大小的内存
id = shmget(key, page_size, IPC_CREAT | 0644);
if (-1 == id)
{
perror("shmget error");
return ;
}

//将共享的内存映射到进程的地址空间中
int *p = (int *)shmat(id, NULL, 0);
if (p == (void *)-1)
{
perror("shmat error");
return ;
}

*p = 0;
pid_t pid;
pid = fork();

if (pid > 0)
{
//创建或者打开一个有名信号量
sem_t *sem;
sem = sem_open("/tmp", O_CREAT, 0644, 1);

int i;
for (i = 0; i < 10000000; i++)
{
sem_wait(sem);
(*p)++;
sem_post(sem);
}

printf("%d\n", *p);
sem_close(sem);
sem_unlink("/tmp");
}
else if (pid == 0)
{
//创建或者打开一个有名信号量
sem_t *sem;
sem = sem_open("/tmp", O_CREAT, 0644, 1);

int i;
for (i = 0; i < 10000000; i++)
{
sem_wait(sem);
(*p)++;
sem_post(sem);
}

printf("%d\n", *p);

sem_close(sem);
sem_unlink("/tmp");
}

while (1);
}

You might also like