【Linux内核源码分析】5个子系统

文摘   2024-11-03 17:24   陕西  

首先一张熟悉的图来说明GNU/linux的基本体系结构:

体系的上部分是用户(或应用程序)空间,这是用户应用程序执行的地方。

用户空间之外是内核空间,Linux 内核正是位于这里。

Linux 内核可以进一步划分成 3 层:

  • 最上面是系统调用接口,用户程序通过软件中断后,调用系统内核提供的功能,这个在用户空间和内核提供的服务之间的接口称为系统调用,它实现了一些基本的功能,例如 read 和 write;

  • 系统调用接口之下是内核代码,可以更精确地定义为独立于体系结构的内核代码,这些代码是 Linux 所支持的所有处理器体系结构所通用的;

  • 内核代码之下是依赖于体系结构的代码,构成了通常称为 BSP(Board Support Package)的部分,这些代码用作给定体系结构的处理器和特定于平台的代码。

Linux内核主要由进程调度(SCHED)、内存管理(MM)、虚拟文件系统(VFS)、网络接口(NET)和进程间通信(IPC)5个子系统组成,如图所示。

子系统之间的依赖关系:



  • 进程调度与内存管理之间的关系:这两个子系统互相依赖。在多道程序环境下,程序要运行必须为之创建进程,而创建进程的第一件事情,就是将程序和数据装入内存。

  • 进程间通信与内存管理的关系:进程间通信子系统要依赖内存管理支持共享内存通信机制,这种机制允许两个进程除了拥有自己的私有空间,还可以存取共同的内存区域。

  • 虚拟文件系统与网络接口之间的关系:虚拟文件系统利用网络接口支持网络文件系统(NFS),也利用内存管理支持RAMDISK设备。

  • 内存管理与虚拟文件系统之间的关系:内存管理利用虚拟文件系统支持交换,交换进程(swapd)定期由调度程序调度,这也是内存管理依赖于进程调度的惟一原因。当一个进程存取的内存映射被换出时,内存管理向文件系统发出请求,同时,挂起当前正在运行的进程。

除了这些依赖关系外,内核中的所有子系统还要依赖于一些共同的资源。这些资源包括所有子系统都用到的例程,如分配和释放内存空间的函数、打印警告或错误信息的函数及系统提供的调试例程等。



进程调度


进程调度控制系统中的多个进程对CPU的访问,使得多个进程能在CPU中“微观串行,宏观并行”地执行。进程调度处于系统的中心位置,内核中其他的子系统都依赖它,因为每个子系统都需要挂起或恢复进程。


如上图所示,Linux的进程在几个状态间进行切换:就绪/运行状态、等待状态(可以被中断打断)、等待状态(不可以被中断打断)、停止状态和僵死状态。

  • TASK_RUNNING:正在运行或处于就绪状态:就绪状态是指进程申请到了CPU以外的其他所有资源,正所谓:万事俱备,只欠东风.提醒:一般的操作系统教科书将正在CPU上执行的进程定义为RUNNING状态、而将可执行但是尚未被调度执行的进程定义为READY状态,这两种状态在Linux下统一为 TASK_RUNNING状态.

  • TASK_INTERRUPTIBLE:处于等待队伍中,等待资源有效时唤醒(比如等待键盘输入、socket连接、信号等等),但可以被中断唤醒.一般情况下,进程列表中的绝大多数进程都处于 TASK_INTERRUPTIBLE状态.毕竟皇帝只有一个(单个CPU时),后宫佳丽几千;如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来.

  • TASK_UNINTERRUPTIBLE:处于等待队伍中,等待资源有效时唤醒(比如等待键盘输入、socket连接、信号等等),但不可以被中断唤醒.

  • TASK_ZOMBIE:僵死状态,进程资源用户空间被释放,但内核中的进程PCB并没有释放,等待父进程回收.

  • TASK_STOPPED:进程被外部程序暂停(如收到SIGSTOP信号,进程会进入到TASK_STOPPED状态),当再次允许时继续执行(进程收到SIGCONT信号,进入TASK_RUNNING状态),因此处于这一状态的进程可以被唤醒.

