mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 14:43:42 +08:00
mod
This commit is contained in:
@@ -0,0 +1,429 @@
|
||||
<audio id="audio" title="36 | 进程间通信:遇到大项目需要项目组之间的合作才行" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/cd/86/cd5106444237bc58781dbadc29236586.mp3"></audio>
|
||||
|
||||
前面咱们接项目的时候,主要强调项目之间的隔离性。这是因为,我们刚开始接的都是小项目。随着我们接的项目越来越多,就难免遇到大项目,这就需要多个项目组进行合作才能完成。
|
||||
|
||||
两个项目组应该通过什么样的方式进行沟通与合作呢?作为老板,你应该如何设计整个流程呢?
|
||||
|
||||
## 管道模型
|
||||
|
||||
好在有这么多成熟的项目管理流程可以参考。最最传统的模型就是软件开发的**瀑布模型**(Waterfall Model)。所谓的瀑布模型,其实就是将整个软件开发过程分成多个阶段,往往是上一个阶段完全做完,才将输出结果交给下一个阶段。就像下面这张图展示的一样。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ed/c9/ed1fd2ede7a8fef5508c877e722345c9.png" alt="">
|
||||
|
||||
这种模型类似进程间通信的**管道模型**。还记得咱们最初学Linux命令的时候,有下面这样一行命令:
|
||||
|
||||
```
|
||||
ps -ef | grep 关键字 | awk '{print $2}' | xargs kill -9
|
||||
|
||||
```
|
||||
|
||||
这里面的竖线“|”就是一个管道。它会将前一个命令的输出,作为后一个命令的输入。从管道的这个名称可以看出来,管道是一种单向传输数据的机制,它其实是一段缓存,里面的数据只能从一端写入,从另一端读出。如果想互相通信,我们需要创建两个管道才行。
|
||||
|
||||
管道分为两种类型,“|” 表示的管道称为**匿名管道**,意思就是这个类型的管道没有名字,用完了就销毁了。就像上面那个命令里面的一样,竖线代表的管道随着命令的执行自动创建、自动销毁。用户甚至都不知道自己在用管道这种技术,就已经解决了问题。所以这也是面试题里面经常会问的,到时候千万别说这是竖线,而要回答背后的机制,管道。
|
||||
|
||||
另外一种类型是**命名管道**。这个类型的管道需要通过mkfifo命令显式地创建。
|
||||
|
||||
```
|
||||
mkfifo hello
|
||||
|
||||
```
|
||||
|
||||
hello就是这个管道的名称。管道以文件的形式存在,这也符合Linux里面一切皆文件的原则。这个时候,我们ls一下,可以看到,这个文件的类型是p,就是pipe的意思。
|
||||
|
||||
```
|
||||
# ls -l
|
||||
prw-r--r-- 1 root root 0 May 21 23:29 hello
|
||||
|
||||
```
|
||||
|
||||
接下来,我们可以往管道里面写入东西。例如,写入一个字符串。
|
||||
|
||||
```
|
||||
# echo "hello world" > hello
|
||||
|
||||
```
|
||||
|
||||
这个时候,管道里面的内容没有被读出,这个命令就是停在这里的,这说明当一个项目组要把它的输出交接给另一个项目组做输入,当没有交接完毕的时候,前一个项目组是不能撒手不管的。
|
||||
|
||||
这个时候,我们就需要重新连接一个终端。在终端中,用下面的命令读取管道里面的内容:
|
||||
|
||||
```
|
||||
# cat < hello
|
||||
hello world
|
||||
|
||||
```
|
||||
|
||||
一方面,我们能够看到,管道里面的内容被读取出来,打印到了终端上;另一方面,echo那个命令正常退出了,也即交接完毕,前一个项目组就完成了使命,可以解散了。
|
||||
|
||||
我们可以看出,瀑布模型的开发流程效率比较低下,因为团队之间无法频繁地沟通。而且,管道的使用模式,也不适合进程间频繁地交换数据。
|
||||
|
||||
于是,我们还得想其他的办法,例如我们是不是可以借鉴传统外企的沟通方式——邮件。邮件有一定的格式,例如抬头,正文,附件等,发送邮件可以建立收件人列表,所有在这个列表中的人,都可以反复地在此邮件基础上回复,达到频繁沟通的目的。
|
||||
|
||||
## 消息队列模型
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ac/a4/ac6ad6c9e7e3831f6d813113ae1c5ba4.png" alt="">
|
||||
|
||||
这种模型类似进程间通信的消息队列模型。和管道将信息一股脑儿地从一个进程,倒给另一个进程不同,消息队列有点儿像邮件,发送数据时,会分成一个一个独立的数据单元,也就是消息体,每个消息体都是固定大小的存储块,在字节流上不连续。
|
||||
|
||||
这个消息结构的定义我写在下面了。这里面的类型type和正文text没有强制规定,只要消息的发送方和接收方约定好即可。
|
||||
|
||||
```
|
||||
struct msg_buffer {
|
||||
long mtype;
|
||||
char mtext[1024];
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
接下来,我们需要创建一个消息队列,使用**msgget函数**。这个函数需要有一个参数key,这是消息队列的唯一标识,应该是唯一的。如何保持唯一性呢?这个还是和文件关联。
|
||||
|
||||
我们可以指定一个文件,ftok会根据这个文件的inode,生成一个近乎唯一的key。只要在这个消息队列的生命周期内,这个文件不要被删除就可以了。只要不删除,无论什么时刻,再调用ftok,也会得到同样的key。这种key的使用方式在这一章会经常遇到,这是因为它们都属于System V IPC进程间通信机制体系中。
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/msg.h>
|
||||
|
||||
|
||||
int main() {
|
||||
int messagequeueid;
|
||||
key_t key;
|
||||
|
||||
|
||||
if((key = ftok("/root/messagequeue/messagequeuekey", 1024)) < 0)
|
||||
{
|
||||
perror("ftok error");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
printf("Message Queue key: %d.\n", key);
|
||||
|
||||
|
||||
if ((messagequeueid = msgget(key, IPC_CREAT|0777)) == -1)
|
||||
{
|
||||
perror("msgget error");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
printf("Message queue id: %d.\n", messagequeueid);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在运行上面这个程序之前,我们先使用命令touch messagequeuekey,创建一个文件,然后多次执行的结果就会像下面这样:
|
||||
|
||||
```
|
||||
# ./a.out
|
||||
Message Queue key: 92536.
|
||||
Message queue id: 32768.
|
||||
|
||||
```
|
||||
|
||||
System V IPC体系有一个统一的命令行工具:ipcmk,ipcs和ipcrm用于创建、查看和删除IPC对象。
|
||||
|
||||
例如,ipcs -q就能看到上面我们创建的消息队列对象。
|
||||
|
||||
```
|
||||
# ipcs -q
|
||||
|
||||
|
||||
------ Message Queues --------
|
||||
key msqid owner perms used-bytes messages
|
||||
0x00016978 32768 root 777 0 0
|
||||
|
||||
```
|
||||
|
||||
接下来,我们来看如何发送信息。发送消息主要调用**msgsnd函数**。第一个参数是message queue的id,第二个参数是消息的结构体,第三个参数是消息的长度,最后一个参数是flag。这里IPC_NOWAIT表示发送的时候不阻塞,直接返回。
|
||||
|
||||
下面的这段程序,getopt_long、do-while循环以及switch,是用来解析命令行参数的。命令行参数的格式定义在long_options里面。每一项的第一个成员“id”“type”“message”是参数选项的全称,第二个成员都为1,表示参数选项后面要跟参数,最后一个成员’i’‘t’'m’是参数选项的简称。
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/msg.h>
|
||||
#include <getopt.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
struct msg_buffer {
|
||||
long mtype;
|
||||
char mtext[1024];
|
||||
};
|
||||
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int next_option;
|
||||
const char* const short_options = "i:t:m:";
|
||||
const struct option long_options[] = {
|
||||
{ "id", 1, NULL, 'i'},
|
||||
{ "type", 1, NULL, 't'},
|
||||
{ "message", 1, NULL, 'm'},
|
||||
{ NULL, 0, NULL, 0 }
|
||||
};
|
||||
|
||||
int messagequeueid = -1;
|
||||
struct msg_buffer buffer;
|
||||
buffer.mtype = -1;
|
||||
int len = -1;
|
||||
char * message = NULL;
|
||||
do {
|
||||
next_option = getopt_long (argc, argv, short_options, long_options, NULL);
|
||||
switch (next_option)
|
||||
{
|
||||
case 'i':
|
||||
messagequeueid = atoi(optarg);
|
||||
break;
|
||||
case 't':
|
||||
buffer.mtype = atol(optarg);
|
||||
break;
|
||||
case 'm':
|
||||
message = optarg;
|
||||
len = strlen(message) + 1;
|
||||
if (len > 1024) {
|
||||
perror("message too long.");
|
||||
exit(1);
|
||||
}
|
||||
memcpy(buffer.mtext, message, len);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}while(next_option != -1);
|
||||
|
||||
|
||||
if(messagequeueid != -1 && buffer.mtype != -1 && len != -1 && message != NULL){
|
||||
if(msgsnd(messagequeueid, &buffer, len, IPC_NOWAIT) == -1){
|
||||
perror("fail to send message.");
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
perror("arguments error");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接下来,我们可以编译并运行这个发送程序。
|
||||
|
||||
```
|
||||
gcc -o send sendmessage.c
|
||||
./send -i 32768 -t 123 -m "hello world"
|
||||
|
||||
```
|
||||
|
||||
接下来,我们再来看如何收消息。收消息主要调用**msgrcv函数**,第一个参数是message queue的id,第二个参数是消息的结构体,第三个参数是可接受的最大长度,第四个参数是消息类型,最后一个参数是flag,这里IPC_NOWAIT表示接收的时候不阻塞,直接返回。
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/msg.h>
|
||||
#include <getopt.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
struct msg_buffer {
|
||||
long mtype;
|
||||
char mtext[1024];
|
||||
};
|
||||
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int next_option;
|
||||
const char* const short_options = "i:t:";
|
||||
const struct option long_options[] = {
|
||||
{ "id", 1, NULL, 'i'},
|
||||
{ "type", 1, NULL, 't'},
|
||||
{ NULL, 0, NULL, 0 }
|
||||
};
|
||||
|
||||
int messagequeueid = -1;
|
||||
struct msg_buffer buffer;
|
||||
long type = -1;
|
||||
do {
|
||||
next_option = getopt_long (argc, argv, short_options, long_options, NULL);
|
||||
switch (next_option)
|
||||
{
|
||||
case 'i':
|
||||
messagequeueid = atoi(optarg);
|
||||
break;
|
||||
case 't':
|
||||
type = atol(optarg);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}while(next_option != -1);
|
||||
|
||||
|
||||
if(messagequeueid != -1 && type != -1){
|
||||
if(msgrcv(messagequeueid, &buffer, 1024, type, IPC_NOWAIT) == -1){
|
||||
perror("fail to recv message.");
|
||||
exit(1);
|
||||
}
|
||||
printf("received message type : %d, text: %s.", buffer.mtype, buffer.mtext);
|
||||
} else {
|
||||
perror("arguments error");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接下来,我们可以编译并运行这个发送程序。可以看到,如果有消息,可以正确地读到消息;如果没有,则返回没有消息。
|
||||
|
||||
```
|
||||
# ./recv -i 32768 -t 123
|
||||
received message type : 123, text: hello world.
|
||||
# ./recv -i 32768 -t 123
|
||||
fail to recv message.: No message of desired type
|
||||
|
||||
```
|
||||
|
||||
有了消息这种模型,两个进程之间的通信就像咱们平时发邮件一样,你来一封,我回一封,可以频繁沟通了。
|
||||
|
||||
## 共享内存模型
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/df/38/df910e4383885b1aceaafb52b9bb5638.png" alt="">
|
||||
|
||||
但是有时候,项目组之间的沟通需要特别紧密,而且要分享一些比较大的数据。如果使用邮件,就发现,一方面邮件的来去不及时;另外一方面,附件大小也有限制,所以,这个时候,我们经常采取的方式就是,把两个项目组在需要合作的期间,拉到一个会议室进行合作开发,这样大家可以直接交流文档呀,架构图呀,直接在白板上画或者直接扔给对方,就可以直接看到。
|
||||
|
||||
可以看出来,共享会议室这种模型,类似进程间通信的**共享内存模型**。前面咱们讲内存管理的时候,知道每个进程都有自己独立的虚拟内存空间,不同的进程的虚拟内存空间映射到不同的物理内存中去。这个进程访问A地址和另一个进程访问A地址,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。
|
||||
|
||||
但是,咱们是不是可以变通一下,拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去。
|
||||
|
||||
共享内存也是System V IPC进程间通信机制体系中的,所以从它使用流程可以看到熟悉的面孔。
|
||||
|
||||
我们可以创建一个共享内存,调用shmget。在这个体系中,创建一个IPC对象都是xxxget,这里面第一个参数是key,和msgget里面的key一样,都是唯一定位一个共享内存对象,也可以通过关联文件的方式实现唯一性。第二个参数是共享内存的大小。第三个参数如果是IPC_CREAT,同样表示创建一个新的。
|
||||
|
||||
```
|
||||
int shmget(key_t key, size_t size, int flag);
|
||||
|
||||
```
|
||||
|
||||
创建完毕之后,我们可以通过ipcs命令查看这个共享内存。
|
||||
|
||||
```
|
||||
#ipcs --shmems
|
||||
|
||||
|
||||
------ Shared Memory Segments ------
|
||||
key shmid owner perms bytes nattch status
|
||||
0x00000000 19398656 marc 600 1048576 2 dest
|
||||
|
||||
```
|
||||
|
||||
接下来,如果一个进程想要访问这一段共享内存,需要将这个内存加载到自己的虚拟地址空间的某个位置,通过shmat函数,就是attach的意思。其中addr就是要指定attach到这个地方。但是这个地址的设定难度比较大,除非对于内存布局非常熟悉,否则可能会attach到一个非法地址。所以,通常的做法是将addr设为NULL,让内核选一个合适的地址。返回值就是真正被attach的地方。
|
||||
|
||||
```
|
||||
void *shmat(int shm_id, const void *addr, int flag);
|
||||
|
||||
```
|
||||
|
||||
如果共享内存使用完毕,可以通过shmdt解除绑定,然后通过shmctl,将cmd设置为IPC_RMID,从而删除这个共享内存对象。
|
||||
|
||||
```
|
||||
int shmdt(void *addr);
|
||||
|
||||
|
||||
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
|
||||
|
||||
```
|
||||
|
||||
## 信号量
|
||||
|
||||
这里你是不是有一个疑问,如果两个进程attach同一个共享内存,大家都往里面写东西,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
|
||||
|
||||
所以,这里就需要一种保护机制,使得同一个共享的资源,同时只能被一个进程访问。在System V IPC进程间通信机制体系中,早就想好了应对办法,就是信号量(Semaphore)。因此,信号量和共享内存往往要配合使用。
|
||||
|
||||
信号量其实是一个计数器,主要用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
|
||||
|
||||
我们可以将信号量初始化为一个数值,来代表某种资源的总体数量。对于信号量来讲,会定义两种原子操作,一个是**P操作**,我们称为**申请资源操作**。这个操作会申请将信号量的数值减去N,表示这些数量被他申请使用了,其他人不能用了。另一个是**V操作**,我们称为**归还资源操作**,这个操作会申请将信号量加上M,表示这些数量已经还给信号量了,其他人可以使用了。
|
||||
|
||||
例如,你有100元钱,就可以将信号量设置为100。其中A向你借80元,就会调用P操作,申请减去80。如果同时B向你借50元,但是B的P操作比A晚,那就没有办法,只好等待A归还钱的时候,B的P操作才能成功。之后,A调用V操作,申请加上30元,也就是还给你30元,这个时候信号量有50元了,这时候B的P操作才能成功,才能借走这50元。
|
||||
|
||||
所谓**原子操作**(Atomic Operation),就是任何一块钱,都只能通过P操作借给一个人,不能同时借给两个人。也就是说,当A的P操作(借80)和B的P操作(借50),几乎同时到达的时候,不能因为大家都看到账户里有100就都成功,必须分个先来后到。
|
||||
|
||||
如果想创建一个信号量,我们可以通过semget函数。看,又是xxxget,第一个参数key也是类似的,第二个参数num_sems不是指资源的数量,而是表示可以创建多少个信号量,形成一组信号量,也就是说,如果你有多种资源需要管理,可以创建一个信号量组。
|
||||
|
||||
```
|
||||
int semget(key_t key, int num_sems, int sem_flags);
|
||||
|
||||
```
|
||||
|
||||
接下来,我们需要初始化信号量的总的资源数量。通过semctl函数,第一个参数semid是这个信号量组的id,第二个参数semnum才是在这个信号量组中某个信号量的id,第三个参数是命令,如果是初始化,则用SETVAL,第四个参数是一个union。如果初始化,应该用里面的val设置资源总量。
|
||||
|
||||
```
|
||||
int semctl(int semid, int semnum, int cmd, union semun args);
|
||||
|
||||
|
||||
union semun
|
||||
{
|
||||
int val;
|
||||
struct semid_ds *buf;
|
||||
unsigned short int *array;
|
||||
struct seminfo *__buf;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
无论是P操作还是V操作,我们统一用semop函数。第一个参数还是信号量组的id,一次可以操作多个信号量。第三个参数numops就是有多少个操作,第二个参数将这些操作放在一个数组中。
|
||||
|
||||
数组的每一项是一个struct sembuf,里面的第一个成员是这个操作的对象是哪个信号量。
|
||||
|
||||
第二个成员就是要对这个信号量做多少改变。如果sem_op < 0,就请求sem_op的绝对值的资源。如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
|
||||
|
||||
当相应的资源数不能满足请求时,就要看sem_flg了。如果把sem_flg设置为IPC_NOWAIT,也就是没有资源也不等待,则semop函数出错返回EAGAIN。如果sem_flg 没有指定IPC_NOWAIT,则进程挂起,直到当相应的资源数可以满足请求。若sem_op > 0,表示进程归还相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则唤醒它们。
|
||||
|
||||
```
|
||||
int semop(int semid, struct sembuf semoparray[], size_t numops);
|
||||
|
||||
|
||||
struct sembuf
|
||||
{
|
||||
short sem_num; // 信号量组中对应的序号,0~sem_nums-1
|
||||
short sem_op; // 信号量值在一次操作中的改变量
|
||||
short sem_flg; // IPC_NOWAIT, SEM_UNDO
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
信号量和共享内存都比较复杂,两者还要结合起来用,就更加复杂,它们内核的机制就更加复杂。这一节我们先不讲,放到本章的最后一节重点讲解。
|
||||
|
||||
## 信号
|
||||
|
||||
上面讲的进程间通信的方式,都是常规状态下的工作模式,对应到咱们平时的工作交接,收发邮件、联合开发等,其实还有一种异常情况下的工作模式。
|
||||
|
||||
例如出现线上系统故障,这个时候,什么流程都来不及了,不可能发邮件,也来不及开会,所有的架构师、开发、运维都要被通知紧急出动。所以,7乘24小时不间断执行的系统都需要有告警系统,一旦出事情,就要通知到人,哪怕是半夜,也要电话叫起来,处理故障。
|
||||
|
||||
对应到操作系统中,就是信号。信号没有特别复杂的数据结构,就是用一个代号一样的数字。Linux提供了几十种信号,分别代表不同的意义。信号之间依靠它们的值来区分。这就像咱们看警匪片,对于紧急的行动,都是说,“1号作战任务”开始执行,警察就开始行动了。情况紧急,不能啰里啰嗦了。
|
||||
|
||||
信号可以在任何时候发送给某一进程,进程需要为这个信号配置信号处理函数。当某个信号发生的时候,就默认执行这个函数就可以了。这就相当于咱们运维一个系统应急手册,当遇到什么情况,做什么事情,都事先准备好,出了事情照着做就可以了。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
这一节,我们整体讲解了一下进程间通信的各种模式。你现在还能记住多少?
|
||||
|
||||
- 类似瀑布开发模式的管道
|
||||
- 类似邮件模式的消息队列
|
||||
- 类似会议室联合开发的共享内存加信号量
|
||||
- 类似应急预案的信号
|
||||
|
||||
当你自己使用的时候,可以根据不同的通信需要,选择不同的模式。
|
||||
|
||||
- 管道,请你记住这是命令行中常用的模式,面试问到的话,不要忘了。
|
||||
- 消息队列其实很少使用,因为有太多的用户级别的消息队列,功能更强大。
|
||||
- 共享内存加信号量是常用的模式。这个需要牢记,常见到一些知名的以C语言开发的开源软件都会用到它。
|
||||
- 信号更加常用,机制也比较复杂。我们后面会有单独的一节来解析。
|
||||
|
||||
## 课堂练习
|
||||
|
||||
这节课的程序,请你务必自己编译通过,搞清楚参数解析是怎么做的,这个以后你自己写程序的时候,很有用,另外消息队列模型的API调用流程,也要搞清楚,要知道他们都属于System V系列,后面我们学共享内存和信号量,能看到完全类似的API调用流程。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
|
||||
@@ -0,0 +1,271 @@
|
||||
<audio id="audio" title="37 | 信号(上):项目组A完成了,如何及时通知项目组B?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/15/fe/15bee5da75b7dc425621b1b30bd7c2fe.mp3"></audio>
|
||||
|
||||
上一节最后,我们讲了信号的机制。在某些紧急情况下,我们需要给进程发送一个信号,紧急处理一些事情。
|
||||
|
||||
这种方式有点儿像咱们运维一个线上系统,为了应对一些突发事件,往往需要制定应急预案。就像下面的列表中一样。一旦发生了突发事件,马上能够找到负责人,根据处理步骤进行紧急响应,并且在限定的事件内搞定。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/0c/498199918340c55f59c91129ceb59f0c.png" alt="">
|
||||
|
||||
我们现在就按照应急预案的设计思路,来看一看Linux信号系统的机制。
|
||||
|
||||
首先,第一件要做的事情就是,整个团队要想一下,线上到底能够产生哪些异常情况,越全越好。于是,我们就有了上面这个很长很长的列表。
|
||||
|
||||
在Linux操作系统中,为了响应各种各样的事件,也是定义了非常多的信号。我们可以通过kill -l命令,查看所有的信号。
|
||||
|
||||
```
|
||||
# kill -l
|
||||
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
|
||||
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
|
||||
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
|
||||
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
|
||||
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
|
||||
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
|
||||
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
|
||||
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
|
||||
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
|
||||
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
|
||||
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
|
||||
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
|
||||
63) SIGRTMAX-1 64) SIGRTMAX
|
||||
|
||||
```
|
||||
|
||||
这些信号都是什么作用呢?我们可以通过man 7 signal命令查看,里面会有一个列表。
|
||||
|
||||
```
|
||||
Signal Value Action Comment
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
SIGHUP 1 Term Hangup detected on controlling terminal
|
||||
or death of controlling process
|
||||
SIGINT 2 Term Interrupt from keyboard
|
||||
SIGQUIT 3 Core Quit from keyboard
|
||||
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
|
||||
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
|
||||
……
|
||||
|
||||
```
|
||||
|
||||
就像应急预案里面给出的一样,每个信号都有一个唯一的ID,还有遇到这个信号的时候的默认操作。
|
||||
|
||||
一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式。
|
||||
|
||||
1.**执行默认操作**。Linux对每种信号都规定了默认操作,例如,上面列表中的Term,就是终止进程的意思。Core的意思是Core Dump,也即终止进程后,通过Core Dump将当前进程的运行状态保存在文件里面,方便程序员事后进行分析问题在哪里。
|
||||
|
||||
2.**捕捉信号**。我们可以为信号定义一个信号处理函数。当信号发生时,我们就执行相应的信号处理函数。
|
||||
|
||||
3.**忽略信号**。当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应用进程无法捕捉和忽略的,即SIGKILL和SEGSTOP,它们用于在任何时候中断或结束某一进程。
|
||||
|
||||
接下来,我们来看一下信号处理最常见的流程。这个过程主要是分成两步,第一步是注册信号处理函数。第二步是发送信号。这一节我们主要看第一步。
|
||||
|
||||
如果我们不想让某个信号执行默认操作,一种方法就是对特定的信号注册相应的信号处理函数,设置信号处理方式的是**signal函数**。
|
||||
|
||||
```
|
||||
typedef void (*sighandler_t)(int);
|
||||
sighandler_t signal(int signum, sighandler_t handler);
|
||||
|
||||
```
|
||||
|
||||
这其实就是定义一个方法,并且将这个方法和某个信号关联起来。当这个进程遇到这个信号的时候,就执行这个方法。
|
||||
|
||||
如果我们在Linux下面执行man signal的话,会发现Linux不建议我们直接用这个方法,而是改用sigaction。定义如下:
|
||||
|
||||
```
|
||||
int sigaction(int signum, const struct sigaction *act,
|
||||
struct sigaction *oldact);
|
||||
|
||||
```
|
||||
|
||||
这两者的区别在哪里呢?其实它还是将信号和一个动作进行关联,只不过这个动作由一个结构struct sigaction表示了。
|
||||
|
||||
```
|
||||
struct sigaction {
|
||||
__sighandler_t sa_handler;
|
||||
unsigned long sa_flags;
|
||||
__sigrestore_t sa_restorer;
|
||||
sigset_t sa_mask; /* mask last for extensibility */
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
和signal类似的是,这里面还是有__sighandler_t。但是,其他成员变量可以让你更加细致地控制信号处理的行为。而signal函数没有给你机会设置这些。这里需要注意的是,signal不是系统调用,而是glibc封装的一个函数。这样就像man signal里面写的一样,不同的实现方式,设置的参数会不同,会导致行为的不同。
|
||||
|
||||
例如,我们在glibc里面会看到了这样一个实现:
|
||||
|
||||
```
|
||||
# define signal __sysv_signal
|
||||
__sighandler_t
|
||||
__sysv_signal (int sig, __sighandler_t handler)
|
||||
{
|
||||
struct sigaction act, oact;
|
||||
......
|
||||
act.sa_handler = handler;
|
||||
__sigemptyset (&act.sa_mask);
|
||||
act.sa_flags = SA_ONESHOT | SA_NOMASK | SA_INTERRUPT;
|
||||
act.sa_flags &= ~SA_RESTART;
|
||||
if (__sigaction (sig, &act, &oact) < 0)
|
||||
return SIG_ERR;
|
||||
return oact.sa_handler;
|
||||
}
|
||||
weak_alias (__sysv_signal, sysv_signal)
|
||||
|
||||
```
|
||||
|
||||
在这里面,sa_flags进行了默认的设置。SA_ONESHOT是什么意思呢?意思就是,这里设置的信号处理函数,仅仅起作用一次。用完了一次后,就设置回默认行为。这其实并不是我们想看到的。毕竟我们一旦安装了一个信号处理函数,肯定希望它一直起作用,直到我显式地关闭它。
|
||||
|
||||
另外一个设置就是**SA_NOMASK**。我们通过__sigemptyset,将sa_mask设置为空。这样的设置表示在这个信号处理函数执行过程中,如果再有其他信号,哪怕相同的信号到来的时候,这个信号处理函数会被中断。如果一个信号处理函数真的被其他信号中断,其实问题也不大,因为当处理完了其他的信号处理函数后,还会回来接着处理这个信号处理函数的,但是对于相同的信号就有点尴尬了,这就需要这个信号处理函数写得比较有技巧了。
|
||||
|
||||
例如,对于这个信号的处理过程中,要操作某个数据结构,因为是相同的信号,很可能操作的是同一个实例,这样的话,同步、死锁这些都要想好。其实一般的思路应该是,当某一个信号的信号处理函数运行的时候,我们暂时屏蔽这个信号。后面我们还会仔细分析屏蔽这个动作,屏蔽并不意味着信号一定丢失,而是暂存,这样能够做到信号处理函数对于相同的信号,处理完一个再处理下一个,这样信号处理函数的逻辑要简单得多。
|
||||
|
||||
还有一个设置就是设置了**SA_INTERRUPT,清除了SA_RESTART**。这是什么意思呢?我们知道,信号的到来时间是不可预期的,有可能程序正在调用某个漫长的系统调用的时候(你可以在一台Linux机器上运行man 7 signal命令,在这里找Interruption of system calls and library functions by signal handlers的部分,里面说得非常详细),这个时候一个信号来了,会中断这个系统调用,去执行信号处理函数,那执行完了以后呢?系统调用怎么办呢?
|
||||
|
||||
这时候有两种处理方法,一种就是SA_INTERRUPT,也即系统调用被中断了,就不再重试这个系统调用了,而是直接返回一个-EINTR常量,告诉调用方,这个系统调用被信号中断了,但是怎么处理你看着办。如果是这样的话,调用方可以根据自己的逻辑,重新调用或者直接返回,这会使得我们的代码非常复杂,在所有系统调用的返回值判断里面,都要特殊判断一下这个值。
|
||||
|
||||
另外一种处理方法是SA_RESTART。这个时候系统调用会被自动重新启动,不需要调用方自己写代码。当然也可能存在问题,例如从终端读入一个字符,这个时候用户在终端输入一个`'a'`字符,在处理`'a'`字符的时候被信号中断了,等信号处理完毕,再次读入一个字符的时候,如果用户不再输入,就停在那里了,需要用户再次输入同一个字符。
|
||||
|
||||
因此,建议你使用sigaction函数,根据自己的需要定制参数。
|
||||
|
||||
接下来,我们来看sigaction具体做了些什么。
|
||||
|
||||
还记得在学习系统调用那一节的时候,我们知道,glibc里面有个文件syscalls.list。这里面定义了库函数调用哪些系统调用,在这里我们找到了sigaction。
|
||||
|
||||
```
|
||||
sigaction - sigaction i:ipp __sigaction sigaction
|
||||
|
||||
```
|
||||
|
||||
接下来,在glibc中,__sigaction会调用__libc_sigaction,并最终调用的系统调用是rt_sigaction。
|
||||
|
||||
```
|
||||
int
|
||||
__sigaction (int sig, const struct sigaction *act, struct sigaction *oact)
|
||||
{
|
||||
......
|
||||
return __libc_sigaction (sig, act, oact);
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
__libc_sigaction (int sig, const struct sigaction *act, struct sigaction *oact)
|
||||
{
|
||||
int result;
|
||||
struct kernel_sigaction kact, koact;
|
||||
|
||||
|
||||
if (act)
|
||||
{
|
||||
kact.k_sa_handler = act->sa_handler;
|
||||
memcpy (&kact.sa_mask, &act->sa_mask, sizeof (sigset_t));
|
||||
kact.sa_flags = act->sa_flags | SA_RESTORER;
|
||||
|
||||
|
||||
kact.sa_restorer = &restore_rt;
|
||||
}
|
||||
|
||||
|
||||
result = INLINE_SYSCALL (rt_sigaction, 4,
|
||||
sig, act ? &kact : NULL,
|
||||
oact ? &koact : NULL, _NSIG / 8);
|
||||
if (oact && result >= 0)
|
||||
{
|
||||
oact->sa_handler = koact.k_sa_handler;
|
||||
memcpy (&oact->sa_mask, &koact.sa_mask, sizeof (sigset_t));
|
||||
oact->sa_flags = koact.sa_flags;
|
||||
oact->sa_restorer = koact.sa_restorer;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这也是很多人看信号处理的内核实现的时候,比较困惑的地方。例如,内核代码注释里面会说,系统调用signal是为了兼容过去,系统调用sigaction也是为了兼容过去,连参数都变成了struct compat_old_sigaction,所以说,我们的库函数虽然调用的是sigaction,到了系统调用层,调用的可不是系统调用sigaction,而是系统调用rt_sigaction。
|
||||
|
||||
```
|
||||
SYSCALL_DEFINE4(rt_sigaction, int, sig,
|
||||
const struct sigaction __user *, act,
|
||||
struct sigaction __user *, oact,
|
||||
size_t, sigsetsize)
|
||||
{
|
||||
struct k_sigaction new_sa, old_sa;
|
||||
int ret = -EINVAL;
|
||||
......
|
||||
if (act) {
|
||||
if (copy_from_user(&new_sa.sa, act, sizeof(new_sa.sa)))
|
||||
return -EFAULT;
|
||||
}
|
||||
|
||||
|
||||
ret = do_sigaction(sig, act ? &new_sa : NULL, oact ? &old_sa : NULL);
|
||||
|
||||
|
||||
if (!ret && oact) {
|
||||
if (copy_to_user(oact, &old_sa.sa, sizeof(old_sa.sa)))
|
||||
return -EFAULT;
|
||||
}
|
||||
out:
|
||||
return ret;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在rt_sigaction里面,我们将用户态的struct sigaction结构,拷贝为内核态的k_sigaction,然后调用do_sigaction。do_sigaction也很简单,还记得进程内核的数据结构里,struct task_struct里面有一个成员sighand,里面有一个action。这是一个数组,下标是信号,内容就是信号处理函数,do_sigaction就是设置sighand里的信号处理函数。
|
||||
|
||||
```
|
||||
int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
|
||||
{
|
||||
struct task_struct *p = current, *t;
|
||||
struct k_sigaction *k;
|
||||
sigset_t mask;
|
||||
......
|
||||
k = &p->sighand->action[sig-1];
|
||||
|
||||
|
||||
spin_lock_irq(&p->sighand->siglock);
|
||||
if (oact)
|
||||
*oact = *k;
|
||||
|
||||
|
||||
if (act) {
|
||||
sigdelsetmask(&act->sa.sa_mask,
|
||||
sigmask(SIGKILL) | sigmask(SIGSTOP));
|
||||
*k = *act;
|
||||
......
|
||||
}
|
||||
|
||||
|
||||
spin_unlock_irq(&p->sighand->siglock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
至此,信号处理函数的注册已经完成了。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
这一节讲了如何通过API注册一个信号处理函数,整个过程如下图所示。
|
||||
|
||||
- 在用户程序里面,有两个函数可以调用,一个是signal,一个是sigaction,推荐使用sigaction。
|
||||
- 用户程序调用的是Glibc里面的函数,signal调用的是__sysv_signal,里面默认设置了一些参数,使得signal的功能受到了限制,sigaction调用的是__sigaction,参数用户可以任意设定。
|
||||
- 无论是__sysv_signal还是__sigaction,调用的都是统一的一个系统调用rt_sigaction。
|
||||
- 在内核中,rt_sigaction调用的是do_sigaction设置信号处理函数。在每一个进程的task_struct里面,都有一个sighand指向struct sighand_struct,里面是一个数组,下标是信号,里面的内容是信号处理函数。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7c/28/7cb86c73b9e73893e6b0e0433d476928.png" alt="">
|
||||
|
||||
## 课堂练习
|
||||
|
||||
你可以试着写一个程序,调用sigaction为某个信号设置一个信号处理函数,在信号处理函数中,如果收到信号则打印一些字符串,然后用命令kill发送信号,看是否字符串被正常输出。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
|
||||
@@ -0,0 +1,480 @@
|
||||
<audio id="audio" title="38 | 信号(下):项目组A完成了,如何及时通知项目组B?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b1/e5/b1615cc6dbdbe57497d8661780de59e5.mp3"></audio>
|
||||
|
||||
信号处理最常见的流程主要是两步,第一步是注册信号处理函数,第二步是发送信号和处理信号。上一节,我们讲了注册信号处理函数,那一般什么情况下会产生信号呢?我们这一节就来看一看。
|
||||
|
||||
## 信号的发送
|
||||
|
||||
有时候,我们在终端输入某些组合键的时候,会给进程发送信号,例如,Ctrl+C产生SIGINT信号,Ctrl+Z产生SIGTSTP信号。
|
||||
|
||||
有的时候,硬件异常也会产生信号。比如,执行了除以0的指令,CPU就会产生异常,然后把SIGFPE信号发送给进程。再如,进程访问了非法内存,内存管理模块就会产生异常,然后把信号SIGSEGV发送给进程。
|
||||
|
||||
这里同样是硬件产生的,对于中断和信号还是要加以区别。咱们前面讲过,中断要注册中断处理函数,但是中断处理函数是在内核驱动里面的,信号也要注册信号处理函数,信号处理函数是在用户态进程里面的。
|
||||
|
||||
对于硬件触发的,无论是中断,还是信号,肯定是先到内核的,然后内核对于中断和信号处理方式不同。一个是完全在内核里面处理完毕,一个是将信号放在对应的进程task_struct里信号相关的数据结构里面,然后等待进程在用户态去处理。当然有些严重的信号,内核会把进程干掉。但是,这也能看出来,中断和信号的严重程度不一样,信号影响的往往是某一个进程,处理慢了,甚至错了,也不过这个进程被干掉,而中断影响的是整个系统。一旦中断处理中有了bug,可能整个Linux都挂了。
|
||||
|
||||
有时候,内核在某些情况下,也会给进程发送信号。例如,向读端已关闭的管道写数据时产生SIGPIPE信号,当子进程退出时,我们要给父进程发送SIG_CHLD信号等。
|
||||
|
||||
最直接的发送信号的方法就是,通过命令kill来发送信号了。例如,我们都知道的kill -9 pid可以发送信号给一个进程,杀死它。
|
||||
|
||||
另外,我们还可以通过kill或者sigqueue系统调用,发送信号给某个进程,也可以通过tkill或者tgkill发送信号给某个线程。虽然方式多种多样,但是最终都是调用了do_send_sig_info函数,将信号放在相应的task_struct的信号数据结构中。
|
||||
|
||||
- kill->kill_something_info->kill_pid_info->group_send_sig_info->do_send_sig_info
|
||||
- tkill->do_tkill->do_send_specific->do_send_sig_info
|
||||
- tgkill->do_tkill->do_send_specific->do_send_sig_info
|
||||
- rt_sigqueueinfo->do_rt_sigqueueinfo->kill_proc_info->kill_pid_info->group_send_sig_info->do_send_sig_info
|
||||
|
||||
do_send_sig_info会调用send_signal,进而调用__send_signal。
|
||||
|
||||
```
|
||||
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
|
||||
{
|
||||
struct siginfo info;
|
||||
|
||||
info.si_signo = sig;
|
||||
info.si_errno = 0;
|
||||
info.si_code = SI_USER;
|
||||
info.si_pid = task_tgid_vnr(current);
|
||||
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
|
||||
|
||||
return kill_something_info(sig, &info, pid);
|
||||
}
|
||||
|
||||
|
||||
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
|
||||
int group, int from_ancestor_ns)
|
||||
{
|
||||
struct sigpending *pending;
|
||||
struct sigqueue *q;
|
||||
int override_rlimit;
|
||||
int ret = 0, result;
|
||||
......
|
||||
pending = group ? &t->signal->shared_pending : &t->pending;
|
||||
......
|
||||
if (legacy_queue(pending, sig))
|
||||
goto ret;
|
||||
|
||||
if (sig < SIGRTMIN)
|
||||
override_rlimit = (is_si_special(info) || info->si_code >= 0);
|
||||
else
|
||||
override_rlimit = 0;
|
||||
|
||||
q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,
|
||||
override_rlimit);
|
||||
if (q) {
|
||||
list_add_tail(&q->list, &pending->list);
|
||||
switch ((unsigned long) info) {
|
||||
case (unsigned long) SEND_SIG_NOINFO:
|
||||
q->info.si_signo = sig;
|
||||
q->info.si_errno = 0;
|
||||
q->info.si_code = SI_USER;
|
||||
q->info.si_pid = task_tgid_nr_ns(current,
|
||||
task_active_pid_ns(t));
|
||||
q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
|
||||
break;
|
||||
case (unsigned long) SEND_SIG_PRIV:
|
||||
q->info.si_signo = sig;
|
||||
q->info.si_errno = 0;
|
||||
q->info.si_code = SI_KERNEL;
|
||||
q->info.si_pid = 0;
|
||||
q->info.si_uid = 0;
|
||||
break;
|
||||
default:
|
||||
copy_siginfo(&q->info, info);
|
||||
if (from_ancestor_ns)
|
||||
q->info.si_pid = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
userns_fixup_signal_uid(&q->info, t);
|
||||
|
||||
}
|
||||
......
|
||||
out_set:
|
||||
signalfd_notify(t, sig);
|
||||
sigaddset(&pending->signal, sig);
|
||||
complete_signal(sig, t, group);
|
||||
ret:
|
||||
return ret;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这里,我们看到,在学习进程数据结构中task_struct里面的sigpending。在上面的代码里面,我们先是要决定应该用哪个sigpending。这就要看我们发送的信号,是给进程的还是线程的。如果是kill发送的,也就是发送给整个进程的,就应该发送给t->signal->shared_pending。这里面是整个进程所有线程共享的信号;如果是tkill发送的,也就是发给某个线程的,就应该发给t->pending。这里面是这个线程的task_struct独享的。
|
||||
|
||||
struct sigpending里面有两个成员,一个是一个集合sigset_t,表示都收到了哪些信号,还有一个链表,也表示收到了哪些信号。它的结构如下:
|
||||
|
||||
```
|
||||
struct sigpending {
|
||||
struct list_head list;
|
||||
sigset_t signal;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
如果都表示收到了信号,这两者有什么区别呢?我们接着往下看__send_signal里面的代码。接下来,我们要调用legacy_queue。如果满足条件,那就直接退出。那legacy_queue里面判断的是什么条件呢?我们来看它的代码。
|
||||
|
||||
```
|
||||
static inline int legacy_queue(struct sigpending *signals, int sig)
|
||||
{
|
||||
return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
|
||||
}
|
||||
|
||||
|
||||
#define SIGRTMIN 32
|
||||
#define SIGRTMAX _NSIG
|
||||
#define _NSIG 64
|
||||
|
||||
```
|
||||
|
||||
当信号小于SIGRTMIN,也即32的时候,如果我们发现这个信号已经在集合里面了,就直接退出了。这样会造成什么现象呢?就是信号的丢失。例如,我们发送给进程100个SIGUSR1(对应的信号为10),那最终能够被我们的信号处理函数处理的信号有多少呢?这就不好说了,比如总共5个SIGUSR1,分别是A、B、C、D、E。
|
||||
|
||||
如果这五个信号来得太密。A来了,但是信号处理函数还没来得及处理,B、C、D、E就都来了。根据上面的逻辑,因为A已经将SIGUSR1放在sigset_t集合中了,因而后面四个都要丢失。 如果是另一种情况,A来了已经被信号处理函数处理了,内核在调用信号处理函数之前,我们会将集合中的标志位清除,这个时候B再来,B还是会进入集合,还是会被处理,也就不会丢。
|
||||
|
||||
这样信号能够处理多少,和信号处理函数什么时候被调用,信号多大频率被发送,都有关系,而且从后面的分析,我们可以知道,信号处理函数的调用时间也是不确定的。看小于32的信号如此不靠谱,我们就称它为**不可靠信号**。
|
||||
|
||||
如果大于32的信号是什么情况呢?我们接着看。接下来,__sigqueue_alloc会分配一个struct sigqueue对象,然后通过list_add_tail挂在struct sigpending里面的链表上。这样就靠谱多了是不是?如果发送过来100个信号,变成链表上的100项,都不会丢,哪怕相同的信号发送多遍,也处理多遍。因此,大于32的信号我们称为**可靠信号**。当然,队列的长度也是有限制的,如果我们执行ulimit命令,可以看到,这个限制pending signals (-i) 15408。
|
||||
|
||||
当信号挂到了task_struct结构之后,最后我们需要调用complete_signal。这里面的逻辑也很简单,就是说,既然这个进程有了一个新的信号,赶紧找一个线程处理一下吧。
|
||||
|
||||
```
|
||||
static void complete_signal(int sig, struct task_struct *p, int group)
|
||||
{
|
||||
struct signal_struct *signal = p->signal;
|
||||
struct task_struct *t;
|
||||
|
||||
/*
|
||||
* Now find a thread we can wake up to take the signal off the queue.
|
||||
*
|
||||
* If the main thread wants the signal, it gets first crack.
|
||||
* Probably the least surprising to the average bear.
|
||||
*/
|
||||
if (wants_signal(sig, p))
|
||||
t = p;
|
||||
else if (!group || thread_group_empty(p))
|
||||
/*
|
||||
* There is just one thread and it does not need to be woken.
|
||||
* It will dequeue unblocked signals before it runs again.
|
||||
*/
|
||||
return;
|
||||
else {
|
||||
/*
|
||||
* Otherwise try to find a suitable thread.
|
||||
*/
|
||||
t = signal->curr_target;
|
||||
while (!wants_signal(sig, t)) {
|
||||
t = next_thread(t);
|
||||
if (t == signal->curr_target)
|
||||
return;
|
||||
}
|
||||
signal->curr_target = t;
|
||||
}
|
||||
......
|
||||
/*
|
||||
* The signal is already in the shared-pending queue.
|
||||
* Tell the chosen thread to wake up and dequeue it.
|
||||
*/
|
||||
signal_wake_up(t, sig == SIGKILL);
|
||||
return;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在找到了一个进程或者线程的task_struct之后,我们要调用signal_wake_up,来企图唤醒它,signal_wake_up会调用signal_wake_up_state。
|
||||
|
||||
```
|
||||
void signal_wake_up_state(struct task_struct *t, unsigned int state)
|
||||
{
|
||||
set_tsk_thread_flag(t, TIF_SIGPENDING);
|
||||
|
||||
|
||||
if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
|
||||
kick_process(t);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
signal_wake_up_state里面主要做了两件事情。第一,就是给这个线程设置TIF_SIGPENDING,这就说明其实信号的处理和进程的调度是采取这样一种类似的机制。还记得咱们调度的时候是怎么操作的吗?
|
||||
|
||||
当发现一个进程应该被调度的时候,我们并不直接把它赶下来,而是设置一个标识位TIF_NEED_RESCHED,表示等待调度,然后等待系统调用结束或者中断处理结束,从内核态返回用户态的时候,调用schedule函数进行调度。信号也是类似的,当信号来的时候,我们并不直接处理这个信号,而是设置一个标识位TIF_SIGPENDING,来表示已经有信号等待处理。同样等待系统调用结束,或者中断处理结束,从内核态返回用户态的时候,再进行信号的处理。
|
||||
|
||||
signal_wake_up_state的第二件事情,就是试图唤醒这个进程或者线程。wake_up_state会调用try_to_wake_up方法。这个函数我们讲进程的时候讲过,就是将这个进程或者线程设置为TASK_RUNNING,然后放在运行队列中,这个时候,当随着时钟不断的滴答,迟早会被调用。如果wake_up_state返回0,说明进程或者线程已经是TASK_RUNNING状态了,如果它在另外一个CPU上运行,则调用kick_process发送一个处理器间中断,强制那个进程或者线程重新调度,重新调度完毕后,会返回用户态运行。这是一个时机会检查TIF_SIGPENDING标识位。
|
||||
|
||||
## 信号的处理
|
||||
|
||||
好了,信号已经发送到位了,什么时候真正处理它呢?
|
||||
|
||||
就是在从系统调用或者中断返回的时候,咱们讲调度的时候讲过,无论是从系统调用返回还是从中断返回,都会调用exit_to_usermode_loop,只不过我们上次主要关注了_TIF_NEED_RESCHED这个标识位,这次我们重点关注**_TIF_SIGPENDING标识位**。
|
||||
|
||||
```
|
||||
static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
|
||||
{
|
||||
while (true) {
|
||||
......
|
||||
if (cached_flags & _TIF_NEED_RESCHED)
|
||||
schedule();
|
||||
......
|
||||
/* deal with pending signal delivery */
|
||||
if (cached_flags & _TIF_SIGPENDING)
|
||||
do_signal(regs);
|
||||
......
|
||||
if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS))
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如果在前一个环节中,已经设置了_TIF_SIGPENDING,我们就调用do_signal进行处理。
|
||||
|
||||
```
|
||||
void do_signal(struct pt_regs *regs)
|
||||
{
|
||||
struct ksignal ksig;
|
||||
|
||||
if (get_signal(&ksig)) {
|
||||
/* Whee! Actually deliver the signal. */
|
||||
handle_signal(&ksig, regs);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Did we come from a system call? */
|
||||
if (syscall_get_nr(current, regs) >= 0) {
|
||||
/* Restart the system call - no handlers present */
|
||||
switch (syscall_get_error(current, regs)) {
|
||||
case -ERESTARTNOHAND:
|
||||
case -ERESTARTSYS:
|
||||
case -ERESTARTNOINTR:
|
||||
regs->ax = regs->orig_ax;
|
||||
regs->ip -= 2;
|
||||
break;
|
||||
|
||||
case -ERESTART_RESTARTBLOCK:
|
||||
regs->ax = get_nr_restart_syscall(regs);
|
||||
regs->ip -= 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
restore_saved_sigmask();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
do_signal会调用handle_signal。按说,信号处理就是调用用户提供的信号处理函数,但是这事儿没有看起来这么简单,因为信号处理函数是在用户态的。
|
||||
|
||||
咱们又要来回忆系统调用的过程了。这个进程当时在用户态执行到某一行Line A,调用了一个系统调用,在进入内核的那一刻,在内核pt_regs里面保存了用户态执行到了Line A。现在我们从系统调用返回用户态了,按说应该从pt_regs拿出Line A,然后接着Line A执行下去,但是为了响应信号,我们不能回到用户态的时候返回Line A了,而是应该返回信号处理函数的起始地址。
|
||||
|
||||
```
|
||||
static void
|
||||
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
|
||||
{
|
||||
bool stepping, failed;
|
||||
......
|
||||
/* Are we from a system call? */
|
||||
if (syscall_get_nr(current, regs) >= 0) {
|
||||
/* If so, check system call restarting.. */
|
||||
switch (syscall_get_error(current, regs)) {
|
||||
case -ERESTART_RESTARTBLOCK:
|
||||
case -ERESTARTNOHAND:
|
||||
regs->ax = -EINTR;
|
||||
break;
|
||||
case -ERESTARTSYS:
|
||||
if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
|
||||
regs->ax = -EINTR;
|
||||
break;
|
||||
}
|
||||
/* fallthrough */
|
||||
case -ERESTARTNOINTR:
|
||||
regs->ax = regs->orig_ax;
|
||||
regs->ip -= 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
......
|
||||
failed = (setup_rt_frame(ksig, regs) < 0);
|
||||
......
|
||||
signal_setup_done(failed, ksig, stepping);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这个时候,我们就需要干预和自己来定制pt_regs了。这个时候,我们要看,是否从系统调用中返回。如果是从系统调用返回的话,还要区分我们是从系统调用中正常返回,还是在一个非运行状态的系统调用中,因为会被信号中断而返回。
|
||||
|
||||
我们这里解析一个最复杂的场景。还记得咱们解析进程调度的时候,我们举的一个例子,就是从一个tap网卡中读取数据。当时我们主要关注schedule那一行,也即如果当发现没有数据的时候,就调用schedule,自己进入等待状态,然后将CPU让给其他进程。具体的代码如下:
|
||||
|
||||
```
|
||||
static ssize_t tap_do_read(struct tap_queue *q,
|
||||
struct iov_iter *to,
|
||||
int noblock, struct sk_buff *skb)
|
||||
{
|
||||
......
|
||||
while (1) {
|
||||
if (!noblock)
|
||||
prepare_to_wait(sk_sleep(&q->sk), &wait,
|
||||
TASK_INTERRUPTIBLE);
|
||||
|
||||
/* Read frames from the queue */
|
||||
skb = skb_array_consume(&q->skb_array);
|
||||
if (skb)
|
||||
break;
|
||||
if (noblock) {
|
||||
ret = -EAGAIN;
|
||||
break;
|
||||
}
|
||||
if (signal_pending(current)) {
|
||||
ret = -ERESTARTSYS;
|
||||
break;
|
||||
}
|
||||
/* Nothing to read, let's sleep */
|
||||
schedule();
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里我们关注和信号相关的部分。这其实是一个信号中断系统调用的典型逻辑。
|
||||
|
||||
首先,我们把当前进程或者线程的状态设置为TASK_INTERRUPTIBLE,这样才能使这个系统调用可以被中断。
|
||||
|
||||
其次,可以被中断的系统调用往往是比较慢的调用,并且会因为数据不就绪而通过schedule让出CPU进入等待状态。在发送信号的时候,我们除了设置这个进程和线程的_TIF_SIGPENDING标识位之外,还试图唤醒这个进程或者线程,也就是将它从等待状态中设置为TASK_RUNNING。
|
||||
|
||||
当这个进程或者线程再次运行的时候,我们根据进程调度第一定律,从schedule函数中返回,然后再次进入while循环。由于这个进程或者线程是由信号唤醒的,而不是因为数据来了而唤醒的,因而是读不到数据的,但是在signal_pending函数中,我们检测到了_TIF_SIGPENDING标识位,这说明系统调用没有真的做完,于是返回一个错误ERESTARTSYS,然后带着这个错误从系统调用返回。
|
||||
|
||||
然后,我们到了exit_to_usermode_loop->do_signal->handle_signal。在这里面,当发现出现错误ERESTARTSYS的时候,我们就知道这是从一个没有调用完的系统调用返回的,设置系统调用错误码EINTR。
|
||||
|
||||
接下来,我们就开始折腾pt_regs了,主要通过调用setup_rt_frame->__setup_rt_frame。
|
||||
|
||||
```
|
||||
static int __setup_rt_frame(int sig, struct ksignal *ksig,
|
||||
sigset_t *set, struct pt_regs *regs)
|
||||
{
|
||||
struct rt_sigframe __user *frame;
|
||||
void __user *fp = NULL;
|
||||
int err = 0;
|
||||
|
||||
frame = get_sigframe(&ksig->ka, regs, sizeof(struct rt_sigframe), &fp);
|
||||
......
|
||||
put_user_try {
|
||||
......
|
||||
/* Set up to return from userspace. If provided, use a stub
|
||||
already in userspace. */
|
||||
/* x86-64 should always use SA_RESTORER. */
|
||||
if (ksig->ka.sa.sa_flags & SA_RESTORER) {
|
||||
put_user_ex(ksig->ka.sa.sa_restorer, &frame->pretcode);
|
||||
}
|
||||
} put_user_catch(err);
|
||||
|
||||
err |= setup_sigcontext(&frame->uc.uc_mcontext, fp, regs, set->sig[0]);
|
||||
err |= __copy_to_user(&frame->uc.uc_sigmask, set, sizeof(*set));
|
||||
|
||||
/* Set up registers for signal handler */
|
||||
regs->di = sig;
|
||||
/* In case the signal handler was declared without prototypes */
|
||||
regs->ax = 0;
|
||||
|
||||
regs->si = (unsigned long)&frame->info;
|
||||
regs->dx = (unsigned long)&frame->uc;
|
||||
regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
|
||||
|
||||
regs->sp = (unsigned long)frame;
|
||||
regs->cs = __USER_CS;
|
||||
......
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
frame的类型是rt_sigframe。frame的意思是帧。我们只有在学习栈的时候,提到过栈帧的概念。对的,这个frame就是一个栈帧。
|
||||
|
||||
我们在get_sigframe中会得到pt_regs的sp变量,也就是原来这个程序在用户态的栈顶指针,然后get_sigframe中,我们会将sp减去sizeof(struct rt_sigframe),也就是把这个栈帧塞到了栈里面,然后我们又在__setup_rt_frame中把regs->sp设置成等于frame。这就相当于强行在程序原来的用户态的栈里面插入了一个栈帧,并在最后将regs->ip设置为用户定义的信号处理函数sa_handler。这意味着,本来返回用户态应该接着原来的代码执行的,现在不了,要执行sa_handler了。那执行完了以后呢?按照函数栈的规则,弹出上一个栈帧来,也就是弹出了frame。
|
||||
|
||||
那如果我们假设sa_handler成功返回了,怎么回到程序原来在用户态运行的地方呢?玄机就在frame里面。要想恢复原来运行的地方,首先,原来的pt_regs不能丢,这个没问题,是在setup_sigcontext里面,将原来的pt_regs保存在了frame中的uc_mcontext里面。
|
||||
|
||||
另外,很重要的一点,程序如何跳过去呢?在__setup_rt_frame中,还有一个不引起重视的操作,那就是通过put_user_ex,将sa_restorer放到了frame->pretcode里面,而且还是按照函数栈的规则。函数栈里面包含了函数执行完跳回去的地址。当sa_handler执行完之后,弹出的函数栈是frame,也就应该跳到sa_restorer的地址。这是什么地址呢?
|
||||
|
||||
咱们在sigaction介绍的时候就没有介绍它,在Glibc的__libc_sigaction函数中也没有注意到,它被赋值成了restore_rt。这其实就是sa_handler执行完毕之后,马上要执行的函数。从名字我们就能感觉到,它将恢复原来程序运行的地方。
|
||||
|
||||
在Glibc中,我们可以找到它的定义,它竟然调用了一个系统调用,系统调用号为__NR_rt_sigreturn。
|
||||
|
||||
```
|
||||
RESTORE (restore_rt, __NR_rt_sigreturn)
|
||||
|
||||
#define RESTORE(name, syscall) RESTORE2 (name, syscall)
|
||||
# define RESTORE2(name, syscall) \
|
||||
asm \
|
||||
( \
|
||||
".LSTART_" #name ":\n" \
|
||||
" .type __" #name ",@function\n" \
|
||||
"__" #name ":\n" \
|
||||
" movq $" #syscall ", %rax\n" \
|
||||
" syscall\n" \
|
||||
......
|
||||
|
||||
```
|
||||
|
||||
我们可以在内核里面找到__NR_rt_sigreturn对应的系统调用。
|
||||
|
||||
```
|
||||
asmlinkage long sys_rt_sigreturn(void)
|
||||
{
|
||||
struct pt_regs *regs = current_pt_regs();
|
||||
struct rt_sigframe __user *frame;
|
||||
sigset_t set;
|
||||
unsigned long uc_flags;
|
||||
|
||||
frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long));
|
||||
if (__copy_from_user(&set, &frame->uc.uc_sigmask, sizeof(set)))
|
||||
goto badframe;
|
||||
if (__get_user(uc_flags, &frame->uc.uc_flags))
|
||||
goto badframe;
|
||||
|
||||
set_current_blocked(&set);
|
||||
|
||||
if (restore_sigcontext(regs, &frame->uc.uc_mcontext, uc_flags))
|
||||
goto badframe;
|
||||
......
|
||||
return regs->ax;
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这里面,我们把上次填充的那个rt_sigframe拿出来,然后restore_sigcontext将pt_regs恢复成为原来用户态的样子。从这个系统调用返回的时候,应用还误以为从上次的系统调用返回的呢。
|
||||
|
||||
至此,整个信号处理过程才全部结束。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
信号的发送与处理是一个复杂的过程,这里来总结一下。
|
||||
|
||||
1. 假设我们有一个进程A,main函数里面调用系统调用进入内核。
|
||||
1. 按照系统调用的原理,会将用户态栈的信息保存在pt_regs里面,也即记住原来用户态是运行到了line A的地方。
|
||||
1. 在内核中执行系统调用读取数据。
|
||||
1. 当发现没有什么数据可读取的时候,只好进入睡眠状态,并且调用schedule让出CPU,这是进程调度第一定律。
|
||||
1. 将进程状态设置为TASK_INTERRUPTIBLE,可中断的睡眠状态,也即如果有信号来的话,是可以唤醒它的。
|
||||
1. 其他的进程或者shell发送一个信号,有四个函数可以调用kill、tkill、tgkill、rt_sigqueueinfo。
|
||||
1. 四个发送信号的函数,在内核中最终都是调用do_send_sig_info。
|
||||
1. do_send_sig_info调用send_signal给进程A发送一个信号,其实就是找到进程A的task_struct,或者加入信号集合,为不可靠信号,或者加入信号链表,为可靠信号。
|
||||
1. do_send_sig_info调用signal_wake_up唤醒进程A。
|
||||
1. 进程A重新进入运行状态TASK_RUNNING,根据进程调度第一定律,一定会接着schedule运行。
|
||||
1. 进程A被唤醒后,检查是否有信号到来,如果没有,重新循环到一开始,尝试再次读取数据,如果还是没有数据,再次进入TASK_INTERRUPTIBLE,即可中断的睡眠状态。
|
||||
1. 当发现有信号到来的时候,就返回当前正在执行的系统调用,并返回一个错误表示系统调用被中断了。
|
||||
1. 系统调用返回的时候,会调用exit_to_usermode_loop。这是一个处理信号的时机。
|
||||
1. 调用do_signal开始处理信号。
|
||||
1. 根据信号,得到信号处理函数sa_handler,然后修改pt_regs中的用户态栈的信息,让pt_regs指向sa_handler。同时修改用户态的栈,插入一个栈帧sa_restorer,里面保存了原来的指向line A的pt_regs,并且设置让sa_handler运行完毕后,跳到sa_restorer运行。
|
||||
1. 返回用户态,由于pt_regs已经设置为sa_handler,则返回用户态执行sa_handler。
|
||||
1. sa_handler执行完毕后,信号处理函数就执行完了,接着根据第15步对于用户态栈帧的修改,会跳到sa_restorer运行。
|
||||
1. sa_restorer会调用系统调用rt_sigreturn再次进入内核。
|
||||
1. 在内核中,rt_sigreturn恢复原来的pt_regs,重新指向line A。
|
||||
1. 从rt_sigreturn返回用户态,还是调用exit_to_usermode_loop。
|
||||
1. 这次因为pt_regs已经指向line A了,于是就到了进程A中,接着系统调用之后运行,当然这个系统调用返回的是它被中断了,没有执行完的错误。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3d/fb/3dcb3366b11a3594b00805896b7731fb.png" alt="">
|
||||
|
||||
## 课堂练习
|
||||
|
||||
在Linux内核里面,很多地方都存在信号和信号处理,所以signal_pending这个函数也随处可见,这样我们就能判断是否有信号发生。请你在内核代码中找到signal_pending出现的一些地方,看有什么规律,我们后面的章节会经常遇到它。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
|
||||
@@ -0,0 +1,482 @@
|
||||
<audio id="audio" title="39 | 管道:项目组A完成了,如何交接给项目组B?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9f/ca/9f9efe1fb4d988f2099ce48c8cd023ca.mp3"></audio>
|
||||
|
||||
在这一章的第一节里,我们大致讲了管道的使用方式以及相应的命令行。这一节,我们就具体来看一下管道是如何实现的。
|
||||
|
||||
我们先来看,我们常用的**匿名管道**(Anonymous Pipes),也即将多个命令串起来的竖线,背后的原理到底是什么。
|
||||
|
||||
上次我们说,它是基于管道的,那管道如何创建呢?管道的创建,需要通过下面这个系统调用。
|
||||
|
||||
```
|
||||
int pipe(int fd[2])
|
||||
|
||||
```
|
||||
|
||||
在这里,我们创建了一个管道pipe,返回了两个文件描述符,这表示管道的两端,一个是管道的读取端描述符fd[0],另一个是管道的写入端描述符fd[1]。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8f/a7/8fa3144bf3a34ddf789884a75fa2d4a7.png" alt="">
|
||||
|
||||
我们来看在内核里面是如何实现的。
|
||||
|
||||
```
|
||||
SYSCALL_DEFINE1(pipe, int __user *, fildes)
|
||||
{
|
||||
return sys_pipe2(fildes, 0);
|
||||
}
|
||||
|
||||
SYSCALL_DEFINE2(pipe2, int __user *, fildes, int, flags)
|
||||
{
|
||||
struct file *files[2];
|
||||
int fd[2];
|
||||
int error;
|
||||
|
||||
error = __do_pipe_flags(fd, files, flags);
|
||||
if (!error) {
|
||||
if (unlikely(copy_to_user(fildes, fd, sizeof(fd)))) {
|
||||
......
|
||||
error = -EFAULT;
|
||||
} else {
|
||||
fd_install(fd[0], files[0]);
|
||||
fd_install(fd[1], files[1]);
|
||||
}
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在内核中,主要的逻辑在pipe2系统调用中。这里面要创建一个数组files,用来存放管道的两端的打开文件,另一个数组fd存放管道的两端的文件描述符。如果调用__do_pipe_flags没有错误,那就调用fd_install,将两个fd和两个struct file关联起来。这一点和打开一个文件的过程很像了。
|
||||
|
||||
我们来看__do_pipe_flags。这里面调用了create_pipe_files,然后生成了两个fd。从这里可以看出,fd[0]是用于读的,fd[1]是用于写的。
|
||||
|
||||
```
|
||||
static int __do_pipe_flags(int *fd, struct file **files, int flags)
|
||||
{
|
||||
int error;
|
||||
int fdw, fdr;
|
||||
......
|
||||
error = create_pipe_files(files, flags);
|
||||
......
|
||||
error = get_unused_fd_flags(flags);
|
||||
......
|
||||
fdr = error;
|
||||
|
||||
error = get_unused_fd_flags(flags);
|
||||
......
|
||||
fdw = error;
|
||||
|
||||
fd[0] = fdr;
|
||||
fd[1] = fdw;
|
||||
return 0;
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
创建一个管道,大部分的逻辑其实都是在create_pipe_files函数里面实现的。这一章第一节的时候,我们说过,命名管道是创建在文件系统上的。从这里我们可以看出,匿名管道,也是创建在文件系统上的,只不过是一种特殊的文件系统,创建一个特殊的文件,对应一个特殊的inode,就是这里面的get_pipe_inode。
|
||||
|
||||
```
|
||||
int create_pipe_files(struct file **res, int flags)
|
||||
{
|
||||
int err;
|
||||
struct inode *inode = get_pipe_inode();
|
||||
struct file *f;
|
||||
struct path path;
|
||||
......
|
||||
path.dentry = d_alloc_pseudo(pipe_mnt->mnt_sb, &empty_name);
|
||||
......
|
||||
path.mnt = mntget(pipe_mnt);
|
||||
|
||||
d_instantiate(path.dentry, inode);
|
||||
|
||||
f = alloc_file(&path, FMODE_WRITE, &pipefifo_fops);
|
||||
......
|
||||
f->f_flags = O_WRONLY | (flags & (O_NONBLOCK | O_DIRECT));
|
||||
f->private_data = inode->i_pipe;
|
||||
|
||||
res[0] = alloc_file(&path, FMODE_READ, &pipefifo_fops);
|
||||
......
|
||||
path_get(&path);
|
||||
res[0]->private_data = inode->i_pipe;
|
||||
res[0]->f_flags = O_RDONLY | (flags & O_NONBLOCK);
|
||||
res[1] = f;
|
||||
return 0;
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从get_pipe_inode的实现,我们可以看出,匿名管道来自一个特殊的文件系统pipefs。这个文件系统被挂载后,我们就得到了struct vfsmount *pipe_mnt。然后挂载的文件系统的superblock就变成了:pipe_mnt->mnt_sb。如果你对文件系统的操作还不熟悉,要返回去复习一下文件系统那一章啊。
|
||||
|
||||
```
|
||||
static struct file_system_type pipe_fs_type = {
|
||||
.name = "pipefs",
|
||||
.mount = pipefs_mount,
|
||||
.kill_sb = kill_anon_super,
|
||||
};
|
||||
|
||||
static int __init init_pipe_fs(void)
|
||||
{
|
||||
int err = register_filesystem(&pipe_fs_type);
|
||||
|
||||
if (!err) {
|
||||
pipe_mnt = kern_mount(&pipe_fs_type);
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
static struct inode * get_pipe_inode(void)
|
||||
{
|
||||
struct inode *inode = new_inode_pseudo(pipe_mnt->mnt_sb);
|
||||
struct pipe_inode_info *pipe;
|
||||
......
|
||||
inode->i_ino = get_next_ino();
|
||||
|
||||
pipe = alloc_pipe_info();
|
||||
......
|
||||
inode->i_pipe = pipe;
|
||||
pipe->files = 2;
|
||||
pipe->readers = pipe->writers = 1;
|
||||
inode->i_fop = &pipefifo_fops;
|
||||
inode->i_state = I_DIRTY;
|
||||
inode->i_mode = S_IFIFO | S_IRUSR | S_IWUSR;
|
||||
inode->i_uid = current_fsuid();
|
||||
inode->i_gid = current_fsgid();
|
||||
inode->i_atime = inode->i_mtime = inode->i_ctime = current_time(inode);
|
||||
|
||||
return inode;
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们从new_inode_pseudo函数创建一个inode。这里面开始填写Inode的成员,这里和文件系统的很像。这里值得注意的是struct pipe_inode_info,这个结构里面有个成员是struct pipe_buffer *bufs。我们可以知道,**所谓的匿名管道,其实就是内核里面的一串缓存**。
|
||||
|
||||
另外一个需要注意的是pipefifo_fops,将来我们对于文件描述符的操作,在内核里面都是对应这里面的操作。
|
||||
|
||||
```
|
||||
const struct file_operations pipefifo_fops = {
|
||||
.open = fifo_open,
|
||||
.llseek = no_llseek,
|
||||
.read_iter = pipe_read,
|
||||
.write_iter = pipe_write,
|
||||
.poll = pipe_poll,
|
||||
.unlocked_ioctl = pipe_ioctl,
|
||||
.release = pipe_release,
|
||||
.fasync = pipe_fasync,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
我们回到create_pipe_files函数,创建完了inode,还需创建一个dentry和他对应。dentry和inode对应好了,我们就要开始创建struct file对象了。先创建用于写入的,对应的操作为pipefifo_fops;再创建读取的,对应的操作也为pipefifo_fops。然后把private_data设置为pipe_inode_info。这样从struct file这个层级上,就能直接操作底层的读写操作。
|
||||
|
||||
至此,一个匿名管道就创建成功了。如果对于fd[1]写入,调用的是pipe_write,向pipe_buffer里面写入数据;如果对于fd[0]的读入,调用的是pipe_read,也就是从pipe_buffer里面读取数据。
|
||||
|
||||
但是这个时候,两个文件描述符都是在一个进程里面的,并没有起到进程间通信的作用,怎么样才能使得管道是跨两个进程的呢?还记得创建进程调用的fork吗?在这里面,创建的子进程会复制父进程的struct files_struct,在这里面fd的数组会复制一份,但是fd指向的struct file对于同一个文件还是只有一份,这样就做到了,两个进程各有两个fd指向同一个struct file的模式,两个进程就可以通过各自的fd写入和读取同一个管道文件实现跨进程通信了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9c/a3/9c0e38e31c7a51da12faf4a1aca10ba3.png" alt="">
|
||||
|
||||
由于管道只能一端写入,另一端读出,所以上面的这种模式会造成混乱,因为父进程和子进程都可以写入,也都可以读出,通常的方法是父进程关闭读取的fd,只保留写入的fd,而子进程关闭写入的fd,只保留读取的fd,如果需要双向通行,则应该创建两个管道。
|
||||
|
||||
一个典型的使用管道在父子进程之间的通信代码如下:
|
||||
|
||||
```
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
int fds[2];
|
||||
if (pipe(fds) == -1)
|
||||
perror("pipe error");
|
||||
|
||||
pid_t pid;
|
||||
pid = fork();
|
||||
if (pid == -1)
|
||||
perror("fork error");
|
||||
|
||||
if (pid == 0){
|
||||
close(fds[0]);
|
||||
char msg[] = "hello world";
|
||||
write(fds[1], msg, strlen(msg) + 1);
|
||||
close(fds[1]);
|
||||
exit(0);
|
||||
} else {
|
||||
close(fds[1]);
|
||||
char msg[128];
|
||||
read(fds[0], msg, 128);
|
||||
close(fds[0]);
|
||||
printf("message : %s\n", msg);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/71/b6/71eb7b4d026d04e4093daad7e24feab6.png" alt="">
|
||||
|
||||
到这里,我们仅仅解析了使用管道进行父子进程之间的通信,但是我们在shell里面的不是这样的。在shell里面运行A|B的时候,A进程和B进程都是shell创建出来的子进程,A和B之间不存在父子关系。
|
||||
|
||||
不过,有了上面父子进程之间的管道这个基础,实现A和B之间的管道就方便多了。
|
||||
|
||||
我们首先从shell创建子进程A,然后在shell和A之间建立一个管道,其中shell保留读取端,A进程保留写入端,然后shell再创建子进程B。这又是一次fork,所以,shell里面保留的读取端的fd也被复制到了子进程B里面。这个时候,相当于shell和B都保留读取端,只要shell主动关闭读取端,就变成了一管道,写入端在A进程,读取端在B进程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/81/fa/81be4d460aaa804e9176ec70d59fdefa.png" alt="">
|
||||
|
||||
接下来我们要做的事情就是,将这个管道的两端和输入输出关联起来。这就要用到dup2系统调用了。
|
||||
|
||||
```
|
||||
int dup2(int oldfd, int newfd);
|
||||
|
||||
```
|
||||
|
||||
这个系统调用,将老的文件描述符赋值给新的文件描述符,让newfd的值和oldfd一样。
|
||||
|
||||
我们还是回忆一下,在files_struct里面,有这样一个表,下标是fd,内容指向一个打开的文件struct file。
|
||||
|
||||
```
|
||||
struct files_struct {
|
||||
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这个表里面,前三项是定下来的,其中第零项STDIN_FILENO表示标准输入,第一项STDOUT_FILENO表示标准输出,第三项STDERR_FILENO表示错误输出。
|
||||
|
||||
在A进程中,写入端可以做这样的操作:dup2(fd[1],STDOUT_FILENO),将STDOUT_FILENO(也即第一项)不再指向标准输出,而是指向创建的管道文件,那么以后往标准输出写入的任何东西,都会写入管道文件。
|
||||
|
||||
在B进程中,读取端可以做这样的操作,dup2(fd[0],STDIN_FILENO),将STDIN_FILENO也即第零项不再指向标准输入,而是指向创建的管道文件,那么以后从标准输入读取的任何东西,都来自于管道文件。
|
||||
|
||||
至此,我们才将A|B的功能完成。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c0/e2/c042b12de704995e4ba04173e0a304e2.png" alt="">
|
||||
|
||||
为了模拟A|B的情况,我们可以将前面的那一段代码,进一步修改成下面这样:
|
||||
|
||||
```
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
int fds[2];
|
||||
if (pipe(fds) == -1)
|
||||
perror("pipe error");
|
||||
|
||||
pid_t pid;
|
||||
pid = fork();
|
||||
if (pid == -1)
|
||||
perror("fork error");
|
||||
|
||||
if (pid == 0){
|
||||
dup2(fds[1], STDOUT_FILENO);
|
||||
close(fds[1]);
|
||||
close(fds[0]);
|
||||
execlp("ps", "ps", "-ef", NULL);
|
||||
} else {
|
||||
dup2(fds[0], STDIN_FILENO);
|
||||
close(fds[0]);
|
||||
close(fds[1]);
|
||||
execlp("grep", "grep", "systemd", NULL);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接下来,我们来看命名管道。我们在讲命令的时候讲过,命名管道需要事先通过命令mkfifo,进行创建。如果是通过代码创建命名管道,也有一个函数,但是这不是一个系统调用,而是Glibc提供的函数。它的定义如下:
|
||||
|
||||
```
|
||||
int
|
||||
mkfifo (const char *path, mode_t mode)
|
||||
{
|
||||
dev_t dev = 0;
|
||||
return __xmknod (_MKNOD_VER, path, mode | S_IFIFO, &dev);
|
||||
}
|
||||
|
||||
int
|
||||
__xmknod (int vers, const char *path, mode_t mode, dev_t *dev)
|
||||
{
|
||||
unsigned long long int k_dev;
|
||||
......
|
||||
/* We must convert the value to dev_t type used by the kernel. */
|
||||
k_dev = (*dev) & ((1ULL << 32) - 1);
|
||||
......
|
||||
return INLINE_SYSCALL (mknodat, 4, AT_FDCWD, path, mode,
|
||||
(unsigned int) k_dev);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Glibc的mkfifo函数会调用mknodat系统调用,还记得咱们学字符设备的时候,创建一个字符设备的时候,也是调用的mknod。这里命名管道也是一个设备,因而我们也用mknod。
|
||||
|
||||
```
|
||||
SYSCALL_DEFINE4(mknodat, int, dfd, const char __user *, filename, umode_t, mode, unsigned, dev)
|
||||
{
|
||||
struct dentry *dentry;
|
||||
struct path path;
|
||||
unsigned int lookup_flags = 0;
|
||||
......
|
||||
retry:
|
||||
dentry = user_path_create(dfd, filename, &path, lookup_flags);
|
||||
......
|
||||
switch (mode & S_IFMT) {
|
||||
......
|
||||
case S_IFIFO: case S_IFSOCK:
|
||||
error = vfs_mknod(path.dentry->d_inode,dentry,mode,0);
|
||||
break;
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
对于mknod的解析,我们在字符设备那一节已经解析过了,先是通过user_path_create对于这个管道文件创建一个dentry,然后因为是S_IFIFO,所以调用vfs_mknod。由于这个管道文件是创建在一个普通文件系统上的,假设是在ext4文件上,于是vfs_mknod会调用ext4_dir_inode_operations的mknod,也即会调用ext4_mknod。
|
||||
|
||||
```
|
||||
const struct inode_operations ext4_dir_inode_operations = {
|
||||
......
|
||||
.mknod = ext4_mknod,
|
||||
......
|
||||
};
|
||||
|
||||
static int ext4_mknod(struct inode *dir, struct dentry *dentry,
|
||||
umode_t mode, dev_t rdev)
|
||||
{
|
||||
handle_t *handle;
|
||||
struct inode *inode;
|
||||
......
|
||||
inode = ext4_new_inode_start_handle(dir, mode, &dentry->d_name, 0,
|
||||
NULL, EXT4_HT_DIR, credits);
|
||||
handle = ext4_journal_current_handle();
|
||||
if (!IS_ERR(inode)) {
|
||||
init_special_inode(inode, inode->i_mode, rdev);
|
||||
inode->i_op = &ext4_special_inode_operations;
|
||||
err = ext4_add_nondir(handle, dentry, inode);
|
||||
if (!err && IS_DIRSYNC(dir))
|
||||
ext4_handle_sync(handle);
|
||||
}
|
||||
if (handle)
|
||||
ext4_journal_stop(handle);
|
||||
......
|
||||
}
|
||||
|
||||
#define ext4_new_inode_start_handle(dir, mode, qstr, goal, owner, \
|
||||
type, nblocks) \
|
||||
__ext4_new_inode(NULL, (dir), (mode), (qstr), (goal), (owner), \
|
||||
0, (type), __LINE__, (nblocks))
|
||||
|
||||
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
|
||||
{
|
||||
inode->i_mode = mode;
|
||||
if (S_ISCHR(mode)) {
|
||||
inode->i_fop = &def_chr_fops;
|
||||
inode->i_rdev = rdev;
|
||||
} else if (S_ISBLK(mode)) {
|
||||
inode->i_fop = &def_blk_fops;
|
||||
inode->i_rdev = rdev;
|
||||
} else if (S_ISFIFO(mode))
|
||||
inode->i_fop = &pipefifo_fops;
|
||||
else if (S_ISSOCK(mode))
|
||||
; /* leave it no_open_fops */
|
||||
else
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在ext4_mknod中,ext4_new_inode_start_handle会调用__ext4_new_inode,在ext4文件系统上真的创建一个文件,但是会调用init_special_inode,创建一个内存中特殊的inode,这个函数我们在字符设备文件中也遇到过,只不过当时inode的i_fop指向的是def_chr_fops,这次换成管道文件了,inode的i_fop变成指向pipefifo_fops,这一点和匿名管道是一样的。
|
||||
|
||||
这样,管道文件就创建完毕了。
|
||||
|
||||
接下来,要打开这个管道文件,我们还是会调用文件系统的open函数。还是沿着文件系统的调用方式,一路调用到pipefifo_fops的open函数,也就是fifo_open。
|
||||
|
||||
```
|
||||
static int fifo_open(struct inode *inode, struct file *filp)
|
||||
{
|
||||
struct pipe_inode_info *pipe;
|
||||
bool is_pipe = inode->i_sb->s_magic == PIPEFS_MAGIC;
|
||||
int ret;
|
||||
filp->f_version = 0;
|
||||
|
||||
if (inode->i_pipe) {
|
||||
pipe = inode->i_pipe;
|
||||
pipe->files++;
|
||||
} else {
|
||||
pipe = alloc_pipe_info();
|
||||
pipe->files = 1;
|
||||
inode->i_pipe = pipe;
|
||||
spin_unlock(&inode->i_lock);
|
||||
}
|
||||
filp->private_data = pipe;
|
||||
filp->f_mode &= (FMODE_READ | FMODE_WRITE);
|
||||
|
||||
switch (filp->f_mode) {
|
||||
case FMODE_READ:
|
||||
pipe->r_counter++;
|
||||
if (pipe->readers++ == 0)
|
||||
wake_up_partner(pipe);
|
||||
if (!is_pipe && !pipe->writers) {
|
||||
if ((filp->f_flags & O_NONBLOCK)) {
|
||||
filp->f_version = pipe->w_counter;
|
||||
} else {
|
||||
if (wait_for_partner(pipe, &pipe->w_counter))
|
||||
goto err_rd;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FMODE_WRITE:
|
||||
pipe->w_counter++;
|
||||
if (!pipe->writers++)
|
||||
wake_up_partner(pipe);
|
||||
if (!is_pipe && !pipe->readers) {
|
||||
if (wait_for_partner(pipe, &pipe->r_counter))
|
||||
goto err_wr;
|
||||
}
|
||||
break;
|
||||
case FMODE_READ | FMODE_WRITE:
|
||||
pipe->readers++;
|
||||
pipe->writers++;
|
||||
pipe->r_counter++;
|
||||
pipe->w_counter++;
|
||||
if (pipe->readers == 1 || pipe->writers == 1)
|
||||
wake_up_partner(pipe);
|
||||
break;
|
||||
......
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在fifo_open里面,创建pipe_inode_info,这一点和匿名管道也是一样的。这个结构里面有个成员是struct pipe_buffer *bufs。我们可以知道,**所谓的命名管道,其实是也是内核里面的一串缓存。**
|
||||
|
||||
接下来,对于命名管道的写入,我们还是会调用pipefifo_fops的pipe_write函数,向pipe_buffer里面写入数据。对于命名管道的读入,我们还是会调用pipefifo_fops的pipe_read,也就是从pipe_buffer里面读取数据。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
无论是匿名管道,还是命名管道,在内核都是一个文件。只要是文件就要有一个inode。这里我们又用到了特殊inode、字符设备、块设备,其实都是这种特殊的inode。
|
||||
|
||||
在这种特殊的inode里面,file_operations指向管道特殊的pipefifo_fops,这个inode对应内存里面的缓存。
|
||||
|
||||
当我们用文件的open函数打开这个管道设备文件的时候,会调用pipefifo_fops里面的方法创建struct file结构,他的inode指向特殊的inode,也对应内存里面的缓存,file_operations也指向管道特殊的pipefifo_fops。
|
||||
|
||||
写入一个pipe就是从struct file结构找到缓存写入,读取一个pipe就是从struct file结构找到缓存读出。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/48/97/486e2bc73abbe91d7083bb1f4f678097.png" alt="">
|
||||
|
||||
## 课堂练习
|
||||
|
||||
上面创建匿名管道的程序,你一定要运行一下,然后试着通过strace查看自己写的程序的系统调用,以及直接在命令行使用匿名管道的系统调用,做一个比较。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
|
||||
@@ -0,0 +1,297 @@
|
||||
<audio id="audio" title="40 | IPC(上):不同项目组之间抢资源,如何协调?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/52/52/5292ca2ad2cdc2f2d54796e5c3d55b52.mp3"></audio>
|
||||
|
||||
我们前面讲了,如果项目组之间需要紧密合作,那就需要共享内存,这样就像把两个项目组放在一个会议室一起沟通,会非常高效。这一节,我们就来详细讲讲这个进程之间共享内存的机制。
|
||||
|
||||
有了这个机制,两个进程可以像访问自己内存中的变量一样,访问共享内存的变量。但是同时问题也来了,当两个进程共享内存了,就会存在同时读写的问题,就需要对于共享的内存进行保护,就需要信号量这样的同步协调机制。这些也都是我们这节需要探讨的问题。下面我们就一一来看。
|
||||
|
||||
共享内存和信号量也是System V系列的进程间通信机制,所以很多地方和我们讲过的消息队列有点儿像。为了将共享内存和信号量结合起来使用,我这里定义了一个share.h头文件,里面放了一些共享内存和信号量在每个进程都需要的函数。
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/ipc.h>
|
||||
#include <sys/shm.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/sem.h>
|
||||
#include <string.h>
|
||||
|
||||
#define MAX_NUM 128
|
||||
|
||||
struct shm_data {
|
||||
int data[MAX_NUM];
|
||||
int datalength;
|
||||
};
|
||||
|
||||
union semun {
|
||||
int val;
|
||||
struct semid_ds *buf;
|
||||
unsigned short int *array;
|
||||
struct seminfo *__buf;
|
||||
};
|
||||
|
||||
int get_shmid(){
|
||||
int shmid;
|
||||
key_t key;
|
||||
|
||||
if((key = ftok("/root/sharememory/sharememorykey", 1024)) < 0){
|
||||
perror("ftok error");
|
||||
return -1;
|
||||
}
|
||||
|
||||
shmid = shmget(key, sizeof(struct shm_data), IPC_CREAT|0777);
|
||||
return shmid;
|
||||
}
|
||||
|
||||
int get_semaphoreid(){
|
||||
int semid;
|
||||
key_t key;
|
||||
|
||||
if((key = ftok("/root/sharememory/semaphorekey", 1024)) < 0){
|
||||
perror("ftok error");
|
||||
return -1;
|
||||
}
|
||||
|
||||
semid = semget(key, 1, IPC_CREAT|0777);
|
||||
return semid;
|
||||
}
|
||||
|
||||
int semaphore_init (int semid) {
|
||||
union semun argument;
|
||||
unsigned short values[1];
|
||||
values[0] = 1;
|
||||
argument.array = values;
|
||||
return semctl (semid, 0, SETALL, argument);
|
||||
}
|
||||
|
||||
int semaphore_p (int semid) {
|
||||
struct sembuf operations[1];
|
||||
operations[0].sem_num = 0;
|
||||
operations[0].sem_op = -1;
|
||||
operations[0].sem_flg = SEM_UNDO;
|
||||
return semop (semid, operations, 1);
|
||||
}
|
||||
|
||||
int semaphore_v (int semid) {
|
||||
struct sembuf operations[1];
|
||||
operations[0].sem_num = 0;
|
||||
operations[0].sem_op = 1;
|
||||
operations[0].sem_flg = SEM_UNDO;
|
||||
return semop (semid, operations, 1);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 共享内存
|
||||
|
||||
我们先来看里面对于共享内存的操作。
|
||||
|
||||
首先,创建之前,我们要有一个key来唯一标识这个共享内存。这个key可以根据文件系统上的一个文件的inode随机生成。
|
||||
|
||||
然后,我们需要创建一个共享内存,就像创建一个消息队列差不多,都是使用xxxget来创建。其中,创建共享内存使用的是下面这个函数:
|
||||
|
||||
```
|
||||
int shmget(key_t key, size_t size, int shmflag);
|
||||
|
||||
```
|
||||
|
||||
其中,key就是前面生成的那个key,shmflag如果为IPC_CREAT,就表示新创建,还可以指定读写权限0777。
|
||||
|
||||
对于共享内存,需要指定一个大小size,这个一般要申请多大呢?一个最佳实践是,我们将多个进程需要共享的数据放在一个struct里面,然后这里的size就应该是这个struct的大小。这样每一个进程得到这块内存后,只要强制将类型转换为这个struct类型,就能够访问里面的共享数据了。
|
||||
|
||||
在这里,我们定义了一个struct shm_data结构。这里面有两个成员,一个是一个整型的数组,一个是数组中元素的个数。
|
||||
|
||||
生成了共享内存以后,接下来就是将这个共享内存映射到进程的虚拟地址空间中。我们使用下面这个函数来进行操作。
|
||||
|
||||
```
|
||||
void *shmat(int shm_id, const void *addr, int shmflg);
|
||||
|
||||
```
|
||||
|
||||
这里面的shm_id,就是上面创建的共享内存的id,addr就是指定映射在某个地方。如果不指定,则内核会自动选择一个地址,作为返回值返回。得到了返回地址以后,我们需要将指针强制类型转换为struct shm_data结构,就可以使用这个指针设置data和datalength了。
|
||||
|
||||
当共享内存使用完毕,我们可以通过shmdt解除它到虚拟内存的映射。
|
||||
|
||||
```
|
||||
int shmdt(const void *shmaddr);
|
||||
|
||||
```
|
||||
|
||||
## 信号量
|
||||
|
||||
看完了共享内存,接下来我们再来看信号量。信号量以集合的形式存在的。
|
||||
|
||||
首先,创建之前,我们同样需要有一个key,来唯一标识这个信号量集合。这个key同样可以根据文件系统上的一个文件的inode随机生成。
|
||||
|
||||
然后,我们需要创建一个信号量集合,同样也是使用xxxget来创建,其中创建信号量集合使用的是下面这个函数。
|
||||
|
||||
```
|
||||
int semget(key_t key, int nsems, int semflg);
|
||||
|
||||
```
|
||||
|
||||
这里面的key,就是前面生成的那个key,shmflag如果为IPC_CREAT,就表示新创建,还可以指定读写权限0777。
|
||||
|
||||
这里,nsems表示这个信号量集合里面有几个信号量,最简单的情况下,我们设置为1。
|
||||
|
||||
信号量往往代表某种资源的数量,如果用信号量做互斥,那往往将信号量设置为1。这就是上面代码中semaphore_init函数的作用,这里面调用semctl函数,将这个信号量集合的中的第0个信号量,也即唯一的这个信号量设置为1。
|
||||
|
||||
对于信号量,往往要定义两种操作,P操作和V操作。对应上面代码中semaphore_p函数和semaphore_v函数,semaphore_p会调用semop函数将信号量的值减一,表示申请占用一个资源,当发现当前没有资源的时候,进入等待。semaphore_v会调用semop函数将信号量的值加一,表示释放一个资源,释放之后,就允许等待中的其他进程占用这个资源。
|
||||
|
||||
我们可以用这个信号量,来保护共享内存中的struct shm_data,使得同时只有一个进程可以操作这个结构。
|
||||
|
||||
你是否记得咱们讲线程同步机制的时候,构建了一个老板分配活的场景。这里我们同样构建一个场景,分为producer.c和consumer.c,其中producer也即生产者,负责往struct shm_data塞入数据,而consumer.c负责处理struct shm_data中的数据。
|
||||
|
||||
下面我们来看producer.c的代码。
|
||||
|
||||
```
|
||||
#include "share.h"
|
||||
|
||||
int main() {
|
||||
void *shm = NULL;
|
||||
struct shm_data *shared = NULL;
|
||||
int shmid = get_shmid();
|
||||
int semid = get_semaphoreid();
|
||||
int i;
|
||||
|
||||
shm = shmat(shmid, (void*)0, 0);
|
||||
if(shm == (void*)-1){
|
||||
exit(0);
|
||||
}
|
||||
shared = (struct shm_data*)shm;
|
||||
memset(shared, 0, sizeof(struct shm_data));
|
||||
semaphore_init(semid);
|
||||
while(1){
|
||||
semaphore_p(semid);
|
||||
if(shared->datalength > 0){
|
||||
semaphore_v(semid);
|
||||
sleep(1);
|
||||
} else {
|
||||
printf("how many integers to caculate : ");
|
||||
scanf("%d",&shared->datalength);
|
||||
if(shared->datalength > MAX_NUM){
|
||||
perror("too many integers.");
|
||||
shared->datalength = 0;
|
||||
semaphore_v(semid);
|
||||
exit(1);
|
||||
}
|
||||
for(i=0;i<shared->datalength;i++){
|
||||
printf("Input the %d integer : ", i);
|
||||
scanf("%d",&shared->data[i]);
|
||||
}
|
||||
semaphore_v(semid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这里面,get_shmid创建了共享内存,get_semaphoreid创建了信号量集合,然后shmat将共享内存映射到了虚拟地址空间的shm指针指向的位置,然后通过强制类型转换,shared的指针指向放在共享内存里面的struct shm_data结构,然后初始化为0。semaphore_init将信号量进行了初始化。
|
||||
|
||||
接着,producer进入了一个无限循环。在这个循环里面,我们先通过semaphore_p申请访问共享内存的权利,如果发现datalength大于零,说明共享内存里面的数据没有被处理过,于是semaphore_v释放权利,先睡一会儿,睡醒了再看。如果发现datalength等于0,说明共享内存里面的数据被处理完了,于是开始往里面放数据。让用户输入多少个数,然后每个数是什么,都放在struct shm_data结构中,然后semaphore_v释放权利,等待其他的进程将这些数拿去处理。
|
||||
|
||||
我们再来看consumer的代码。
|
||||
|
||||
```
|
||||
#include "share.h"
|
||||
|
||||
int main() {
|
||||
void *shm = NULL;
|
||||
struct shm_data *shared = NULL;
|
||||
int shmid = get_shmid();
|
||||
int semid = get_semaphoreid();
|
||||
int i;
|
||||
|
||||
shm = shmat(shmid, (void*)0, 0);
|
||||
if(shm == (void*)-1){
|
||||
exit(0);
|
||||
}
|
||||
shared = (struct shm_data*)shm;
|
||||
while(1){
|
||||
semaphore_p(semid);
|
||||
if(shared->datalength > 0){
|
||||
int sum = 0;
|
||||
for(i=0;i<shared->datalength-1;i++){
|
||||
printf("%d+",shared->data[i]);
|
||||
sum += shared->data[i];
|
||||
}
|
||||
printf("%d",shared->data[shared->datalength-1]);
|
||||
sum += shared->data[shared->datalength-1];
|
||||
printf("=%d\n",sum);
|
||||
memset(shared, 0, sizeof(struct shm_data));
|
||||
semaphore_v(semid);
|
||||
} else {
|
||||
semaphore_v(semid);
|
||||
printf("no tasks, waiting.\n");
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这里面,get_shmid获得producer创建的共享内存,get_semaphoreid获得producer创建的信号量集合,然后shmat将共享内存映射到了虚拟地址空间的shm指针指向的位置,然后通过强制类型转换,shared的指针指向放在共享内存里面的struct shm_data结构。
|
||||
|
||||
接着,consumer进入了一个无限循环,在这个循环里面,我们先通过semaphore_p申请访问共享内存的权利,如果发现datalength等于0,就说明没什么活干,需要等待。如果发现datalength大于0,就说明有活干,于是将datalength个整型数字从data数组中取出来求和。最后将struct shm_data清空为0,表示任务处理完毕,通过semaphore_v释放权利。
|
||||
|
||||
通过程序创建的共享内存和信号量集合,我们可以通过命令ipcs查看。当然,我们也可以通过ipcrm进行删除。
|
||||
|
||||
```
|
||||
# ipcs
|
||||
------ Message Queues --------
|
||||
key msqid owner perms used-bytes messages
|
||||
------ Shared Memory Segments --------
|
||||
key shmid owner perms bytes nattch status
|
||||
0x00016988 32768 root 777 516 0
|
||||
------ Semaphore Arrays --------
|
||||
key semid owner perms nsems
|
||||
0x00016989 32768 root 777 1
|
||||
|
||||
```
|
||||
|
||||
下面我们来运行一下producer和consumer,可以得到下面的结果:
|
||||
|
||||
```
|
||||
# ./producer
|
||||
how many integers to caculate : 2
|
||||
Input the 0 integer : 3
|
||||
Input the 1 integer : 4
|
||||
how many integers to caculate : 4
|
||||
Input the 0 integer : 3
|
||||
Input the 1 integer : 4
|
||||
Input the 2 integer : 5
|
||||
Input the 3 integer : 6
|
||||
how many integers to caculate : 7
|
||||
Input the 0 integer : 9
|
||||
Input the 1 integer : 8
|
||||
Input the 2 integer : 7
|
||||
Input the 3 integer : 6
|
||||
Input the 4 integer : 5
|
||||
Input the 5 integer : 4
|
||||
Input the 6 integer : 3
|
||||
|
||||
# ./consumer
|
||||
3+4=7
|
||||
3+4+5+6=18
|
||||
9+8+7+6+5+4+3=42
|
||||
|
||||
```
|
||||
|
||||
## 总结时刻
|
||||
|
||||
这一节的内容差不多了,我们来总结一下。共享内存和信号量的配合机制,如下图所示:
|
||||
|
||||
- 无论是共享内存还是信号量,创建与初始化都遵循同样流程,通过ftok得到key,通过xxxget创建对象并生成id;
|
||||
- 生产者和消费者都通过shmat将共享内存映射到各自的内存空间,在不同的进程里面映射的位置不同;
|
||||
- 为了访问共享内存,需要信号量进行保护,信号量需要通过semctl初始化为某个值;
|
||||
- 接下来生产者和消费者要通过semop(-1)来竞争信号量,如果生产者抢到信号量则写入,然后通过semop(+1)释放信号量,如果消费者抢到信号量则读出,然后通过semop(+1)释放信号量;
|
||||
- 共享内存使用完毕,可以通过shmdt来解除映射。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/0b/469552bffe601d594c432d4fad97490b.png" alt="">
|
||||
|
||||
## 课堂练习
|
||||
|
||||
信号量大于1的情况下,应该如何使用?你可以试着构建一个场景。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
|
||||
@@ -0,0 +1,576 @@
|
||||
<audio id="audio" title="41 | IPC(中):不同项目组之间抢资源,如何协调?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5e/d6/5e5caec562b544ec17f47848e115cdd6.mp3"></audio>
|
||||
|
||||
了解了如何使用共享内存和信号量集合之后,今天我们来解析一下,内核里面都做了什么。
|
||||
|
||||
不知道你有没有注意到,咱们讲消息队列、共享内存、信号量的机制的时候,我们其实能够从中看到一些统一的规律:**它们在使用之前都要生成key,然后通过key得到唯一的id,并且都是通过xxxget函数。**
|
||||
|
||||
在内核里面,这三种进程间通信机制是使用统一的机制管理起来的,都叫ipcxxx。
|
||||
|
||||
为了维护这三种进程间通信进制,在内核里面,我们声明了一个有三项的数组。
|
||||
|
||||
我们通过这段代码,来具体看一看。
|
||||
|
||||
```
|
||||
struct ipc_namespace {
|
||||
......
|
||||
struct ipc_ids ids[3];
|
||||
......
|
||||
}
|
||||
|
||||
#define IPC_SEM_IDS 0
|
||||
#define IPC_MSG_IDS 1
|
||||
#define IPC_SHM_IDS 2
|
||||
|
||||
#define sem_ids(ns) ((ns)->ids[IPC_SEM_IDS])
|
||||
#define msg_ids(ns) ((ns)->ids[IPC_MSG_IDS])
|
||||
#define shm_ids(ns) ((ns)->ids[IPC_SHM_IDS])
|
||||
|
||||
```
|
||||
|
||||
根据代码中的定义,第0项用于信号量,第1项用于消息队列,第2项用于共享内存,分别可以通过sem_ids、msg_ids、shm_ids来访问。
|
||||
|
||||
这段代码里面有ns,全称叫namespace。可能不容易理解,你现在可以将它认为是将一台Linux服务器逻辑的隔离为多台Linux服务器的机制,它背后的原理是一个相当大的话题,我们需要在容器那一章详细讲述。现在,你就可以简单的认为没有namespace,整个Linux在一个namespace下面,那这些ids也是整个Linux只有一份。
|
||||
|
||||
接下来,我们再来看struct ipc_ids里面保存了什么。
|
||||
|
||||
首先,in_use表示当前有多少个ipc;其次,seq和next_id用于一起生成ipc唯一的id,因为信号量,共享内存,消息队列,它们三个的id也不能重复;ipcs_idr是一棵基数树,我们又碰到它了,一旦涉及从一个整数查找一个对象,它都是最好的选择。
|
||||
|
||||
```
|
||||
struct ipc_ids {
|
||||
int in_use;
|
||||
unsigned short seq;
|
||||
struct rw_semaphore rwsem;
|
||||
struct idr ipcs_idr;
|
||||
int next_id;
|
||||
};
|
||||
|
||||
struct idr {
|
||||
struct radix_tree_root idr_rt;
|
||||
unsigned int idr_next;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
也就是说,对于sem_ids、msg_ids、shm_ids各有一棵基数树。那这棵树里面究竟存放了什么,能够统一管理这三类ipc对象呢?
|
||||
|
||||
通过下面这个函数ipc_obtain_object_idr,我们可以看出端倪。这个函数根据id,在基数树里面找出来的是struct kern_ipc_perm。
|
||||
|
||||
```
|
||||
struct kern_ipc_perm *ipc_obtain_object_idr(struct ipc_ids *ids, int id)
|
||||
{
|
||||
struct kern_ipc_perm *out;
|
||||
int lid = ipcid_to_idx(id);
|
||||
out = idr_find(&ids->ipcs_idr, lid);
|
||||
return out;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
如果我们看用于表示信号量、消息队列、共享内存的结构,就会发现,这三个结构的第一项都是struct kern_ipc_perm。
|
||||
|
||||
```
|
||||
struct sem_array {
|
||||
struct kern_ipc_perm sem_perm; /* permissions .. see ipc.h */
|
||||
time_t sem_ctime; /* create/last semctl() time */
|
||||
struct list_head pending_alter; /* pending operations */
|
||||
/* that alter the array */
|
||||
struct list_head pending_const; /* pending complex operations */
|
||||
/* that do not alter semvals */
|
||||
struct list_head list_id; /* undo requests on this array */
|
||||
int sem_nsems; /* no. of semaphores in array */
|
||||
int complex_count; /* pending complex operations */
|
||||
unsigned int use_global_lock;/* >0: global lock required */
|
||||
|
||||
struct sem sems[];
|
||||
} __randomize_layout;
|
||||
|
||||
struct msg_queue {
|
||||
struct kern_ipc_perm q_perm;
|
||||
time_t q_stime; /* last msgsnd time */
|
||||
time_t q_rtime; /* last msgrcv time */
|
||||
time_t q_ctime; /* last change time */
|
||||
unsigned long q_cbytes; /* current number of bytes on queue */
|
||||
unsigned long q_qnum; /* number of messages in queue */
|
||||
unsigned long q_qbytes; /* max number of bytes on queue */
|
||||
pid_t q_lspid; /* pid of last msgsnd */
|
||||
pid_t q_lrpid; /* last receive pid */
|
||||
|
||||
struct list_head q_messages;
|
||||
struct list_head q_receivers;
|
||||
struct list_head q_senders;
|
||||
} __randomize_layout;
|
||||
|
||||
struct shmid_kernel /* private to the kernel */
|
||||
{
|
||||
struct kern_ipc_perm shm_perm;
|
||||
struct file *shm_file;
|
||||
unsigned long shm_nattch;
|
||||
unsigned long shm_segsz;
|
||||
time_t shm_atim;
|
||||
time_t shm_dtim;
|
||||
time_t shm_ctim;
|
||||
pid_t shm_cprid;
|
||||
pid_t shm_lprid;
|
||||
struct user_struct *mlock_user;
|
||||
|
||||
/* The task created the shm object. NULL if the task is dead. */
|
||||
struct task_struct *shm_creator;
|
||||
struct list_head shm_clist; /* list by creator */
|
||||
} __randomize_layout;
|
||||
|
||||
```
|
||||
|
||||
也就是说,我们完全可以通过struct kern_ipc_perm的指针,通过进行强制类型转换后,得到整个结构。做这件事情的函数如下:
|
||||
|
||||
```
|
||||
static inline struct sem_array *sem_obtain_object(struct ipc_namespace *ns, int id)
|
||||
{
|
||||
struct kern_ipc_perm *ipcp = ipc_obtain_object_idr(&sem_ids(ns), id);
|
||||
return container_of(ipcp, struct sem_array, sem_perm);
|
||||
}
|
||||
|
||||
static inline struct msg_queue *msq_obtain_object(struct ipc_namespace *ns, int id)
|
||||
{
|
||||
struct kern_ipc_perm *ipcp = ipc_obtain_object_idr(&msg_ids(ns), id);
|
||||
return container_of(ipcp, struct msg_queue, q_perm);
|
||||
}
|
||||
|
||||
static inline struct shmid_kernel *shm_obtain_object(struct ipc_namespace *ns, int id)
|
||||
{
|
||||
struct kern_ipc_perm *ipcp = ipc_obtain_object_idr(&shm_ids(ns), id);
|
||||
return container_of(ipcp, struct shmid_kernel, shm_perm);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
通过这种机制,我们就可以将信号量、消息队列、共享内存抽象为ipc类型进行统一处理。你有没有觉得,这有点儿面向对象编程中抽象类和实现类的意思?没错,如果你试图去了解C++中类的实现机制,其实也是这么干的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/08/af/082b742753d862cfeae520fb02aa41af.png" alt="">
|
||||
|
||||
有了抽象类,接下来我们来看共享内存和信号量的具体实现。
|
||||
|
||||
## 如何创建共享内存?
|
||||
|
||||
首先,我们来看创建共享内存的的系统调用。
|
||||
|
||||
```
|
||||
SYSCALL_DEFINE3(shmget, key_t, key, size_t, size, int, shmflg)
|
||||
{
|
||||
struct ipc_namespace *ns;
|
||||
static const struct ipc_ops shm_ops = {
|
||||
.getnew = newseg,
|
||||
.associate = shm_security,
|
||||
.more_checks = shm_more_checks,
|
||||
};
|
||||
struct ipc_params shm_params;
|
||||
ns = current->nsproxy->ipc_ns;
|
||||
shm_params.key = key;
|
||||
shm_params.flg = shmflg;
|
||||
shm_params.u.size = size;
|
||||
return ipcget(ns, &shm_ids(ns), &shm_ops, &shm_params);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里面调用了抽象的ipcget、参数分别为共享内存对应的shm_ids、对应的操作shm_ops以及对应的参数shm_params。
|
||||
|
||||
如果key设置为IPC_PRIVATE则永远创建新的,如果不是的话,就会调用ipcget_public。ipcget的具体代码如下:
|
||||
|
||||
```
|
||||
int ipcget(struct ipc_namespace *ns, struct ipc_ids *ids,
|
||||
const struct ipc_ops *ops, struct ipc_params *params)
|
||||
{
|
||||
if (params->key == IPC_PRIVATE)
|
||||
return ipcget_new(ns, ids, ops, params);
|
||||
else
|
||||
return ipcget_public(ns, ids, ops, params);
|
||||
}
|
||||
|
||||
static int ipcget_public(struct ipc_namespace *ns, struct ipc_ids *ids, const struct ipc_ops *ops, struct ipc_params *params)
|
||||
{
|
||||
struct kern_ipc_perm *ipcp;
|
||||
int flg = params->flg;
|
||||
int err;
|
||||
ipcp = ipc_findkey(ids, params->key);
|
||||
if (ipcp == NULL) {
|
||||
if (!(flg & IPC_CREAT))
|
||||
err = -ENOENT;
|
||||
else
|
||||
err = ops->getnew(ns, params);
|
||||
} else {
|
||||
if (flg & IPC_CREAT && flg & IPC_EXCL)
|
||||
err = -EEXIST;
|
||||
else {
|
||||
err = 0;
|
||||
if (ops->more_checks)
|
||||
err = ops->more_checks(ipcp, params);
|
||||
......
|
||||
}
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在ipcget_public中,我们会按照key,去查找struct kern_ipc_perm。如果没有找到,那就看是否设置了IPC_CREAT;如果设置了,就创建一个新的。如果找到了,就将对应的id返回。
|
||||
|
||||
我们这里重点看,如何按照参数shm_ops,创建新的共享内存,会调用newseg。
|
||||
|
||||
```
|
||||
static int newseg(struct ipc_namespace *ns, struct ipc_params *params)
|
||||
{
|
||||
key_t key = params->key;
|
||||
int shmflg = params->flg;
|
||||
size_t size = params->u.size;
|
||||
int error;
|
||||
struct shmid_kernel *shp;
|
||||
size_t numpages = (size + PAGE_SIZE - 1) >> PAGE_SHIFT;
|
||||
struct file *file;
|
||||
char name[13];
|
||||
vm_flags_t acctflag = 0;
|
||||
......
|
||||
shp = kvmalloc(sizeof(*shp), GFP_KERNEL);
|
||||
......
|
||||
shp->shm_perm.key = key;
|
||||
shp->shm_perm.mode = (shmflg & S_IRWXUGO);
|
||||
shp->mlock_user = NULL;
|
||||
|
||||
shp->shm_perm.security = NULL;
|
||||
......
|
||||
file = shmem_kernel_file_setup(name, size, acctflag);
|
||||
......
|
||||
shp->shm_cprid = task_tgid_vnr(current);
|
||||
shp->shm_lprid = 0;
|
||||
shp->shm_atim = shp->shm_dtim = 0;
|
||||
shp->shm_ctim = get_seconds();
|
||||
shp->shm_segsz = size;
|
||||
shp->shm_nattch = 0;
|
||||
shp->shm_file = file;
|
||||
shp->shm_creator = current;
|
||||
|
||||
error = ipc_addid(&shm_ids(ns), &shp->shm_perm, ns->shm_ctlmni);
|
||||
......
|
||||
list_add(&shp->shm_clist, &current->sysvshm.shm_clist);
|
||||
......
|
||||
file_inode(file)->i_ino = shp->shm_perm.id;
|
||||
|
||||
ns->shm_tot += numpages;
|
||||
error = shp->shm_perm.id;
|
||||
......
|
||||
return error;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**newseg函数的第一步,通过kvmalloc在直接映射区分配一个struct shmid_kernel结构。**这个结构就是用来描述共享内存的。这个结构最开始就是上面说的struct kern_ipc_perm结构。接下来就是填充这个struct shmid_kernel结构,例如key、权限等。
|
||||
|
||||
**newseg函数的第二步,共享内存需要和文件进行关联**。**为什么要做这个呢?我们在讲内存映射的时候讲过,虚拟地址空间可以和物理内存关联,但是物理内存是某个进程独享的。虚拟地址空间也可以映射到一个文件,文件是可以跨进程共享的。
|
||||
|
||||
咱们这里的共享内存需要跨进程共享,也应该借鉴文件映射的思路。只不过不应该映射一个硬盘上的文件,而是映射到一个内存文件系统上的文件。mm/shmem.c里面就定义了这样一个基于内存的文件系统。这里你一定要注意区分shmem和shm的区别,前者是一个文件系统,后者是进程通信机制。
|
||||
|
||||
在系统初始化的时候,shmem_init注册了shmem文件系统shmem_fs_type,并且挂在到了shm_mnt下面。
|
||||
|
||||
```
|
||||
int __init shmem_init(void)
|
||||
{
|
||||
int error;
|
||||
error = shmem_init_inodecache();
|
||||
error = register_filesystem(&shmem_fs_type);
|
||||
shm_mnt = kern_mount(&shmem_fs_type);
|
||||
......
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct file_system_type shmem_fs_type = {
|
||||
.owner = THIS_MODULE,
|
||||
.name = "tmpfs",
|
||||
.mount = shmem_mount,
|
||||
.kill_sb = kill_litter_super,
|
||||
.fs_flags = FS_USERNS_MOUNT,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
接下来,newseg函数会调用shmem_kernel_file_setup,其实就是在shmem文件系统里面创建一个文件。
|
||||
|
||||
```
|
||||
/**
|
||||
* shmem_kernel_file_setup - get an unlinked file living in tmpfs which must be kernel internal.
|
||||
* @name: name for dentry (to be seen in /proc/<pid>/maps
|
||||
* @size: size to be set for the file
|
||||
* @flags: VM_NORESERVE suppresses pre-accounting of the entire object size */
|
||||
struct file *shmem_kernel_file_setup(const char *name, loff_t size, unsigned long flags)
|
||||
{
|
||||
return __shmem_file_setup(name, size, flags, S_PRIVATE);
|
||||
}
|
||||
|
||||
static struct file *__shmem_file_setup(const char *name, loff_t size,
|
||||
unsigned long flags, unsigned int i_flags)
|
||||
{
|
||||
struct file *res;
|
||||
struct inode *inode;
|
||||
struct path path;
|
||||
struct super_block *sb;
|
||||
struct qstr this;
|
||||
......
|
||||
this.name = name;
|
||||
this.len = strlen(name);
|
||||
this.hash = 0; /* will go */
|
||||
sb = shm_mnt->mnt_sb;
|
||||
path.mnt = mntget(shm_mnt);
|
||||
path.dentry = d_alloc_pseudo(sb, &this);
|
||||
d_set_d_op(path.dentry, &anon_ops);
|
||||
......
|
||||
inode = shmem_get_inode(sb, NULL, S_IFREG | S_IRWXUGO, 0, flags);
|
||||
inode->i_flags |= i_flags;
|
||||
d_instantiate(path.dentry, inode);
|
||||
inode->i_size = size;
|
||||
......
|
||||
res = alloc_file(&path, FMODE_WRITE | FMODE_READ,
|
||||
&shmem_file_operations);
|
||||
return res;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
__shmem_file_setup会创建新的shmem文件对应的dentry和inode,并将它们两个关联起来,然后分配一个struct file结构,来表示新的shmem文件,并且指向独特的shmem_file_operations。
|
||||
|
||||
```
|
||||
static const struct file_operations shmem_file_operations = {
|
||||
.mmap = shmem_mmap,
|
||||
.get_unmapped_area = shmem_get_unmapped_area,
|
||||
#ifdef CONFIG_TMPFS
|
||||
.llseek = shmem_file_llseek,
|
||||
.read_iter = shmem_file_read_iter,
|
||||
.write_iter = generic_file_write_iter,
|
||||
.fsync = noop_fsync,
|
||||
.splice_read = generic_file_splice_read,
|
||||
.splice_write = iter_file_splice_write,
|
||||
.fallocate = shmem_fallocate,
|
||||
#endif
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
**newseg函数的第三步,通过ipc_addid将新创建的struct shmid_kernel结构挂到shm_ids里面的基数树上,并返回相应的id,并且将struct shmid_kernel挂到当前进程的sysvshm队列中。**
|
||||
|
||||
至此,共享内存的创建就完成了。
|
||||
|
||||
## 如何将共享内存映射到虚拟地址空间?
|
||||
|
||||
从上面的代码解析中,我们知道,共享内存的数据结构struct shmid_kernel,是通过它的成员struct file *shm_file,来管理内存文件系统shmem上的内存文件的。无论这个共享内存是否被映射,shm_file都是存在的。
|
||||
|
||||
接下来,我们要将共享内存映射到虚拟地址空间中。调用的是shmat,对应的系统调用如下:
|
||||
|
||||
```
|
||||
SYSCALL_DEFINE3(shmat, int, shmid, char __user *, shmaddr, int, shmflg)
|
||||
{
|
||||
unsigned long ret;
|
||||
long err;
|
||||
err = do_shmat(shmid, shmaddr, shmflg, &ret, SHMLBA);
|
||||
force_successful_syscall_return();
|
||||
return (long)ret;
|
||||
}
|
||||
|
||||
long do_shmat(int shmid, char __user *shmaddr, int shmflg,
|
||||
ulong *raddr, unsigned long shmlba)
|
||||
{
|
||||
struct shmid_kernel *shp;
|
||||
unsigned long addr = (unsigned long)shmaddr;
|
||||
unsigned long size;
|
||||
struct file *file;
|
||||
int err;
|
||||
unsigned long flags = MAP_SHARED;
|
||||
unsigned long prot;
|
||||
int acc_mode;
|
||||
struct ipc_namespace *ns;
|
||||
struct shm_file_data *sfd;
|
||||
struct path path;
|
||||
fmode_t f_mode;
|
||||
unsigned long populate = 0;
|
||||
......
|
||||
prot = PROT_READ | PROT_WRITE;
|
||||
acc_mode = S_IRUGO | S_IWUGO;
|
||||
f_mode = FMODE_READ | FMODE_WRITE;
|
||||
......
|
||||
ns = current->nsproxy->ipc_ns;
|
||||
shp = shm_obtain_object_check(ns, shmid);
|
||||
......
|
||||
path = shp->shm_file->f_path;
|
||||
path_get(&path);
|
||||
shp->shm_nattch++;
|
||||
size = i_size_read(d_inode(path.dentry));
|
||||
......
|
||||
sfd = kzalloc(sizeof(*sfd), GFP_KERNEL);
|
||||
......
|
||||
file = alloc_file(&path, f_mode,
|
||||
is_file_hugepages(shp->shm_file) ?
|
||||
&shm_file_operations_huge :
|
||||
&shm_file_operations);
|
||||
......
|
||||
file->private_data = sfd;
|
||||
file->f_mapping = shp->shm_file->f_mapping;
|
||||
sfd->id = shp->shm_perm.id;
|
||||
sfd->ns = get_ipc_ns(ns);
|
||||
sfd->file = shp->shm_file;
|
||||
sfd->vm_ops = NULL;
|
||||
......
|
||||
addr = do_mmap_pgoff(file, addr, size, prot, flags, 0, &populate, NULL);
|
||||
*raddr = addr;
|
||||
err = 0;
|
||||
......
|
||||
return err;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这个函数里面,shm_obtain_object_check会通过共享内存的id,在基数树中找到对应的struct shmid_kernel结构,通过它找到shmem上的内存文件。
|
||||
|
||||
接下来,我们要分配一个struct shm_file_data,来表示这个内存文件。将shmem中指向内存文件的shm_file赋值给struct shm_file_data中的file成员。
|
||||
|
||||
然后,我们创建了一个struct file,指向的也是shmem中的内存文件。
|
||||
|
||||
为什么要再创建一个呢?这两个的功能不同,shmem中shm_file用于管理内存文件,是一个中立的,独立于任何一个进程的角色。而新创建的struct file是专门用于做内存映射的,就像咱们在讲内存映射那一节讲过的,一个硬盘上的文件要映射到虚拟地址空间中的时候,需要在vm_area_struct里面有一个struct file *vm_file指向硬盘上的文件,现在变成内存文件了,但是这个结构还是不能少。
|
||||
|
||||
新创建的struct file的private_data,指向struct shm_file_data,这样内存映射那部分的数据结构,就能够通过它来访问内存文件了。
|
||||
|
||||
新创建的struct file的file_operations也发生了变化,变成了shm_file_operations。
|
||||
|
||||
```
|
||||
static const struct file_operations shm_file_operations = {
|
||||
.mmap = shm_mmap,
|
||||
.fsync = shm_fsync,
|
||||
.release = shm_release,
|
||||
.get_unmapped_area = shm_get_unmapped_area,
|
||||
.llseek = noop_llseek,
|
||||
.fallocate = shm_fallocate,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
接下来,do_mmap_pgoff函数我们遇到过,原来映射硬盘上的文件的时候,也是调用它。这里我们不再详细解析了。它会分配一个vm_area_struct指向虚拟地址空间中没有分配的区域,它的vm_file指向这个内存文件,然后它会调用shm_file_operations的mmap函数,也即shm_mmap进行映射。
|
||||
|
||||
```
|
||||
static int shm_mmap(struct file *file, struct vm_area_struct *vma)
|
||||
{
|
||||
struct shm_file_data *sfd = shm_file_data(file);
|
||||
int ret;
|
||||
ret = __shm_open(vma);
|
||||
ret = call_mmap(sfd->file, vma);
|
||||
sfd->vm_ops = vma->vm_ops;
|
||||
vma->vm_ops = &shm_vm_ops;
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
shm_mmap中调用了shm_file_data中的file的mmap函数,这次调用的是shmem_file_operations的mmap,也即shmem_mmap。
|
||||
|
||||
```
|
||||
static int shmem_mmap(struct file *file, struct vm_area_struct *vma)
|
||||
{
|
||||
file_accessed(file);
|
||||
vma->vm_ops = &shmem_vm_ops;
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里面,vm_area_struct的vm_ops指向shmem_vm_ops。等从call_mmap中返回之后,shm_file_data的vm_ops指向了shmem_vm_ops,而vm_area_struct的vm_ops改为指向shm_vm_ops。
|
||||
|
||||
我们来看一下,shm_vm_ops和shmem_vm_ops的定义。
|
||||
|
||||
```
|
||||
static const struct vm_operations_struct shm_vm_ops = {
|
||||
.open = shm_open, /* callback for a new vm-area open */
|
||||
.close = shm_close, /* callback for when the vm-area is released */
|
||||
.fault = shm_fault,
|
||||
};
|
||||
|
||||
static const struct vm_operations_struct shmem_vm_ops = {
|
||||
.fault = shmem_fault,
|
||||
.map_pages = filemap_map_pages,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
它们里面最关键的就是fault函数,也即访问虚拟内存的时候,访问不到应该怎么办。
|
||||
|
||||
当访问不到的时候,先调用vm_area_struct的vm_ops,也即shm_vm_ops的fault函数shm_fault。然后它会转而调用shm_file_data的vm_ops,也即shmem_vm_ops的fault函数shmem_fault。
|
||||
|
||||
```
|
||||
static int shm_fault(struct vm_fault *vmf)
|
||||
{
|
||||
struct file *file = vmf->vma->vm_file;
|
||||
struct shm_file_data *sfd = shm_file_data(file);
|
||||
return sfd->vm_ops->fault(vmf);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
虽然基于内存的文件系统,已经为这个内存文件分配了inode,但是内存也却是一点儿都没分配,只有在发生缺页异常的时候才进行分配。
|
||||
|
||||
```
|
||||
static int shmem_fault(struct vm_fault *vmf)
|
||||
{
|
||||
struct vm_area_struct *vma = vmf->vma;
|
||||
struct inode *inode = file_inode(vma->vm_file);
|
||||
gfp_t gfp = mapping_gfp_mask(inode->i_mapping);
|
||||
......
|
||||
error = shmem_getpage_gfp(inode, vmf->pgoff, &vmf->page, sgp,
|
||||
gfp, vma, vmf, &ret);
|
||||
......
|
||||
}
|
||||
|
||||
/*
|
||||
* shmem_getpage_gfp - find page in cache, or get from swap, or allocate
|
||||
*
|
||||
* If we allocate a new one we do not mark it dirty. That's up to the
|
||||
* vm. If we swap it in we mark it dirty since we also free the swap
|
||||
* entry since a page cannot live in both the swap and page cache.
|
||||
*
|
||||
* fault_mm and fault_type are only supplied by shmem_fault:
|
||||
* otherwise they are NULL.
|
||||
*/
|
||||
static int shmem_getpage_gfp(struct inode *inode, pgoff_t index,
|
||||
struct page **pagep, enum sgp_type sgp, gfp_t gfp,
|
||||
struct vm_area_struct *vma, struct vm_fault *vmf, int *fault_type)
|
||||
{
|
||||
......
|
||||
page = shmem_alloc_and_acct_page(gfp, info, sbinfo,
|
||||
index, false);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
shmem_fault会调用shmem_getpage_gfp在page cache和swap中找一个空闲页,如果找不到就通过shmem_alloc_and_acct_page分配一个新的页,他最终会调用内存管理系统的alloc_page_vma在物理内存中分配一个页。
|
||||
|
||||
至此,共享内存才真的映射到了虚拟地址空间中,进程可以像访问本地内存一样访问共享内存。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
我们来总结一下共享内存的创建和映射过程。
|
||||
|
||||
1. 调用shmget创建共享内存。
|
||||
1. 先通过ipc_findkey在基数树中查找key对应的共享内存对象shmid_kernel是否已经被创建过,如果已经被创建,就会被查询出来,例如producer创建过,在consumer中就会查询出来。
|
||||
1. 如果共享内存没有被创建过,则调用shm_ops的newseg方法,创建一个共享内存对象shmid_kernel。例如,在producer中就会新建。
|
||||
1. 在shmem文件系统里面创建一个文件,共享内存对象shmid_kernel指向这个文件,这个文件用struct file表示,我们姑且称它为file1。
|
||||
1. 调用shmat,将共享内存映射到虚拟地址空间。
|
||||
1. shm_obtain_object_check先从基数树里面找到shmid_kernel对象。
|
||||
1. 创建用于内存映射到文件的file和shm_file_data,这里的struct file我们姑且称为file2。
|
||||
1. 关联内存区域vm_area_struct和用于内存映射到文件的file,也即file2,调用file2的mmap函数。
|
||||
1. file2的mmap函数shm_mmap,会调用file1的mmap函数shmem_mmap,设置shm_file_data和vm_area_struct的vm_ops。
|
||||
1. 内存映射完毕之后,其实并没有真的分配物理内存,当访问内存的时候,会触发缺页异常do_page_fault。
|
||||
1. vm_area_struct的vm_ops的shm_fault会调用shm_file_data的vm_ops的shmem_fault。
|
||||
1. 在page cache中找一个空闲页,或者创建一个空闲页。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/20/51/20e8f4e69d47b7469f374bc9fbcf7251.png" alt="">
|
||||
|
||||
## 课堂练习
|
||||
|
||||
在这里,我们只分析了shm_ids的结构,消息队列的程序我们写过了,但是msg_ids的结构没有解析,你可以试着解析一下。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
|
||||
@@ -0,0 +1,539 @@
|
||||
<audio id="audio" title="42 | IPC(下):不同项目组之间抢资源,如何协调?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/09/32/097590a47025a46e19b1551183297c32.mp3"></audio>
|
||||
|
||||
IPC这块的内容比较多,为了让你能够更好地理解,我分成了三节来讲。前面我们解析完了共享内存的内核机制后,今天我们来看最后一部分,信号量的内核机制。
|
||||
|
||||
首先,我们需要创建一个信号量,调用的是系统调用semget。代码如下:
|
||||
|
||||
```
|
||||
SYSCALL_DEFINE3(semget, key_t, key, int, nsems, int, semflg)
|
||||
{
|
||||
struct ipc_namespace *ns;
|
||||
static const struct ipc_ops sem_ops = {
|
||||
.getnew = newary,
|
||||
.associate = sem_security,
|
||||
.more_checks = sem_more_checks,
|
||||
};
|
||||
struct ipc_params sem_params;
|
||||
ns = current->nsproxy->ipc_ns;
|
||||
sem_params.key = key;
|
||||
sem_params.flg = semflg;
|
||||
sem_params.u.nsems = nsems;
|
||||
return ipcget(ns, &sem_ids(ns), &sem_ops, &sem_params);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们解析过了共享内存,再看信号量,就顺畅很多了。这里同样调用了抽象的ipcget,参数分别为信号量对应的sem_ids、对应的操作sem_ops以及对应的参数sem_params。
|
||||
|
||||
ipcget的代码我们已经解析过了。如果key设置为IPC_PRIVATE则永远创建新的;如果不是的话,就会调用ipcget_public。
|
||||
|
||||
在ipcget_public中,我们能会按照key,去查找struct kern_ipc_perm。如果没有找到,那就看看是否设置了IPC_CREAT。如果设置了,就创建一个新的。如果找到了,就将对应的id返回。
|
||||
|
||||
我们这里重点看,如何按照参数sem_ops,创建新的信号量会调用newary。
|
||||
|
||||
```
|
||||
static int newary(struct ipc_namespace *ns, struct ipc_params *params)
|
||||
{
|
||||
int retval;
|
||||
struct sem_array *sma;
|
||||
key_t key = params->key;
|
||||
int nsems = params->u.nsems;
|
||||
int semflg = params->flg;
|
||||
int i;
|
||||
......
|
||||
sma = sem_alloc(nsems);
|
||||
......
|
||||
sma->sem_perm.mode = (semflg & S_IRWXUGO);
|
||||
sma->sem_perm.key = key;
|
||||
sma->sem_perm.security = NULL;
|
||||
......
|
||||
for (i = 0; i < nsems; i++) {
|
||||
INIT_LIST_HEAD(&sma->sems[i].pending_alter);
|
||||
INIT_LIST_HEAD(&sma->sems[i].pending_const);
|
||||
spin_lock_init(&sma->sems[i].lock);
|
||||
}
|
||||
sma->complex_count = 0;
|
||||
sma->use_global_lock = USE_GLOBAL_LOCK_HYSTERESIS;
|
||||
INIT_LIST_HEAD(&sma->pending_alter);
|
||||
INIT_LIST_HEAD(&sma->pending_const);
|
||||
INIT_LIST_HEAD(&sma->list_id);
|
||||
sma->sem_nsems = nsems;
|
||||
sma->sem_ctime = get_seconds();
|
||||
retval = ipc_addid(&sem_ids(ns), &sma->sem_perm, ns->sc_semmni);
|
||||
......
|
||||
ns->used_sems += nsems;
|
||||
......
|
||||
return sma->sem_perm.id;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
newary函数的第一步,通过kvmalloc在直接映射区分配一个struct sem_array结构。这个结构是用来描述信号量的,这个结构最开始就是上面说的struct kern_ipc_perm结构。接下来就是填充这个struct sem_array结构,例如key、权限等。
|
||||
|
||||
struct sem_array里有多个信号量,放在struct sem sems[]数组里面,在struct sem里面有当前的信号量的数值semval。
|
||||
|
||||
```
|
||||
struct sem {
|
||||
int semval; /* current value */
|
||||
/*
|
||||
* PID of the process that last modified the semaphore. For
|
||||
* Linux, specifically these are:
|
||||
* - semop
|
||||
* - semctl, via SETVAL and SETALL.
|
||||
* - at task exit when performing undo adjustments (see exit_sem).
|
||||
*/
|
||||
int sempid;
|
||||
spinlock_t lock; /* spinlock for fine-grained semtimedop */
|
||||
struct list_head pending_alter; /* pending single-sop operations that alter the semaphore */
|
||||
struct list_head pending_const; /* pending single-sop operations that do not alter the semaphore*/
|
||||
time_t sem_otime; /* candidate for sem_otime */
|
||||
} ____cacheline_aligned_in_smp;
|
||||
|
||||
```
|
||||
|
||||
struct sem_array和struct sem各有一个链表struct list_head pending_alter,分别表示对于整个信号量数组的修改和对于某个信号量的修改。
|
||||
|
||||
newary函数的第二步,就是初始化这些链表。
|
||||
|
||||
newary函数的第三步,通过ipc_addid将新创建的struct sem_array结构,挂到sem_ids里面的基数树上,并返回相应的id。
|
||||
|
||||
信号量创建的过程到此结束,接下来我们来看,如何通过semctl对信号量数组进行初始化。
|
||||
|
||||
```
|
||||
SYSCALL_DEFINE4(semctl, int, semid, int, semnum, int, cmd, unsigned long, arg)
|
||||
{
|
||||
int version;
|
||||
struct ipc_namespace *ns;
|
||||
void __user *p = (void __user *)arg;
|
||||
ns = current->nsproxy->ipc_ns;
|
||||
switch (cmd) {
|
||||
case IPC_INFO:
|
||||
case SEM_INFO:
|
||||
case IPC_STAT:
|
||||
case SEM_STAT:
|
||||
return semctl_nolock(ns, semid, cmd, version, p);
|
||||
case GETALL:
|
||||
case GETVAL:
|
||||
case GETPID:
|
||||
case GETNCNT:
|
||||
case GETZCNT:
|
||||
case SETALL:
|
||||
return semctl_main(ns, semid, semnum, cmd, p);
|
||||
case SETVAL:
|
||||
return semctl_setval(ns, semid, semnum, arg);
|
||||
case IPC_RMID:
|
||||
case IPC_SET:
|
||||
return semctl_down(ns, semid, cmd, version, p);
|
||||
default:
|
||||
return -EINVAL;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里我们重点看,SETALL操作调用的semctl_main函数,以及SETVAL操作调用的semctl_setval函数。
|
||||
|
||||
对于SETALL操作来讲,传进来的参数为union semun里面的unsigned short *array,会设置整个信号量集合。
|
||||
|
||||
```
|
||||
static int semctl_main(struct ipc_namespace *ns, int semid, int semnum,
|
||||
int cmd, void __user *p)
|
||||
{
|
||||
struct sem_array *sma;
|
||||
struct sem *curr;
|
||||
int err, nsems;
|
||||
ushort fast_sem_io[SEMMSL_FAST];
|
||||
ushort *sem_io = fast_sem_io;
|
||||
DEFINE_WAKE_Q(wake_q);
|
||||
sma = sem_obtain_object_check(ns, semid);
|
||||
nsems = sma->sem_nsems;
|
||||
......
|
||||
switch (cmd) {
|
||||
......
|
||||
case SETALL:
|
||||
{
|
||||
int i;
|
||||
struct sem_undo *un;
|
||||
......
|
||||
if (copy_from_user(sem_io, p, nsems*sizeof(ushort))) {
|
||||
......
|
||||
}
|
||||
......
|
||||
for (i = 0; i < nsems; i++) {
|
||||
sma->sems[i].semval = sem_io[i];
|
||||
sma->sems[i].sempid = task_tgid_vnr(current);
|
||||
}
|
||||
......
|
||||
sma->sem_ctime = get_seconds();
|
||||
/* maybe some queued-up processes were waiting for this */
|
||||
do_smart_update(sma, NULL, 0, 0, &wake_q);
|
||||
err = 0;
|
||||
goto out_unlock;
|
||||
}
|
||||
}
|
||||
......
|
||||
wake_up_q(&wake_q);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在semctl_main函数中,先是通过sem_obtain_object_check,根据信号量集合的id在基数树里面找到struct sem_array对象,发现如果是SETALL操作,就将用户的参数中的unsigned short *array通过copy_from_user拷贝到内核里面的sem_io数组,然后是一个循环,对于信号量集合里面的每一个信号量,设置semval,以及修改这个信号量值的pid。
|
||||
|
||||
对于SETVAL操作来讲,传进来的参数union semun里面的int val,仅仅会设置某个信号量。
|
||||
|
||||
```
|
||||
static int semctl_setval(struct ipc_namespace *ns, int semid, int semnum,
|
||||
unsigned long arg)
|
||||
{
|
||||
struct sem_undo *un;
|
||||
struct sem_array *sma;
|
||||
struct sem *curr;
|
||||
int err, val;
|
||||
DEFINE_WAKE_Q(wake_q);
|
||||
......
|
||||
sma = sem_obtain_object_check(ns, semid);
|
||||
......
|
||||
curr = &sma->sems[semnum];
|
||||
......
|
||||
curr->semval = val;
|
||||
curr->sempid = task_tgid_vnr(current);
|
||||
sma->sem_ctime = get_seconds();
|
||||
/* maybe some queued-up processes were waiting for this */
|
||||
do_smart_update(sma, NULL, 0, 0, &wake_q);
|
||||
......
|
||||
wake_up_q(&wake_q);
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在semctl_setval函数中,我们先是通过sem_obtain_object_check,根据信号量集合的id在基数树里面找到struct sem_array对象,对于SETVAL操作,直接根据参数中的val设置semval,以及修改这个信号量值的pid。
|
||||
|
||||
至此,信号量数组初始化完毕。接下来我们来看P操作和V操作。无论是P操作,还是V操作都是调用semop系统调用。
|
||||
|
||||
```
|
||||
SYSCALL_DEFINE3(semop, int, semid, struct sembuf __user *, tsops,
|
||||
unsigned, nsops)
|
||||
{
|
||||
return sys_semtimedop(semid, tsops, nsops, NULL);
|
||||
}
|
||||
|
||||
SYSCALL_DEFINE4(semtimedop, int, semid, struct sembuf __user *, tsops,
|
||||
unsigned, nsops, const struct timespec __user *, timeout)
|
||||
{
|
||||
int error = -EINVAL;
|
||||
struct sem_array *sma;
|
||||
struct sembuf fast_sops[SEMOPM_FAST];
|
||||
struct sembuf *sops = fast_sops, *sop;
|
||||
struct sem_undo *un;
|
||||
int max, locknum;
|
||||
bool undos = false, alter = false, dupsop = false;
|
||||
struct sem_queue queue;
|
||||
unsigned long dup = 0, jiffies_left = 0;
|
||||
struct ipc_namespace *ns;
|
||||
|
||||
ns = current->nsproxy->ipc_ns;
|
||||
......
|
||||
if (copy_from_user(sops, tsops, nsops * sizeof(*tsops))) {
|
||||
error = -EFAULT;
|
||||
goto out_free;
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
struct timespec _timeout;
|
||||
if (copy_from_user(&_timeout, timeout, sizeof(*timeout))) {
|
||||
}
|
||||
jiffies_left = timespec_to_jiffies(&_timeout);
|
||||
}
|
||||
......
|
||||
/* On success, find_alloc_undo takes the rcu_read_lock */
|
||||
un = find_alloc_undo(ns, semid);
|
||||
......
|
||||
sma = sem_obtain_object_check(ns, semid);
|
||||
......
|
||||
queue.sops = sops;
|
||||
queue.nsops = nsops;
|
||||
queue.undo = un;
|
||||
queue.pid = task_tgid_vnr(current);
|
||||
queue.alter = alter;
|
||||
queue.dupsop = dupsop;
|
||||
|
||||
error = perform_atomic_semop(sma, &queue);
|
||||
if (error == 0) { /* non-blocking succesfull path */
|
||||
DEFINE_WAKE_Q(wake_q);
|
||||
......
|
||||
do_smart_update(sma, sops, nsops, 1, &wake_q);
|
||||
......
|
||||
wake_up_q(&wake_q);
|
||||
goto out_free;
|
||||
}
|
||||
/*
|
||||
* We need to sleep on this operation, so we put the current
|
||||
* task into the pending queue and go to sleep.
|
||||
*/
|
||||
if (nsops == 1) {
|
||||
struct sem *curr;
|
||||
curr = &sma->sems[sops->sem_num];
|
||||
......
|
||||
list_add_tail(&queue.list,
|
||||
&curr->pending_alter);
|
||||
......
|
||||
} else {
|
||||
......
|
||||
list_add_tail(&queue.list, &sma->pending_alter);
|
||||
......
|
||||
}
|
||||
|
||||
do {
|
||||
queue.status = -EINTR;
|
||||
queue.sleeper = current;
|
||||
|
||||
__set_current_state(TASK_INTERRUPTIBLE);
|
||||
if (timeout)
|
||||
jiffies_left = schedule_timeout(jiffies_left);
|
||||
else
|
||||
schedule();
|
||||
......
|
||||
/*
|
||||
* If an interrupt occurred we have to clean up the queue.
|
||||
*/
|
||||
if (timeout && jiffies_left == 0)
|
||||
error = -EAGAIN;
|
||||
} while (error == -EINTR && !signal_pending(current)); /* spurious */
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
semop会调用semtimedop,这是一个非常复杂的函数。
|
||||
|
||||
semtimedop做的第一件事情,就是将用户的参数,例如,对于信号量的操作struct sembuf,拷贝到内核里面来。另外,如果是P操作,很可能让进程进入等待状态,是否要为这个等待状态设置一个超时,timeout也是一个参数,会把它变成时钟的滴答数目。
|
||||
|
||||
semtimedop做的第二件事情,是通过sem_obtain_object_check,根据信号量集合的id,获得struct sem_array,然后,创建一个struct sem_queue表示当前的信号量操作。为什么叫queue呢?因为这个操作可能马上就能完成,也可能因为无法获取信号量不能完成,不能完成的话就只好排列到队列上,等待信号量满足条件的时候。semtimedop会调用perform_atomic_semop在实施信号量操作。
|
||||
|
||||
```
|
||||
static int perform_atomic_semop(struct sem_array *sma, struct sem_queue *q)
|
||||
{
|
||||
int result, sem_op, nsops;
|
||||
struct sembuf *sop;
|
||||
struct sem *curr;
|
||||
struct sembuf *sops;
|
||||
struct sem_undo *un;
|
||||
|
||||
sops = q->sops;
|
||||
nsops = q->nsops;
|
||||
un = q->undo;
|
||||
|
||||
for (sop = sops; sop < sops + nsops; sop++) {
|
||||
curr = &sma->sems[sop->sem_num];
|
||||
sem_op = sop->sem_op;
|
||||
result = curr->semval;
|
||||
......
|
||||
result += sem_op;
|
||||
if (result < 0)
|
||||
goto would_block;
|
||||
......
|
||||
if (sop->sem_flg & SEM_UNDO) {
|
||||
int undo = un->semadj[sop->sem_num] - sem_op;
|
||||
.....
|
||||
}
|
||||
}
|
||||
|
||||
for (sop = sops; sop < sops + nsops; sop++) {
|
||||
curr = &sma->sems[sop->sem_num];
|
||||
sem_op = sop->sem_op;
|
||||
result = curr->semval;
|
||||
|
||||
if (sop->sem_flg & SEM_UNDO) {
|
||||
int undo = un->semadj[sop->sem_num] - sem_op;
|
||||
un->semadj[sop->sem_num] = undo;
|
||||
}
|
||||
curr->semval += sem_op;
|
||||
curr->sempid = q->pid;
|
||||
}
|
||||
return 0;
|
||||
would_block:
|
||||
q->blocking = sop;
|
||||
return sop->sem_flg & IPC_NOWAIT ? -EAGAIN : 1;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在perform_atomic_semop函数中,对于所有信号量操作都进行两次循环。在第一次循环中,如果发现计算出的result小于0,则说明必须等待,于是跳到would_block中,设置q->blocking = sop表示这个queue是block在这个操作上,然后如果需要等待,则返回1。如果第一次循环中发现无需等待,则第二个循环实施所有的信号量操作,将信号量的值设置为新的值,并且返回0。
|
||||
|
||||
接下来,我们回到semtimedop,来看它干的第三件事情,就是如果需要等待,应该怎么办?
|
||||
|
||||
如果需要等待,则要区分刚才的对于信号量的操作,是对一个信号量的,还是对于整个信号量集合的。如果是对于一个信号量的,那我们就将queue挂到这个信号量的pending_alter中;如果是对于整个信号量集合的,那我们就将queue挂到整个信号量集合的pending_alter中。
|
||||
|
||||
接下来的do-while循环,就是要开始等待了。如果等待没有时间限制,则调用schedule让出CPU;如果等待有时间限制,则调用schedule_timeout让出CPU,过一段时间还回来。当回来的时候,判断是否等待超时,如果没有等待超时则进入下一轮循环,再次等待,如果超时则退出循环,返回错误。在让出CPU的时候,设置进程的状态为TASK_INTERRUPTIBLE,并且循环的结束会通过signal_pending查看是否收到过信号,这说明这个等待信号量的进程是可以被信号中断的,也即一个等待信号量的进程是可以通过kill杀掉的。
|
||||
|
||||
我们再来看,semtimedop要做的第四件事情,如果不需要等待,应该怎么办?
|
||||
|
||||
如果不需要等待,就说明对于信号量的操作完成了,也改变了信号量的值。接下来,就是一个标准流程。我们通过DEFINE_WAKE_Q(wake_q)声明一个wake_q,调用do_smart_update,看这次对于信号量的值的改变,可以影响并可以激活等待队列中的哪些struct sem_queue,然后把它们都放在wake_q里面,调用wake_up_q唤醒这些进程。其实,所有的对于信号量的值的修改都会涉及这三个操作,如果你回过头去仔细看SETALL和SETVAL操作,在设置完毕信号量之后,也是这三个操作。
|
||||
|
||||
我们来看do_smart_update是如何实现的。do_smart_update会调用update_queue。
|
||||
|
||||
```
|
||||
static int update_queue(struct sem_array *sma, int semnum, struct wake_q_head *wake_q)
|
||||
{
|
||||
struct sem_queue *q, *tmp;
|
||||
struct list_head *pending_list;
|
||||
int semop_completed = 0;
|
||||
|
||||
if (semnum == -1)
|
||||
pending_list = &sma->pending_alter;
|
||||
else
|
||||
pending_list = &sma->sems[semnum].pending_alter;
|
||||
|
||||
again:
|
||||
list_for_each_entry_safe(q, tmp, pending_list, list) {
|
||||
int error, restart;
|
||||
......
|
||||
error = perform_atomic_semop(sma, q);
|
||||
|
||||
/* Does q->sleeper still need to sleep? */
|
||||
if (error > 0)
|
||||
continue;
|
||||
|
||||
unlink_queue(sma, q);
|
||||
......
|
||||
wake_up_sem_queue_prepare(q, error, wake_q);
|
||||
......
|
||||
}
|
||||
return semop_completed;
|
||||
}
|
||||
|
||||
static inline void wake_up_sem_queue_prepare(struct sem_queue *q, int error,
|
||||
struct wake_q_head *wake_q)
|
||||
{
|
||||
wake_q_add(wake_q, q->sleeper);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
update_queue会依次循环整个信号量集合的等待队列pending_alter,或者某个信号量的等待队列。试图在信号量的值变了的情况下,再次尝试perform_atomic_semop进行信号量操作。如果不成功,则尝试队列中的下一个;如果尝试成功,则调用unlink_queue从队列上取下来,然后调用wake_up_sem_queue_prepare,将q->sleeper加到wake_q上去。q->sleeper是一个task_struct,是等待在这个信号量操作上的进程。
|
||||
|
||||
接下来,wake_up_q就依次唤醒wake_q上的所有task_struct,调用的是我们在进程调度那一节学过的wake_up_process方法。
|
||||
|
||||
```
|
||||
void wake_up_q(struct wake_q_head *head)
|
||||
{
|
||||
struct wake_q_node *node = head->first;
|
||||
|
||||
while (node != WAKE_Q_TAIL) {
|
||||
struct task_struct *task;
|
||||
|
||||
task = container_of(node, struct task_struct, wake_q);
|
||||
|
||||
node = node->next;
|
||||
task->wake_q.next = NULL;
|
||||
|
||||
wake_up_process(task);
|
||||
put_task_struct(task);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
至此,对于信号量的主流操作都解析完毕了。
|
||||
|
||||
其实还有一点需要强调一下,信号量是一个整个Linux可见的全局资源,而不像咱们在线程同步那一节讲过的都是某个进程独占的资源,好处是可以跨进程通信,坏处就是如果一个进程通过P操作拿到了一个信号量,但是不幸异常退出了,如果没有来得及归还这个信号量,可能所有其他的进程都阻塞了。
|
||||
|
||||
那怎么办呢?Linux有一种机制叫SEM_UNDO,也即每一个semop操作都会保存一个反向struct sem_undo操作,当因为某个进程异常退出的时候,这个进程做的所有的操作都会回退,从而保证其他进程可以正常工作。
|
||||
|
||||
如果你回头看,我们写的程序里面的semaphore_p函数和semaphore_v函数,都把sem_flg设置为SEM_UNDO,就是这个作用。
|
||||
|
||||
等待队列上的每一个struct sem_queue,都有一个struct sem_undo,以此来表示这次操作的反向操作。
|
||||
|
||||
```
|
||||
struct sem_queue {
|
||||
struct list_head list; /* queue of pending operations */
|
||||
struct task_struct *sleeper; /* this process */
|
||||
struct sem_undo *undo; /* undo structure */
|
||||
int pid; /* process id of requesting process */
|
||||
int status; /* completion status of operation */
|
||||
struct sembuf *sops; /* array of pending operations */
|
||||
struct sembuf *blocking; /* the operation that blocked */
|
||||
int nsops; /* number of operations */
|
||||
bool alter; /* does *sops alter the array? */
|
||||
bool dupsop; /* sops on more than one sem_num */
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
在进程的task_struct里面对于信号量有一个成员struct sysv_sem,里面是一个struct sem_undo_list,将这个进程所有的semop所带来的undo操作都串起来。
|
||||
|
||||
```
|
||||
struct task_struct {
|
||||
......
|
||||
struct sysv_sem sysvsem;
|
||||
......
|
||||
}
|
||||
|
||||
struct sysv_sem {
|
||||
struct sem_undo_list *undo_list;
|
||||
};
|
||||
|
||||
struct sem_undo {
|
||||
struct list_head list_proc; /* per-process list: *
|
||||
* all undos from one process
|
||||
* rcu protected */
|
||||
struct rcu_head rcu; /* rcu struct for sem_undo */
|
||||
struct sem_undo_list *ulp; /* back ptr to sem_undo_list */
|
||||
struct list_head list_id; /* per semaphore array list:
|
||||
* all undos for one array */
|
||||
int semid; /* semaphore set identifier */
|
||||
short *semadj; /* array of adjustments */
|
||||
/* one per semaphore */
|
||||
};
|
||||
|
||||
struct sem_undo_list {
|
||||
atomic_t refcnt;
|
||||
spinlock_t lock;
|
||||
struct list_head list_proc;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
为了让你更清楚地理解struct sem_undo的原理,我们这里举一个例子。
|
||||
|
||||
假设我们创建了两个信号量集合。一个叫semaphore1,它包含三个信号量,初始化值为3,另一个叫semaphore2,它包含4个信号量,初始化值都为4。初始化时候的信号量以及undo结构里面的值如图中(1)标号所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/03/d6/0352227c5f49d194b6094f229220cdd6.png" alt="">
|
||||
|
||||
首先,我们来看进程1。我们调用semop,将semaphore1的三个信号量的值,分别加1、加2和减3,从而信号量的值变为4,5,0。于是在semaphore1和进程1链表交汇的undo结构里面,填写-1,-2,+3,是semop操作的反向操作,如图中(2)标号所示。
|
||||
|
||||
然后,我们来看进程2。我们调用semop,将semaphore1的三个信号量的值,分别减3、加2和加1,从而信号量的值变为1、7、1。于是在semaphore1和进程2链表交汇的undo结构里面,填写+3、-2、-1,是semop操作的反向操作,如图中(3)标号所示。
|
||||
|
||||
然后,我们接着看进程2。我们调用semop,将semaphore2的四个信号量的值,分别减3、加1、加4和减1,从而信号量的值变为1、5、8、3。于是,在semaphore2和进程2链表交汇的undo结构里面,填写+3、-1、-4、+1,是semop操作的反向操作,如图中(4)标号所示。
|
||||
|
||||
然后,我们再来看进程1。我们调用semop,将semaphore2的四个信号量的值,分别减1、减4、减5和加2,从而信号量的值变为0、1、3、5。于是在semaphore2和进程1链表交汇的undo结构里面,填写+1、+4、+5、-2,是semop操作的反向操作,如图中(5)标号所示。
|
||||
|
||||
从这个例子可以看出,无论哪个进程异常退出,只要将undo结构里面的值加回当前信号量的值,就能够得到正确的信号量的值,不会因为一个进程退出,导致信号量的值处于不一致的状态。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
信号量的机制也很复杂,我们对着下面这个图总结一下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/60/7c/6028c83b0aa00e65916988911aa01b7c.png" alt="">
|
||||
|
||||
1. 调用semget创建信号量集合。
|
||||
1. ipc_findkey会在基数树中,根据key查找信号量集合sem_array对象。如果已经被创建,就会被查询出来。例如producer被创建过,在consumer中就会查询出来。
|
||||
1. 如果信号量集合没有被创建过,则调用sem_ops的newary方法,创建一个信号量集合对象sem_array。例如,在producer中就会新建。
|
||||
1. 调用semctl(SETALL)初始化信号量。
|
||||
1. sem_obtain_object_check先从基数树里面找到sem_array对象。
|
||||
1. 根据用户指定的信号量数组,初始化信号量集合,也即初始化sem_array对象的struct sem sems[]成员。
|
||||
1. 调用semop操作信号量。
|
||||
1. 创建信号量操作结构sem_queue,放入队列。
|
||||
1. 创建undo结构,放入链表。
|
||||
|
||||
## 课堂练习
|
||||
|
||||
现在,我们的共享内存、信号量、消息队列都讲完了,你是不是觉得,它们的API非常相似。为了方便记忆,你可以自己整理一个表格,列一下这三种进程间通信机制、行为创建xxxget、使用、控制xxxctl、对应的API和系统调用。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user