Linux——进程间通信
进程间通信介绍
1.1 通信的介绍
通信就是一个进程把数据传递给另一个进程,但是每个进程都具有独立性
通信的本质:OS需要直接或者间接给通信双方的进程提供“内存空间”,需要通信的进程在这份空间中进行数据的交互,所以通信的前提是双方能够先看到同一份公共的资源
1.2 进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
1.3 通信的分类
采用文件的做法:管道—基于文件系统(匿名管道、命名管道)
采用标准的做法:System V进程间通信(聚焦在本地通信,如共享内存、消息队列)、POSIX进程间通信(让通信过程可以跨主机)
强调:
进程间通信的本质:让不同的进程先看到同一份资源(通常都是由OS提供)
一、管道
管道是Unix中最古老的进程间通信的形式
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
1.1 管道运行原理
想让进程间进行通信,就势必需要先让进程间看到同一份资源,而我们之前学的文件部分的内容刚好可以做到这点一点
只需要将两个进程同时打开同一个文件就可以做到让两个进程间先看到同一份资源
在之前的学习中,我们知道进程在打开文件的时候,会为其创建
struct file
结构体,并将其链入到struct file *fd_array[]
的数组中管理起来,这里就不多描述细节了
struct file
中有三个主要的东西,分别是struct inode
、方法集、还有文件缓冲区,用户级缓冲区会根据一定的刷新规则,定期向文件缓冲区中刷入数据,OS会将文件缓冲区中的内容写入磁盘中
此处我们想要做的只有进程间通信,所以我们不需要将文件缓冲区的内容写入到磁盘中,只是借助了文件缓冲区使得进程间能看到同一份资源
值得注意的是管道通信需要遵循单向通信的规则,也就是只能一端写,一端读
1.2 匿名管道
1.2.1 匿名管道实现原理
匿名管道:顾名思义就是没有具体名称的管道,之所以没有名字是因为匿名管道是基于父子进程之间形成的
由父进程先创建并打开一个管道文件(分别以读方式和写方式打开这个文件)
父进程再通过fork创建子进程,此时子进程就会根据父进程的task_struct
创建自己的task_struct
,而其中指向struct files_struct
的指针也是同一份,所以默认子进程也分别以读写方式打开了这个管道文件
随后根据双方的通信规则,指定读端与写端,这里默认父读子写,所以我们将父进程的写方式close()
掉,再将子进程的读端close()
掉
由于不管是以读还是写打开这个管道文件,父子双方都能看到这个管道文件的文件缓冲区,此时就建立起来了一条单向通信的管道
1.2.2 匿名管道的接口
#include <unistd.h>
功能:创建一无名管道
原型:int pipe(int fd[2]);
参数fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
返回值:成功返回0,失败返回错误代码
调用pipe创建管道时,需要传入一个int类型的数组,数组需要有两个元素,前面说到父进程会分别以读方式和写方式打开这个文件,也就是会返回两个文件描述符fd,而创建成功后会将这两个fd写入数组中,作为返回形参数返回给父进程,其中fd[0]表示读端,fd[1]表示写端
1.2.3 匿名管道测试代码
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstdlib>
using namespace std;
int main()
{
int pipefd[2];
int n = pipe(pipefd);
assert(n == 0);
int pid = fork();
//父读子写
if(pid == 0)
{
close(pipefd[0]);
int cnt = 10;
while(cnt--)
{
char message[1024];
snprintf(message,sizeof(message),"hello father, I an child pid : %d cnt : %d",getpid(),cnt);
write(pipefd[1],message,strlen(message));
sleep(1);
}
exit(0);
}
while(true)
{
close(pipefd[1]);
char buffer[1024];
ssize_t n = read(pipefd[0],buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n] = 0;
cout<<buffer<<endl;
}
else if(n == 0)//子进程退出后,父进程再读会什么都读不到read返回0,代表子进程退出,所以父进程也退出
{
cout<<"child write end"<<endl;
break;
}
}
//父进程等待子进程
int rpid = waitpid(pid,NULL,0);
if(rpid == pid)
{
cout<<"wait success"<<endl;
}
return 0;
}
1.2.4 匿名管道实现进程池
ProgressPool.cc
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cassert>
#include <string>
#include <vector>
#include "Task.hpp"
using namespace std;
static int count_num = 0;
class channel
{
public:
pid_t _pid;
int _fd;
string _name;
public:
channel(int pid, int fd, string name = "channel")
: _pid(pid), _fd(fd), _name(name + to_string(++count_num))
{
}
};
void work()
{
int code;
while (true)
{
int n = read(0, &code, sizeof(code));
if (n == sizeof(code))
{
if (init.ChackSafe(code))
{
init.RunTask(code);
}
continue;
}
else if (n == 0)
{
break;
}
}
}
void CreateChannels(vector<channel> *c)
{
vector<int> Last;
for (int i = 0; i < 5; i++)
{
int pipefd[2];
int n = pipe(pipefd);
assert(n == 0);
int id = fork();
// 0读 1写
if (id == 0)
{
close(pipefd[1]);
//关闭由父端继承过来的其他子进程的读端
if(!Last.empty())
{
for(auto& e : Last)
{
close(e);
//cout<<e<<" ";
}
//cout<<endl;
}
// 此处等于固定读入位置的fd,避免了传参,所有的子进程都从0中读任务,简化了后续work()中的操作
dup2(pipefd[0], 0);
work();
exit(0);
}
//cout<<id<<" "<<pipefd[0]<<" "<<pipefd[1]<<endl;
c->push_back(channel(id, pipefd[1]));
close(pipefd[0]);
Last.push_back(pipefd[1]);
}
}
// flag = true 一直派发任务
// flag = flase 派发num个任务
void SendCommend(const vector<channel>& c, bool flag, int num = -1)
{
int n = 0;
while (true)
{
int code = init.SelectTask();
//cout<<c[n]._name<<" "<<code<<endl;
write(c[n++]._fd, &code, sizeof(code));
n %= c.size();
if (!flag)
{
num--;
if (num <= 0)
{
break;
}
}
sleep(1);
}
}
void CloseChannals(const vector<channel>& c)
{
for(auto& e : c)
{
close(e._fd);
pid_t rpid = waitpid(e._pid,nullptr,0);
if(rpid == e._pid)
{
cout<<"wait success"<<"pid : "<< rpid <<endl;
}
}
}
int main()
{
vector<channel> channels;
CreateChannels(&channels);
//SendCommend(channels, 1);
SendCommend(channels, 0, 5);
CloseChannals(channels);
//sleep(3);
return 0;
}
Task.hpp
#pragma once
#include <iostream>
#include <vector>
#include <functional>
#include <unistd.h>
using namespace std;
typedef function<void()> task;
void Download()
{
std::cout << "我是一个下载任务" << " 处理者: " << getpid() << std::endl;
}
void PrintLog()
{
std::cout << "我是一个打印日志的任务" << " 处理者: " << getpid() << std::endl;
}
void PushVideoStream()
{
std::cout << "这是一个推送视频流的任务" << " 处理者: " << getpid() << std::endl;
}
class Init
{
public:
const static int g_download_code = 0;
const static int g_printlog_code = 1;
const static int g_push_videostream_code = 2;
vector<task> tasks;
public:
Init()
{
tasks.push_back(Download);
tasks.push_back(PrintLog);
tasks.push_back(PushVideoStream);
srand(time(nullptr));
}
bool ChackSafe(int code)
{
if(code>=0 && code<tasks.size())
{
return true;
}
else
{
return false;
}
}
void RunTask(int code)
{
tasks[code]();
}
int SelectTask()
{
return rand() % tasks.size();
}
};
Init init;
1.3 命名管道
匿名管道是基于父子进程之间进行通信,这是具有血缘关系的进程进行进程间通信
那么如何让两个毫不相干的进程进行通信呢?
命名管道(命名管道是其中的一种方法)
1.3.1 命名管道的原理
想要进行进程间通信的前提是先让通信双方能看到同一份资源,匿名管道是借助父子进程之间的继承使得父子之间能看到同一份文件,而毫不相干的进程之间肯定也要通信,怎么能先看到同一份资源呢?
路径是具有唯一性的,我们可以使用路径+文件名,来唯一的让不同进程看到同一份资源!
没有血缘关系的进程之间,可以借助管道文件来进行通信,通过创建管道文件,该文件就有了路径+文件名,通信双方就能找到这个文件,也就看到了同一份资源
1.3.2 创建命名管道的接口
#include <sys/types.h>
#include <sys/stat.h>
功能:创建命名管道
原型:int mkfifo(const char *pathname,mode_t mode);
参数:pathname表示命名管道的名字,mode表示该文件的权限
返回值:成功返回0,失败返回-1,错误码被设置
创建好命名管道后,通过命名管道的路径+文件名就能使通信双方都看到这份共享的资源,再借助文件操作open分别以读写方式打开命名管道,就能进行通信了
1.3.3 命名管道实现server端和client端通信
comm.h
#pragma once
#define FILENAME "fifo"
server.cc
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <unistd.h>
#include "comm.h"
using namespace std;
bool Makefifo()
{
int n = mkfifo(FILENAME, 0666);
if (n == 0)
{
return true;
}
else
{
return false;
}
}
int main()
{
start:
int rfd = open(FILENAME, O_RDONLY);
if (rfd < 0)
{
cout << "error: " << errno << ", errstring: " << strerror(errno) << endl;
if (Makefifo())
{
goto start;
}
else
{
return 1;
}
}
cout << "open fifo success !" << endl;
char buffer[1024];
while (true)
{
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
cout << "Client say# " << buffer << endl;
}
else if (n == 0)
{
cout << "client end, server end too !" << endl;
break;
}
}
close(rfd);
cout << "close file success !" << endl;
return 0;
}
client.cc
#include <cstring>
#include <unistd.h>
#include <string>
#include "comm.h"
using namespace std;
int main()
{
start:
int wfd = open(FILENAME, O_WRONLY);
if (wfd < 0)
{
cout << "error: " << errno << ", errstring: " << strerror(errno) << endl;
return 1;
}
cout << "open fifo success !" << endl;
string message;
while (true)
{
cout << "Please Enter# " << endl;
getline(cin, message);
ssize_t s = write(wfd, message.c_str(), message.size());
if (s < 0)
{
cout << "error: " << errno << ", errstring: " << strerror(errno) << endl;
break;
}
}
close(wfd);
cout << "close file success !" << endl;
return 0;
}
1.4 匿名管道与命名管道的区别
匿名管道由pipe函数创建并打开
命名管道由mkfifo函数创建,打开用open()
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义
可以看到,无论是匿名管道还是命名管道其实是复用了文件的接口实现的,所以管道是特殊的文件,只不过这个文件不需要将文件缓冲区的内容写入磁盘中,只需要利用文件缓冲区这个场所,让进程之间进行通信,而两者之间的差别就是看到同一份资源的方式不同,但通信原理是一样的,都是借用了文件的原理
1.5 管道的特征
管道的4种情况:
1. 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据了)
2. 正常情况,如果管道被写满了,写端必须等待,直到有空间为正(读端读走数据)
3. 写端关闭,读端一直读取,读端会读到read返回值为0,表示读到文件结尾
4. 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过向目标进程发送SIGPIPE(13)信号,终止目标进程
管道的5种特性:
1. 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信,常用与父子,仅限于此
2. 匿名管道,默认给读写端要提供同步机制——了解现象就行
3. 面向字节流的——了解现象就行
4. 管道的生命周期是随进程的
5. 管道是单向通信的,半双工通信的一种特殊情况
二、system V
进程间通信,例如上文中的管道,是基于文件的通信方式,只是复用了文件的接口,而为了脱离文件进行进程间通信,设计了SystemV标准的进程间通信方式
2.1 共享内存
共享内存 = 物理内存块 + 共享内存的相关属性
进程间通信的前提:必须让不同的进程看到同一份资源(必须由OS提供)
2.1.1 共享内存接口
shmget函数:
功能:用来创建共享内存
原型:int shmget(key_t key, size_t size, int shmflg);
参数: key—>这个共享内存段名字,size—>共享内存大小,shmflg—>由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码shmid
,失败返回-1
shmflag:通常有两种选项—>IPC_CREAT、IPC_EXCL
IPC_CREAT: shm不存在,就创建,存在就获取并返回
IPC_EXCL: 无法单独使用
IPC_CREAT | IPC_EXCL: shm 不存在就创建,存在,出错返回(保证共享内存是新创建的)
如果shmflag是0默认就是IPC_CREAT
shmat函数:
功能:将共享内存段连接到进程地址空间
原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:shmid—>共享内存标识,shmaddr—>指定连接的地址,shmflg—>它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个字节,失败返回-1
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍,公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数:
功能:将共享内存段与当前进程脱离
原型:int shmdt(const void *shmaddr);
参数:shmaddr—> 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl函数:
功能:用于控制共享内存
原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:shmid—>由shmget返回的共享内存标识码,cmd—>将要采取的动作(有三个可取值),buf—>指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0,失败返回-1
只需要了解删除共享内存段IPC_RMID
2.1.2 共享内存原理
借助共享内存接口,在物理内存中申请一块空间,通过页表映射,映射到进程地址空间的共享区中,双方借助key来找到这块在物理内存中申请的空间,实现通信双方看到同一块资源,进行进程间通信
OS一定会允许系统中同时存在多个共享内存,肯定需要进行管理,如何进行管理?
先描述,再组织
所以我们在申请共享内存的时候OS势必会创建struct shm
进行维护管理,就像进程的PCBtask_struct
,文件的struct file
一样
如何保证第二个之后的参与通信的进程,看到的就是同一个共享内存呢?
要求共享内存必须有唯一的标识,而且在
struct shm
结构体中肯定记录了标识符,方便找到对应的共享内存进行管理
怎么做到有唯一的标识符?怎么给另一个进程?
通过
key_t ftok( const char *pathname,int proj_id);
函数获取key
参数:pathname—>路径名,proj_id—>项目id
(实际上pathname和proj_id的值是多少并不重要,ftok的工作原理就像哈希函数一样,结合这两个值通过某种转换方式转换成一个标识符,并且pathname和proj_id不变,生成的标识符就不变)
返回值:如果创建成功返回一个共享内存的标识符,如果失败则返回-1
关于怎么给另一个进程,这个问题暂时无法回答,需要后续的学习
关于标识符key:
key是共享内存的标识符,在实际中肯定会有很多的共享内存,而每个共享内存都要被OS管理起来,共享内存 = 物理内存块 + 共享内存的相关属性,描述共享内存时就有一个字段
struct shm
其中就有key属性, 一个进程创建内存块后把key值写进相关属性中,而另一个进程拿着key值遍历相关属性查找,这样就完成了两个进程共享一块内存块,看到同一份资源
key与shmid:
key是我们在调用shmget的时候传入的参数,而shmget的返回值是shmid,等于说我们用key得到了一个shmid,所以key和shmid都是标识内存块的,key是在内核标识唯一性,而shmid是在用户层标识唯一性,这样即使操作系统有什么变化也不会影响用户使用,充分的解耦,他们的关系就类似于fd与inode
2.1.3 查看IPC资源
共享内存的生命周期不是随着进程的,而是随着OS的,这也是所有system V进程间通信的特征
ipcs -m
查看共享内存
ipcs -q
查看消息队列
ipcs -s
查看信号量
ipcs
三个一起查看
ipcrm -m shmid
删除共享内存
删除共享内存是用shmid,不是用key,上文中提到了key是在内核标识唯一性,而shmid是在用户层标识唯一性,命令行操作实际上是bash进程帮我们完成指令任务,所以用shmid
2.1.4 共享内存实现server端和client端通信
共享内存不像管道一样,具有一定的同步机制(其实管道有同步机制一定是OS对管道实现了同步规则),而共享内存没有,所以单纯的共享内存server端和client端在读写的时候根本不会等待对方有没有就绪,没有同步机制,会一直读,造成资源浪费,如何解决?
可以借助命名管道来实现阻塞,从而为共享内存实现同步机制
借助信号量,这种方法需要后续的学习
下面代码借助管道实现了共享内存的同步机制,并且进行了一定的封装
comm.hpp
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
#define PATHNAME "/home/hs/code/linux-warehouse/shm"
#define PROJ_ID 0x112233
#define SHMSIZE 4096
#define FILENAME "fifo"
key_t GetKey()
{
key_t key = ftok(PATHNAME, PROJ_ID);
if (key < 0)
{
cout << "error: " << errno << ", errstring: " << strerror(errno) << endl;
exit(1);
}
return key;
}
int CreateShmHelper(key_t key, int flag)
{
int shmid = shmget(key, SHMSIZE, flag);
if (shmid == -1)
{
cout << "error: " << errno << ", errstring: " << strerror(errno) << endl;
exit(2);
}
return shmid;
}
// IPC_CREAT | IPC_EXCL 不存在就创建,存在就出错返回
// server端保证每次的共享内存都是新的
// bug——0644必须带上,否则无法运行
int CreateShm(key_t key)
{
return CreateShmHelper(key, IPC_CREAT | IPC_EXCL | 0644);
}
// IPC_CREAT 不存在就创建,存在就获取并返回
int GetShm(key_t key)
{
return CreateShmHelper(key, IPC_CREAT);
}
string ToHex(int key)
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
bool Makefifo()
{
int n = mkfifo(FILENAME, 0666);
if (n == 0)
{
return true;
}
else
{
return false;
}
}
server.cc
#include "comm.hpp"
class Init
{
public:
Init()
{
// 创建管道文件
if (!Makefifo())
{
exit(3);
}
// 创建共享内存
key = GetKey();
cout << "key: " << ToHex(key) << endl;
// sleep(3);
shmid = CreateShm(key);
cout << "shmid: " << shmid << endl;
cout << "将shm映射到进程地址空间" << endl;
// sleep(3);
s = (char *)shmat(shmid, nullptr, 0);
fd = open(FILENAME, O_RDONLY);
}
~Init()
{
close(fd);
unlink(FILENAME);
cout << "将shm从进程地址空间移除" << endl;
// sleep(3);
shmdt(s);
cout << "将shm从OS中删除" << endl;
// sleep(3);
shmctl(shmid, IPC_RMID, nullptr);
}
public:
key_t key;
int shmid;
int fd;
char *s;
};
int main()
{
Init init;
while (true)
{
// 实现server端同步
int flag = 0;
ssize_t n = read(init.fd, &flag, sizeof(flag));
if (n > 0)
{
// 从共享内存中读取数据
cout << "client: " << init.s << endl;
sleep(2);
}
else if (n == 0)
{
break;
}
}
return 0;
}
client.cc
#include "comm.hpp"
int main()
{
key_t key = GetKey();
int shmid = GetShm(key);
char *s = (char *)shmat(shmid, nullptr, 0);
cout << "将shm映射到进程地址空间" << endl;
int fd = open(FILENAME, O_WRONLY);
//cout << "open success" << endl;
// 将信息写入共享内存
char i = 'a';
for (; i <= 'z'; i++)
{
s[i - 'a'] = i;
cout << "write: " << i << endl;
// 实现client端同步
int flag = 1;
ssize_t n = write(fd, &flag, sizeof(flag));
sleep(2);
}
cout << "将shm从进程地址空间移除" << endl;
// sleep(3);
shmdt(s);
close(fd);
return 0;
}
2.1.5 共享内存的特性
1. 共享内存是所有的进程间通信速度最快的
2. 共享内存可以提供较大的空间
3. 共享内存的生命周期是随OS的,而不是随进程的,这是所有System V进程间通信的共性
4. 共享内存不提供任何同步或者互斥机制,共享内存是直接裸露给所有的使用者的,
不提供不代表不需要,所以需要程序员自行保证数据的安全,这也造成了共享内存在多进程中是不太安全的
为什么说共享内存的通信速度是最快的?
凡是数据迁移,都是拷贝!
管道:
char buffer[1024];
snprintf(buffer,sizeof(buffer),"hello linux");
write(pipefd(1),buffer,strlen(buffer));
如果用管道通信,从写端到读端,最起码需要四次拷贝,这里是直接写入内核缓冲区,如果考虑用户缓冲区,则是最少六次拷贝
共享内存:
写端只需要将内容直接写入共享内存区,读端只需要直接从共享内存区读取,只需要两次拷贝,所以共享内存最快
2.2 消息队列
消息队列 = 队列 + 队列的属性
2.2.1 消息队列的概念
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法,读端和写端共用一个队列,每个数据块就是队列的一个节点,每个数据块都会有个记录类型的数据,来判断该数据块该被哪个进程读取
2.2.2 消息队列接口
消息队列的获取msgget函数:
int msgget(key_t key, int msgflg);
返回值:msgget函数返回的一个有效的消息队列标识符
与shmget函数的用法一致,这里不过多介绍
消息队列发送数据msgsnd函数:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数介绍:
msqid:消息队列的用户级标识符。
msgp:指向待发送的数据块结构体的指针(输出型参数,数据块格式如下)
mtype:程序员定义,代表这个数据块是谁的
#define ATYPE 1
#define BTYPE 2
msgsz:表示所发送数据块的大小
msgflg:表示发送数据块的方式,一般默认为0即可
参数:成功返回0,失败返回-1
消息队列获取msgrcv函数:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数与msgsnd一致
消息队列的控制msgctl函数
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid:消息队列的标识符
参数跟shmctl一样
2.2.3 消息队列细节
队列的生命周期随内核
消息队列 = 队列 + 队列的属性
消息队列,系统中可不可以同时存在多个消息队列?
可以,消息队列也要在内核中,被管理起来,如何管理呢?先描述,再组织
2.3 信号量
2.3.1 信号量概念
信号量的本质是一把计数器,它统计的是公共资源资源还剩多少
公共资源
可以被多个进程同时访问的资源,而如果访问没有被保护的公共资源,就会导致数据不一致问题(一个进程还在写的时候另一个进程就开始读),所以公共资源需要保护,被保护起来的公共资源称为临界资源,反之称为非临界资源,而临界资源的那部分代码称为临界区,非临界资源的代码就称为非临界区
解决一个问题必然伴随着一个新的问题的产生!
为了让进程间通信—>多个执行流看到的同一份资源,公共资源—>并发访问—>数据不一致的问题—>保护起来—>互乐和同步
同步和互斥
互斥:任何一个时刻只允许一个执行流(进程)访问公共资源,当这个进程访问完了,下一个进程才能访问,加锁完成,用户:共享内存
同步:多个执行流执行的时候,按照一定的顺序执行,OS:匿名命名,管道,消息队列
原子性
要么不做,要么做完,只有这两种状态的情况
2.3.2 信号量接口
目前简单了解下即可,主要是知道有信号量这个概念
2.3.3 理解信号量
看电影的例子
电影院和内部座位,就是多个人共享的资源—公共资源
我们买票的本质是对资源的预订机制
信号量表示公共资源的个数(可能会被拆分为多份资源),就像电影院里面座位的数量
信号量表示资源数目的计数器,每一个执行流想访问公共资源内的谋一份资源,不应该让执行流直接访问,而是先申请信号量资源,其实就是先对信号量计数器进行
--
操作,本质上,只要--
成功,就完成了对资源的预订机制
如果申请不成功?
执行流被挂起阻塞
假设电影院里面有VIP厅,一次只有一个用户使用,这种情况就是二元信号量,互斥锁,完成互斥功能
2.3.4 信号量细节
信号量的引入,意味着每个进程都得先看到同一个信号量资源,就只能由OS提供了,使用IPC体系(看下文)
既然每个进程都得先看到同一个信号量资源,所以信号量本质也是公共资源
这样的信号量资源必然有多个,OS也势必要将其管理起来,struct sem
其中必然有int count
,也有阻塞队列——将申请不成功的进程链入其中
2.4 IPC资源的维护方式
我们观察发现,无论是共享内存,消息队列,信号量,他们的接口都非常相似
对应的
xxxctl()
接口中都有一个struct xxxid ds *buf
的参数
这个参数是一个输出型参数,并且在库中存在对于这三个结构体的定义,struct shmid_ds
,struct msgid_ds
,struct semid_ds
,也就是说我们可以直接创建对应的临时变量
这些结构体中包含的内容就有该IPC资源的部分属性(内核让我们看到的,还有部分权限不足内核不允许用户查看),是用来管理对应的IPC资源的,例如共享内存的大小,链接数,创建的pid等等
在调用xxxctl()
时可以传入对应的变量拿到我们想要属性值,也可以对其进行修改
在struct shmid_ds
,struct msgid_ds
,struct semid_ds
结构体中的首部都有一个叫struct ipc_perm shm_perm;
的结构体
我们看看struct ipc_perm shm_perm;
这个结构体中的内容
key: 用于创建共享内存段的键值,通常由ftok()函数生成
uid (所有者用户ID): 拥有共享内存段的用户的用户ID
gid (所有者组ID): 拥有共享内存段的用户的主要组的组ID
cuid (创建者用户ID): 创建共享内存段的用户的用户ID
cgid (创建者组ID): 创建共享内存段的用户的主要组的组ID
mode (权限模式): 共享内存段的访问权限,类似于文件的权限,包括读、写和执行权限,但通常只使用读和写权限
seq (序列数): 一个序列数,用于唯一标识一个IPC资源,与IPC资源的key一起使用
看到这个结构,你想到了什么?
多态!
struct ipc_perm
就像是一个基类,而struct shmid_ds
,struct msgid_ds
,struct semid_ds
则是继承了基类的子类
这种结构可以看作是C语言版本的多态,利用柔性数组,在堆上动态开辟空间(malloc)
在内核中,OS肯定是要管理IPC资源的,无论是共享内存,消息队列,信号量,只需要将其对应的
struct ipc_perm
链入struct ipc_id_ary
中的柔性数组中,管理IPC只需要维护这个数组即可!
未来进程需要查找或者链接对应的IPC资源,只需要拿着key依次遍历这个数组就能找到对应的struct ipc_perm
的地址
又由于struct ipc_perm
是在struct xxxid_ds
的第一个变量,也就是struct ipc_perm
的首地址就是struct xxxid_ds
的首地址
所以只需要把在数组中找到的struct ipc_perm
的地址强转成struct xxxid_ds *
,这样就能找到对应的管理IPC资源的结构体了