在设备驱动编程中,当请求的资源不能得到满足时,驱动一般会调度其他进程执行,并使本进程进入睡眠状态,直到它请求的资源被释放,才会被唤醒而进入就绪态。睡眠分成可被打断的睡眠和不可被打断的睡眠,两者的区别在于可被打断的睡眠在收到信号的时候会醒。

在设备驱动编程中,当请求的资源不能得到满足时,驱动一般会调度其他进程执行,其对应进程进入睡眠状态,直到它请求的资源被释放,才会被唤醒而进入就绪态。设备驱动中,如果需要几个并发执行的任务,可以启动内核线程,启动内核线程的函数为:

pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);

当用户使用系统提供的库函数进行进程编程,用户可以动态地创建进程,进程之间还有等待,互斥等操作,这些操作都是由linux内核来实现的。linux内核通过进程管理子系统实现了进程有关的操作,在linux系统上,所有的计算工作都是通过进程表现的,进程可以是短期的(执行一个命令),也可以是长期的(一种网络服务)。linux系统是一种动态系统,通过进程管理能够适应不断变化的计算需求。

在用户空间,进程是由进程标示符(PID)表示的。从用户角度看,一个PID是一个数字值,可以唯一标识一个进程,一个PID值在进程的整个生命周期中不会更改,但是PID可以在进程销毁后被重新使用。创建进程可以使用几种方式,可以创建一个新的进程,也可以创建当前进程的子进程。

在linux内核空间,每个进程都有一个独立的数据结构,用来保存该进程的ID、优先级、地址的空间等信息,这个结构也被称做进程控制块(Process Control Block)。所谓的进程管理就是对进程控制块的管理。

Linux的进程是通过fork()函数系统调用产生的。调用fork()的进程叫做父进程,生成的进程叫做子进程。子进程被创建的时候,除了进程ID外,其它数据结构与父进程完全一致。在fork()系统调用创建内存之后,子进程马上被加入内核的进程调试队列,然后使用exec()系统调用,把程序的代码加入到子进程的地址空间,之后子进程就开始执行自己的代码。

在一个系统上可以有多个进程,但是一般情况下只有一个CPU,在同一个时刻只能有一个进程在工作,即使有多个CPU,也不可能和进程的数量一样多。如果让若干的进程都能在CPU上工作,这就是进程管理子系统的工作。linux内核设计了存放进程队列的结构,在一个系统上会有若干队列,分别存放不同状态的进程。一个进程可以有若干状态,具体是由操作系统来定义的,但是至少包含运行态、就绪态和等待3种状态,内核设计了对应的队列存放对应状态的进程控制块。

当一个用户进程被加载后,会进入就绪态,被加入到就绪态队列,CPU时间被轮转到就绪态队列后,切换到进程的代码,进程被执行,当进程的时间片到了以后被换出。如果进程发生I/O操作也会被提前被换出,并且存放到等待队列,当I/O请求返回后,进程又被放入就绪队列。linux系统对进程队列的管理设计了若干不同的方法,主要的目的是提高进程调试的稳定性。




内存管理


内存管理的主要作用是控制多个进程安全地共享主内存区域。当CPU提供内存管理单元(MMU)时,Linux内存管理完成为每个进程进行虚拟内存到物理内存的转换。Linux 2.6引入了对无MMU CPU的支持。

如下图所示,一般而言,Linux的每个进程享有4GB的内存空间,0~3GB属于用户空间,3~4GB属于内核空间,内核空间对常规内存、I/O设备内存以及高端内存存在不同的处理方式。



地址0G-3G:用户空间,运行应用程序 (每一个应用程序都认为自己独占这0~3G 的空间)

地址3G-4G:内核空间

内核空间包括:

1、3G – 3G+896MB 直接映射区

2、vmalloc区

3、永久映射区

4、固定映射区

使用虚拟内存技术的计算机,内存管理的硬件按照分页方式管理内存。分页方式是把计算机系统的物理内存按照相同大小等分,每个内存分片称作内存页,通常内存页大小是4KB。Linux内核的内存管理子系统管理虚拟内存与物理内存之间的映射关系,以及系统可用内存空间。

内存管理要管理的不仅是4KB缓冲区。Linux提供了对4KB缓冲区的抽象,例如slab分配器。这种内存管理模式使用4KB缓冲区为基数,然后从中分配结构,并跟踪内存页使用情况,比如哪些内存页是满的,哪些页面没有完全使用,哪些页面为空。这样就允许该模式根据系统需要来动态调整内存使用。

在支持多用户的系统上,由于内存占用的增大,容易出现物理内存被消耗尽的情况。为了解决物理内存被耗尽的问题,内存管理子系统规定页面可以移出内存并放入磁盘中,这个过程称为交换。内存管理的源代码可以在./linux/mm中找到。



虚拟文件系统


如下图4所示,Linux虚拟文件系统(VFS)隐藏各种了硬件的具体细节,为所有的设备提供了统一的接口。而且,它独立于各个具体的文件系统,是对各种文件系统的一个抽象,它使用超级块super block存放文件系统相关信息,使用索引节点inode存放文件的物理信息,使用目录项dentry存放文件的逻辑信息。



在不同格式的文件分区上,程序都可以正确地读写文件,并且结果是一样的。有时在使用linux系统的时候发现,可以在不同类型的文件分区内直接复制文件,对应用程序来说,并不知道文件系统的类型,甚至不知道文件的类型,这就是虚拟文件系统在背后做的工作。虚拟文件系统屏蔽了不同文件系统间的差异,向用户提供了统一的接口。

虚拟文件系统,即VFS(Virtual File System)是Linux内核中的一个软件抽象层。它通过一些数据结构及其方法向实际的文件系统如ext2,vfat等提供接口机制。通过使用同一套文件 I/O 系统调用即可对Linux中的任意文件进行操作而无需考虑其所在的具体文件系统格式;更进一步,文件操作可以在不同文件系统之间进行。在linux系统中,一切都可以被看做是文件。不仅普通的文本文件、目录可以当做文件进行处理,而且字符设备、块设备、套接字等都可以被当做文件进行处理。这些文件虽然类型不同,但是却使用同一种操作方法。这也是UNIX/Linux设计的基本哲学之一。

虚拟文件系统(简称VFS)是实现“一切都是文件”特性的关键,是Linux内核的一个软件层,向用户空间的程序提供文件系统接口;同时提供了内核中的一个抽象功能,允许不同类型的文件系统存在。VFS可以被理解为一种抽象的接口标准,系统中所有的文件系统不仅依靠VFS共存,也依靠VFS协同工作。为了能够支持不同的文件系统,VFS定义了所有文件系统都支持的、最基本的一个概念上的接口和数据结构,在实现一个具体的文件系统的时候,需要向VFS提供符合VFS标准的接口和数据结构,不同的文件系统可能在实体概念上有差别,但是使用VFS接口时需要和VFS定义的概念保持一致,只有这样,才能实现对用户的文件系统无关性。VFS隐藏了具体文件系统的操作细节,所以,在VFS这一层以及内核其他部分看来,所有的文件系统都是相同的。对文件系统访问的系统调用通过VFS软件层处理,VFS根据访问的请求调用不同的文件系统驱动的函数处理用户的请求。文件系统的代码在访问物理设备的时候,需要使用物理设备驱动访问真正的硬件。




网络接口


网络接口提供了对各种网络标准的存取和各种网络硬件的支持。如下图所示,在Linux中网络接口可分为网络协议和网络驱动程序,网络协议部分负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备通信,每一种可能的硬件设备都有相应的设备驱动程序。


(1)网络协议接口层向网络层协议提供统一的数据包收发接口,不论上层协议是ARP还是IP,都通过dev_queue_xmit()函数发送数据,并通过netif_rx()函数接收数据。这一层的存在使得上层协议独立于具体的设备。

(2)网络设备接口层向协议接口层提供的用于描述具体网络设备属性和操作的结构体net_device,该结构体是设备驱动功能层各函数的容器。

(3)设备驱动功能层的各函数是网络设备接口层net_device数据结构的具体成员,是驱使网络设备硬件完成相应动作的程序,它通过nto_start_xmit()函数启动发送操作,并通过网络设备上的中断触发接收操作。

(4)网络设备与媒介层是完成数据包发送和接收的物理实体,包括网络适配器和具体的传输媒介,网络适配器被设备驱动功能层中的函数在物理上驱动。

驱动工程师的工作:在设计具体的网络设备驱动程序时,需要完成的主要工作是编写设备驱动功能层的相关函数以填充net_device数据结构的内容并将net_device注册入内核。

写网络应用程序,使用socket通过TCP/IP协议与其他机器通信,和前面介绍的内核子系统相似,socket相关的函数也是通过内核的子系统完成的,担当这部分任务的是内核的网络子系统,有时也把这部分代码称为“网络堆栈”。Linux内核提供了优秀的网络处理能力和功能,这与网络堆栈代码的设计思想是分不开的,Linux的网络堆栈部分沿袭了传统的层次结构,网络数据从用户进程到达实际的网络设备需要四个层次:用户进程,套接字,网络协议,网络设备。

实际上,在每层里面还可以分为好多层次,数据传输的路径是按照层次来的,不能跨越某个层次。linux网络子系统对网络层次采用了类似面向对象的设计思路,把需要处理的层次抽象为不同的实体,并且定义了实体之间的关系和数据处理流程:

(1)网络协议:网络协议可以理解为一种语言,用于网络中不同设备之间的通信,是一种通信的规范。
(2)套接字:套接字是内核与用户程序的接口,一个套接字对应一个数据连接,并且向用户提供了文件I/O,用户可以像操作文件一样在数据连接上收发数据,具体的协议处理由网络协议部分处理。套接字是用户使用网络的接口。
(3)设备接口:设备接口是网络子系统中软件和硬件的接口,用户的数据最终是需要通过网络硬件设备发送和接收的,网络设备千差万别,设备驱动也不尽相同,通过设备接口屏蔽了具体设备驱动的差异。
(4)网络缓冲区:网络缓冲区也称为套接字缓冲区(sk_buff),是网络子系统中的一个重要结构。网络传输数据存在许多不定因素,除了物理设备对传输数据的限制(例如MMU),网络受到干扰、丢包、重传等,都会造成数据的不稳定,网络缓冲区通过对网络数据的重新整理,使业务处理的数据包是完整的。网络缓冲区是内存中的一块缓冲区,是网络系统与内存管理的接口。



进程通信


进程通信支持提供进程之间的通信,Linux支持进程间的多种通信机制,包含信号量、共享内存、管道等,这些机制可协助多个进程、多资源的互斥访问、进程间的同步和消息传递。


一、管道

管道,通常指无名管道,是 UNIX 系统IPC最古老的形式。


1、特点:

它是半双工的(即数据只能在一个方向上流动),具有固定的读端和写端。


它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。


它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。


2、原型:


#include <stdio.h>int pipe(int fd[2]);  // 返回值:若成功返回0,失败返回-1;


当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。如下图:

要关闭管道只需将这两个文件描述符关闭即可。

3、For Example

单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。如下图所示:


若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。

#include <stdio.h>#include <stdlib.h>int main(){    int fd[2]; // 两个文件描述符    pid_t pid;    char buff[20];    if (pipe(fd) < 0)    {        printf("Create Pipe Error!\n");    }    if ((pid = fork()) < 0) // 创建子进程    {        printf("Fork Error!\n");    }    else if (pid > 0) // 父进程    {        close(fd[0]); // 关闭读端        write(fd[1], "Hello World\n", 12);    }    else    {        close(fd[1]); // 关闭写端        read(fd[0], buff, 20);        printf ("%s", buff);    }    return 0;}

二、FIFO

FIFO,也称为命名管道,是一种文件类型。

1、特点

  1. FIFO可以在无关的进程之间交换数据,与无名管道不同。

  2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

2、原型

#include <stdio.h>// 返回值:成功返回0, 出错返回-1int mkfifo(const char* pathname, mode_t mode);

其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。


当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:


若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。


若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。


3、For Example

FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。下面的例子演示了使用 FIFO 进行 IPC 的过程:

write_fifo.c

#include<stdio.h>#include<stdlib.h> // exit#include<fcntl.h> // O_WRONLY#include<sys/stat.h>#include<time.h> // timeint main(){    int fd;    int n, i;    char buf[1024];    time_t tp;    printf("I am %d process.\n", getpid()); // 说明进程ID    if((fd = open("fifo1", O_WRONLY)) < 0) // 以写打开一个FIFO    {        perror("Open FIFO Failed");        exit(1);    }    for(i=0; i<10; ++i)    {        time(&tp); // 取系统当前时间        n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp));        printf("Send message: %s", buf); // 打印        if(write(fd, buf, n+1) < 0) // 写入到FIFO中        {            perror("Write FIFO Failed");            close(fd);            exit(1);        }        sleep(1); // 休眠1秒    }    close(fd); // 关闭FIFO文件    return 0;}


read_fifo.c

#include <stdio.h>int main(){    int fd;    int len;    char buf[1024];
if (mkfifo("fifo 1", 0666) < 0 && errno != EXIST) // 创建FIFO管道 { perror ("Create FIFO Failed\n"); } if ((fd = open("fifo 1", R_ONLY)) < 0) // 以读方式打开 { perror ("Open FIFO Failed\n"); exit(1); }
while ((len == read(buf, buf, 1024)) > 0) // 读取FIFO管道 { printf ("Read Message: %s", buf); }
close(fd); return 0;}

在两个终端里用 gcc 分别编译运行上面两个文件,可以看到输出结果如下:

[xq@localhost]$ ./write_fifoI am 7872 processSend message: Process 7872's time is Mon Jan 16 18:00:23 2023Send message: Process 7872's time is Mon Jan 16 18:00:24 2023Send message: Process 7872's time is Mon Jan 16 18:00:25 2023Send message: Process 7872's time is Mon Jan 16 18:00:26 2023Send message: Process 7872's time is Mon Jan 16 18:00:27 2023Send message: Process 7872's time is Mon Jan 16 18:00:28 2023Send message: Process 7872's time is Mon Jan 16 18:00:29 2023Send message: Process 7872's time is Mon Jan 16 18:00:30 2023Send message: Process 7872's time is Mon Jan 16 18:00:31 2023Send message: Process 7872's time is Mon Jan 16 18:00:32 2023



[xq@localhost]$ ./write_fifoI am 7872 processSend message: Process 7872's time is Mon Jan 16 18:00:23 2023Send message: Process 7872's time is Mon Jan 16 18:00:24 2023Send message: Process 7872's time is Mon Jan 16 18:00:25 2023Send message: Process 7872's time is Mon Jan 16 18:00:26 2023Send message: Process 7872's time is Mon Jan 16 18:00:27 2023Send message: Process 7872's time is Mon Jan 16 18:00:28 2023Send message: Process 7872's time is Mon Jan 16 18:00:29 2023Send message: Process 7872's time is Mon Jan 16 18:00:30 2023Send message: Process 7872's time is Mon Jan 16 18:00:31 2023Send message: Process 7872's time is Mon Jan 16 18:00:32 2023

上面的例子可以扩展成 客户端进程—服务端进程通信的实例,write_fifo的作用类似于客户端,可以打开多个客户端向一个服务器发送请求信息,read_fifo类似于服务器,它适时监控着FIFO的读端,当有数据时,读出并进行处理,但是有一个关键的问题是,每一个客户端必须预先知道服务器提供的FIFO接口,下图显示了这样的操作:


三、消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。

1、特点

  1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。

  2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。

  3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。

2、原型

#include <stdio.h>// 创建或打开消息队列:成功返回队列ID,失败返回-1int msgget(key_t key, int flag);// 添加消息:成功返回0,失败返回-1int msgsnd(int msqid, const void ptr, size_t size, int flag);// 读取消息:成功返回消息数据的长度,失败返回-1int msgrcv(int msqid, void* ptr, size_t size, long type, int flag);// 控制消息队列:成功返回0, 失败返回-1int msgctl(int msqid, int cmd, struct msqid_ds * buf);



在以下两种情况下,msgget将创建一个新的消息队列:

  • 如果没有与键值key相对应的消息队列,并且flag中包含了IPC_CREAT标志位。

  • key参数为IPC_PRIVATE。

函数msgrcv在读取消息队列时,type参数有下面几种情况:

  • type == 0,返回队列中的第一个消息;

  • type > 0,返回队列中消息类型为 type 的第一个消息;

  • type < 0,返回队列中消息类型值小于或等于 type 绝对值的消息,如果有多个,则取类型值最小的消息。

可以看出,type值非 0 时用于以非先进先出次序读消息。也可以把 type 看做优先级的权值。(其他的参数解释,请自行Google之)


四、信号量

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

1、特点

  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

  2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。

  4. 支持信号量组。


2、原型

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

#include <stdio.h>// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1int semget(key_t key, int num_sems, int sem_flags);// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1int semop(int semid, struct sembuf semoparray[], size_t numops);  // 控制信号量的相关信息int semctl(int semid, int sem_num, int cmd, ...);

当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1;如果是引用一个现有的集合,则将num_sems指定为 0 。

在semop函数中,sembuf结构的定义如下:

struct sembuf{short sem_num; // 信号量组中对应的序号,0~sem_nums-1short sem_op;  // 信号量值在一次操作中的改变量short sem_flg; // IPC_NOWAIT, SEM_UNDO}

其中 sem_op 是一次操作中的信号量的改变量:

  • 若sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。

  • 若sem_op < 0,请求 sem_op 的绝对值的资源。

    • sem_flg 指定IPC_NOWAIT,则semop函数出错返回EAGAIN

    • sem_flg 没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:

      • 当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;

      • 此信号量被删除,函数smeop出错返回EIDRM;

      • 进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR

      • 如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。

      • 当相应的资源数不能满足请求时,这个操作与sem_flg有关。

    • 若sem_op == 0,进程阻塞直到信号量的相应值为0:

      • sem_flg指定IPC_NOWAIT,则出错返回EAGAIN。

      • sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:

      • 信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;

      • 此信号量被删除,函数smeop出错返回EIDRM;

      • 进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR

      • 当信号量已经为0,函数立即返回。

      • 如果信号量的值不为0,则依据sem_flg决定函数动作:

在semctl函数中的命令有多种,这里就说两个常用的:

  • SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。

  • IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

五、共享内存

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。

1、特点

  1. 共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。

  2. 因为多个进程可以同时操作,所以需要进行同步。

  3. 信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。

2、原型

#include <stdio.h>// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1int shmget(key_t key, size_t size, int flag);// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1void shmat(int shm_id, const void *addr, int flag);// 断开与共享内存的连接:成功返回0,失败返回-1int shmdt(void addr); // 控制共享内存的相关信息:成功返回0,失败返回-1int shmctl(int shm_id, int cmd, struct shmid_ds *buf);


当用shmget函数创建一段共享内存时,必须指定其size;而如果引用一个已存在的共享内存,则将size指定为0 。


当一段共享内存被创建以后,它并不能被任何进程访问。必须使用shmat函数连接该共享内存到当前进程的地址空间,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问。


shmdt函数是用来断开shmat建立的连接的。注意,这并不是从系统中删除该共享内存,只是当前进程不能再访问该共享内存而已。

shmctl函数可以对共享内存执行多种操作,根据参数 cmd 执行相应的操作。常用的是IPC_RMID(从系统中删除该共享内存)。





往期回顾

【Linux内核源码分析】无线子系统
Linux内核源码分析课程





Linux内核之旅
Linux内核之旅
 最新文章