Linux系统与系统编程(12)——进程间通信

📅 2026/7/27 9:23:59 👁️ 阅读次数 📝 编程学习
Linux系统与系统编程(12)——进程间通信

前言

欢迎观看Linux系列文章!!第12篇主要讲述了管道systemV共享内存其他systemV标准的IPC原理

管道

目的

数据传输:一个进程需要将它的数据发送给另一个进程
资源进程:多个进程之间共享同样的资源
通知时间:一个进程需要向另一个或一组进程发送消息,通知发生了某种时间(如进程终止时要通知父进程)
进程控制:有些进程希望完成控制另一个进程的执行(如debug过程)

什么是管道?

最古老的进程间通信的形式。它是从一个进程连接到另一个进程的一个数据流。

匿名管道

匿名管道(Anonymous Pipe)是操作系统提供的一种单向、无名称的进程间通信(IPC)机制,主要用于父子进程之间具有亲缘关系的进程间传输数据。

#include<unistd.h> int pipe(int fd[2]);

fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示为写端
返回值:成功返回0,失败返回错误代码。

管道是字节流缓冲,写入的数据会先进入内核缓冲区

父子单向通信


如图即可完成父子间单向通信,如果要进行双向通信,那就建立两个管道即可。因为子进程和父进程共享代码,而且会继承父进程的文件描述符表,所以可以保证打开同一个管道文件。

管道文件没有文件名,是内存级的,匿名的。下文还有命名管道。是命名的。

示例代码:

#include<iostream> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> #include<string.h> using namespace std; void ChildWrite(int wfd) { char buffer[1024]; int cnt = 0; while(true) { snprintf(buffer, sizeof(buffer), "I am a Child, pid:%d, cnt: %d", getpid(), cnt++); write(wfd, buffer, strlen(buffer)); sleep(1); } } void FatherRead(int rfd) { char buffer[1024]; while(true) { buffer[0] = 0; //read读完之后,在下一次读的时候会阻塞等待 //这是管道文件自带的同步机制 ssize_t n = read(rfd, buffer, sizeof(buffer) - 1); if(n > 0) { buffer[n] = 0; cout << "child say:" << buffer << endl; } } } int main() { //创建管道 int fds[2] = {0}; int n = pipe(fds); if(n < 0) { cerr << "pipe error" << endl; return 1; } cout << "fds[0]:" << fds[0] << endl; cout << "fds[1]:" << fds[1] << endl; //创建子进程 pid_t id = fork(); if(id == 0) { //关闭不需要的读写端,形成通信信道 //父进程读,子进程写 close(fds[0]); ChildWrite(fds[1]); close(fds[1]); exit(0); } //关闭不需要的读写端,形成通信信道 //父进程读,子进程写 close(fds[1]); FatherRead(fds[0]); waitpid(id, nullptr, 0); close(fds[0]); return 0; }

根据上述代码,我们可以得到匿名管道的一些性质:

①匿名管道只能用来进行具有血缘关系和亲缘关系的进程间通信(父子,兄弟皆可)。
②管道文件,自带同步机制。
③管道是面向字节流的。

void ChildWrite(int wfd) { char buffer[1024]; int cnt = 0; while(true) { snprintf(buffer, sizeof(buffer), "I am a Child, pid:%d, cnt: %d", getpid(), cnt++); write(wfd, buffer, strlen(buffer)); printf("child:%d\n", cnt); } } void FatherRead(int rfd) { char buffer[1024]; while(true) { sleep(5); buffer[0] = 0; ssize_t n = read(rfd, buffer, sizeof(buffer) - 1); if(n > 0) { buffer[n] = 0; cout << "child say:" << buffer << endl; sleep(2); } } }

这里修改了代码,把原本“读得快,写的慢”的情况变为“读得慢,写得快”,运行后可以发现,无论写得快还是慢,不会影响读操作,而如果写满了管道文件,写端就会阻塞。
学习完Linux网络后可以更深刻的理解什么是面向字节流,这里不多说。
④管道是只能单向通信的,属于半双工的一个特殊情况。
半双工:任何时刻,一个发一个收;
全双工:任何时刻,可以同时发和收。
⑤(管道)文件的生命周期是随进程的。

同时,使用管道通信有4种情况:

❶写得慢,读得快——读端(进程)阻塞。
❷写得快,读得慢——写满了,写端阻塞等待,读端读取之后再覆盖写。
❸写端关闭,读端继续读,直到读完。
❹读端关闭,写端继续写是没有意义的,所以OS会发送信号13,杀掉写端进程,避免浪费资源。

管道容量

void ChildWrite(int wfd) { char c = 0; int cnt = 0; while(true) { //snprintf(buffer, sizeof(buffer), "I am a Child, pid:%d, cnt: %d", getpid(), cnt++); write(wfd, &c, 1); printf("child:%d\n", ++cnt); } } void FatherRead(int rfd) { char buffer[1024]; while(true) { sleep(100); buffer[0] = 0; ssize_t n = read(rfd, buffer, sizeof(buffer) - 1); if(n > 0) { buffer[n] = 0; cout << "child say:" << buffer << endl; sleep(2); } } }

用以上代码可以测试出管道文件的容量大小

所以容量大小为65536字节,也就是64KB(不同的操作系统可能会不一样)。

基于匿名管道——进程池


利用管道读写端的阻塞,来暂停和唤醒子进程。同时通过管道传输一个任务码,不同的任务码对应不同的任务,子进程接收任务码后执行对应任务,这种工作叫作池化技术,这幅图就是进程池。

可以减少创建某些资源的成本,提高访问效率。

示例代码:
ProcessPool.hpp

#ifndef __PROCESS_POOL_HPP__ #define __PROCESS_POOL_HPP__ #include <iostream> #include <cstdlib> // stdlib.h stdio.h -> cstdlib cstdio #include <vector> #include <unistd.h> #include <sys/wait.h> #include "Task.hpp" class Channel { public: Channel(int fd, pid_t id) : _wfd(fd), _subid(id) { _name = "channel-" + std::to_string(_wfd) + "-" + std::to_string(_subid); } ~Channel() { } void Send(int code) { int n = write(_wfd, &code, sizeof(code)); (void)n; // ? } void Close() { close(_wfd); } void Wait() { pid_t rid = waitpid(_subid, nullptr, 0); (void)rid; } int Fd() { return _wfd; } pid_t SubId() { return _subid; } std::string Name() { return _name; } private: int _wfd; pid_t _subid; std::string _name; // int _loadnum; }; class ChannelManager { public: ChannelManager() : _next(0) { } void Insert(int wfd, pid_t subid) { _channels.emplace_back(wfd, subid); // Channel c(wfd, subid); // _channels.push_back(std::move(c)); } Channel &Select() { auto &c = _channels[_next]; _next++; _next %= _channels.size(); return c; } void PrintChannel() { for (auto &channel : _channels) { std::cout << channel.Name() << std::endl; } } void StopSubProcess() { for (auto &channel : _channels) { channel.Close(); std::cout << "关闭: " << channel.Name() << std::endl; } } void WaitSubProcess() { for (auto &channel : _channels) { channel.Wait(); std::cout << "回收: " << channel.Name() << std::endl; } } ~ChannelManager() {} private: std::vector<Channel> _channels; int _next; }; const int gdefaultnum = 5; class ProcessPool { public: ProcessPool(int num) : _process_num(num) { _tm.Register(PrintLog); _tm.Register(Download); _tm.Register(Upload); } void Work(int rfd) { while (true) { int code = 0; ssize_t n = read(rfd, &code, sizeof(code)); if (n > 0) { if (n != sizeof(code)) { continue; } std::cout << "子进程[" << getpid() << "]收到一个任务码: " << code << std::endl; _tm.Execute(code); } else if (n == 0) { std::cout << "子进程退出" << std::endl; break; } else { std::cout << "读取错误" << std::endl; break; } } } bool Start() { for (int i = 0; i < _process_num; i++) { //创建管道 int pipefd[2] = {0}; int n = pipe(pipefd); if (n < 0) return false; //创建子进程 pid_t subid = fork(); if (subid < 0) return false; else if (subid == 0) { //子进程 //关闭不需要的文件描述符 close(pipefd[1]); Work(pipefd[0]); //?? close(pipefd[0]); exit(0); } else { //父进程 //关闭不需要的文件描述符 close(pipefd[0]); // 写端:pipefd[1]; _cm.Insert(pipefd[1], subid); // wfd, subid } } return true; } void Debug() { _cm.PrintChannel(); } void Run() { //选择一个任务 int taskcode = _tm.Code(); //选择一个信道[子进程],负载均衡的选择一个子进程,完成任务 auto &c = _cm.Select(); std::cout << "选择了一个子进程: " << c.Name() << std::endl; //发送任务 c.Send(taskcode); std::cout << "发送了一个任务码: " << taskcode << std::endl; } void Stop() { //关闭父进程所有的wfd即可 _cm.StopSubProcess(); //回收所有子进程 _cm.WaitSubProcess(); } ~ProcessPool() { } private: ChannelManager _cm; int _process_num; TaskManager _tm; }; #endif

Task.hpp

#pragma once #include <iostream> #include <vector> #include <ctime> typedef void (*task_t)(); void PrintLog() { std::cout << "我是一个打印日志的任务" << std::endl; } void Download() { std::cout << "我是一个下载的任务" << std::endl; } void Upload() { std::cout << "我是一个上传的任务" << std::endl; } class TaskManager { public: TaskManager() { srand(time(nullptr)); } void Register(task_t t) { _tasks.push_back(t); } int Code() { return rand() % _tasks.size(); } void Execute(int code) { if(code >= 0 && code < _tasks.size()) { _tasks[code](); } } ~TaskManager() {} private: std::vector<task_t> _tasks; };

Main.cpp

#include "ProcessPool.hpp" int main() { // 创建进程池对象 ProcessPool pp(gdefaultnum); // 启动进程池 pp.Start(); // 自动派发任务 int cnt = 10; while(cnt--) { pp.Run(); sleep(1); } // 回收,结束进程池 pp.Stop(); return 0; }

进程池的代码存在一个问题,当父子进程分别把自己的读和写端关闭之后,形成了单向通信。当父进程创建了新的子进程,他会继承父亲的文件描述符表,其中包括了父亲的写端。所以,在父亲关闭了自己的写端时,管道文件还有之后创建的子进程的写端指向,所以子进程read会一直阻塞而不是返回0。

解决方法:
指定回收顺序,根据创建时间,从后往前回收。
或者关闭子进程的写端即可,使得只有父进程有文件描述符指向管道文件的写端。

命名管道

当两个进程之间没有关联时,匿名管道就不起作用了,这里就引入命名管道。

#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode);

mkfifo函数可创建一个命名管道文件。
成功返回0,失败返回-1并设置错误码

命名管道依旧是单向的,使用逻辑和匿名管道没有本质区别。
写端将内容写入文件,读端将内容从管道文件读出来。

示例代码:
comm.hpp

#pragma once #include <iostream> #include <string> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #define FIFONAME "fifo" #define PATH "." using namespace std; class NamedFifo { public: NamedFifo(const string &path, const string &name) : _path(path), _name(name) { // 新建管道 _fifoname = _path + "/" + _name; umask(0); int n = mkfifo(_fifoname.c_str(), 0666); if (n < 0) { cerr << "mkfifo error" << endl; } else { cout << "mkfifo success" << endl; } } ~NamedFifo() { // 删除管道 int n = unlink(FIFONAME); if (n == 0) { cout << "remove fifo success" << endl; } else { cout << "remove fifo failed" << endl; } } private: string _path; string _name; string _fifoname; }; class FileOper { public: FileOper(const string &path, const string &name) : _path(path), _name(name) { _fifoname = _path + "/" + _name; } // 打开文件 void OpenForRead() { _fd = open(_fifoname.c_str(), O_RDONLY); if (_fd < 0) { cerr << "open fifo error" << endl; } cout << "open fifo success" << endl; } // 写入文件 void OpenForWrite() { _fd = open(_fifoname.c_str(), O_WRONLY); if (_fd < 0) { cerr << "open fifo error" << endl; } cout << "open fifo success" << endl; } void Write() { // 写入操作 string message; int cnt = 1; pid_t id = getpid(); while (true) { cout << "Please Enter# "; getline(cin, message); message += (", message number: " + to_string(cnt++) + ",[" + to_string(id) + "]"); write(_fd, message.c_str(), message.size()); } } void Read() { while (true) { char buffer[1024]; int number = read(_fd, buffer, sizeof(buffer) - 1); if (number > 0) { buffer[number] = 0; cout << "Client Say# " << buffer << endl; ; } else if (number == 0) { cout << "Client quit! Me, too!" << endl; break; } else { cerr << "read error" << endl; break; } } } void Close() { if (_fd > 0) { close(_fd); } } ~FileOper() { } private: string _path; string _name; string _fifoname; int _fd; };

server.cpp

#include"comm.hpp" using namespace std; int main() { //创建管道文件 NamedFifo fifo(".", "fifo"); //文件操作 FileOper readerfile(".", "fifo"); readerfile.OpenForRead(); readerfile.Read(); readerfile.Close(); }

client.cpp

#include"comm.hpp" using namespace std; int main() { FileOper writerfile(PATH, FIFONAME); writerfile.OpenForWrite(); writerfile.Write(); writerfile.Close(); return 0; }

system V共享内存

system V其实是一个标准,Linux内核支持了这种标准,专门设计了一个IPC通信模块。
而读写共享内存,并没有调用系统调用。因为共享区属于用户空间,可以让用户直接使用。

原理

内核在物理内存中划出一块区域,让进程A和进程B在自己的虚拟地址空间中都映射到同一块物理内存上。数据直接由进程A写入这块内存,进程B即可直接读取,整个过程无需内核介入,也无需数据拷贝。这一块内存就是共享内存。取消两进程的关联关系之后,共享内存就会自行释放。

可能会同时存在多组进程,都在使用不同的共享内存来通信,所以需要对共享内存进行管理。

同样是先描述后组织,在底层会使用结构体,链表等存储共享内存的信息,如哪些进程在用,大小,位置等等。

而共享内存是进程间通信中,速度最快的方式:
1.映射之后,读写操作可以直接被对方看到
2.不需要进行系统调用来获取或者写入内容

但是也有对应的缺点,就是通信双方没有同步机制,无法满足数据一致性。

接口

shmget——创建共享内存

#include<sys/ipc.h> #include<sys/shm.h> int shmget(key_t key, size_t size, int shmflg);

参数:

size:共享内存的大小。
shmflg:标志位选项。
IPC_CREAT:创建共享内存,如果目标共享内存不存在,就创建;否则打开已存在的共享内
存并返回。
IPC_CREAT | IPC_EXCL:如果创建的shm不存在,就创建它;否则就报错。
key:不同的共享内存有不同的key来标识它的唯一性。但是它不是由系统形成的,而是需要用户层构建并传入OS。
通信时要让两进程直接约定一个key。可以使用ftok函数形成唯一的key。

#include<sys/types.h> #include<sys/ipc.h> key_t ftok(const char* pathname, int proj_id);

这个函数依旧是用户层的算法,没有深入到内核,他会根据两个参数计算出一个key,这样生成的key重复可能性小。但是也会有可能会冲突,那就修改一下参数就行,生成另一个key。

返回值:

返回一个int类型的共享内存标识符,用来标识共享内存的唯一性。key也能标识,key给内核级使用,共享内存标识符是给用户级使用的。

shmctl——操作共享内存(删除)

创建了共享内存后,如果没有删除共享内存,共享内存就会一直存在。共享内存的生命周期是随内核的,就算进程结束,共享内存的资源也会一直存在。
可以通过指令删除或者代码删除。

ipcs -m --查看共享内存 ipcrm -m [shmid] --释放指定shmid的共享内存
#include<sys/ipc.h> #include<sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds* buf);

参数:

shmid:指定要操作的共享内存shmid。
cmd:操作共享内存的命令。如IPC_RMID,释放删除指定共享内存。
buf:用于保存描述共享内存的相关属性。

shmat——挂接共享内存

#include<sys/ipc.h> #include<sys/shm.h> int shmat(int shmid, const void* shmaddr, int shmflg);

参数:

shmid:指定要操作的共享内存shmid。
shmaddr:虚拟地址,向固定地址开始进行挂接(一般让系统自行设置,用户传NULL即可)。
shmflg:设置共享内存段的访问权限和连接方式,填0为使用默认设置,即可读可写。也可以使用SHM_RDONLY,即只读方式。

shmdt——断联共享内存

#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);

shmaddr:shmat挂接时要返回的那个指向共享内存的地址

维护共享内存

因为共享内存没有同步机制,所以程序员需要主动维护共享内存来保护数据一致性。

实现局部保护的一种方法就是使用命名管道。这种办法只是简单的模拟进行同步,之后还会学习更多的同步机制。

可见下文示例代码。

示例代码:
Shm.hpp

#pragma once #include <iostream> #include <cstdio> #include <string> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <unistd.h> #include "Comm.hpp" const int gdefaultid = -1; const int gsize = 4096; const std::string pathname = "."; const int projid = 0x66; const int gmode = 0666; #define CREATER "creater" #define USER "user" class Shm { private: // 创建的一定要是一个全新的共享内存 void CreateHelper(int flg) { printf("key: 0x%x\n", _key); // 共享内存的生命周期,随内核 _shmid = shmget(_key, _size, flg); if (_shmid < 0) { ERR_EXIT("shmget"); } printf("shmid: %d\n", _shmid); } void Create() { CreateHelper(IPC_CREAT | IPC_EXCL | gmode); } void Attach() { _start_mem = shmat(_shmid, nullptr, 0); if ((long long)_start_mem < 0) { ERR_EXIT("shmat"); } printf("attach success\n"); } void Detach() { int n = shmdt(_start_mem); if (n == 0) { printf("detach success\n"); } } void Get() { CreateHelper(IPC_CREAT); } void Destroy() { // if (_shmid == gdefaultid) // return; Detach(); if (_usertype == CREATER) { int n = shmctl(_shmid, IPC_RMID, nullptr); if (n > 0) { printf("shmctl delete shm: %d success!\n", _shmid); } else { ERR_EXIT("shmctl"); } } } public: Shm(const std::string &pathname, int projid, const std::string &usertype) : _shmid(gdefaultid), _size(gsize), _start_mem(nullptr), _usertype(usertype) { _key = ftok(pathname.c_str(), projid); if (_key < 0) { ERR_EXIT("ftok"); } if (_usertype == CREATER) Create(); else if (_usertype == USER) Get(); else { } Attach(); } void *VirtualAddr() { printf("VirtualAddr: %p\n", _start_mem); return _start_mem; } int Size() { return _size; } void Attr() { struct shmid_ds ds; int n = shmctl(_shmid, IPC_STAT, &ds); // ds:输出型参数 printf("shm_segsz: %ld\n", ds.shm_segsz); printf("key: 0x%x\n", ds.shm_perm.__key); } ~Shm() { std::cout << _usertype << std::endl; if(_usertype == CREATER) Destroy(); } private: int _shmid; key_t _key; int _size; void *_start_mem; std::string _usertype; };

Client.cpp

#include"Shm.hpp" #include"Fifo.hpp" using namespace std; int main() { FileOper writerfile(PATH, FIFONAME); writerfile.OpenForWrite(); Shm shm(pathname, projid, USER); char* mem = (char*)shm.VirtualAddr(); int index = 0; for(char c = 'A'; c <= 'Z'; c++, index += 2) { mem[index] = c; sleep(1); mem[index+1] = c; sleep(1); mem[index+2] == '\0'; writerfile.Wakeup(); } writerfile.Close(); return 0; }

Server.cpp

#include "Shm.hpp" #include "Fifo.hpp" int main() { Shm shm(pathname, projid, CREATER); // sleep(5); shm.Attr(); NamedFifo fifo(PATH, FIFONAME); // 文件操作了 FileOper readerfile(PATH, FIFONAME); readerfile.OpenForRead(); char *mem = (char *)shm.VirtualAddr(); // 我们读写共享内存,有没有使用系统调用??也没有!! while (true) { if (readerfile.Wait()) { printf("%s\n", mem); } else break; } readerfile.Close(); std::cout << "server end normal!" << std::endl; // server段的析构函数没有被成功调用! return 0; }

Fifo.hpp

#pragma once #include <iostream> #include <cstdio> #include <string> #include <iostream> #include <string> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include "Comm.hpp" #define PATH "." #define FIFONAME "fifo" class NamedFifo { public: NamedFifo(const std::string &path, const std::string &name) : _path(path), _name(name) { _fifoname = _path + "/" + _name; umask(0); // 新建管道 int n = mkfifo(_fifoname.c_str(), 0666); if (n < 0) { ERR_EXIT("mkfifo"); } else { std::cout << "mkfifo success" << std::endl; } } ~NamedFifo() { // 删除管道文件 int n = unlink(_fifoname.c_str()); if (n == 0) { // ERR_EXIT("unlink"); // bug在这里,先析构fifo,导致shm的析构没有被调用 } else { std::cout << "remove fifo failed" << std::endl; } } private: std::string _path; std::string _name; std::string _fifoname; }; class FileOper { public: FileOper(const std::string &path, const std::string &name) : _path(path), _name(name), _fd(-1) { _fifoname = _path + "/" + _name; } void OpenForRead() { // 打开, write 方没有执行open的时候,read方,就要在open内部进行阻塞 // 直到有人把管道文件打开了,open才会返回! _fd = open(_fifoname.c_str(), O_RDONLY); if (_fd < 0) { ERR_EXIT("open"); } std::cout << "open fifo success" << std::endl; } void OpenForWrite() { // write _fd = open(_fifoname.c_str(), O_WRONLY); if (_fd < 0) { ERR_EXIT("open"); } std::cout << "open fifo success" << std::endl; } void Wakeup() { // 写入操作 char c = 'c'; int n = write(_fd, &c, 1); printf("尝试唤醒: %d\n", n); } bool Wait() { char c; int number = read(_fd, &c, 1); if(number > 0) { printf("醒来: %d\n", number); return true; } return false; } void Close() { if (_fd > 0) close(_fd); } ~FileOper() { } private: std::string _path; std::string _name; std::string _fifoname; int _fd; };

Comm.hpp

#pragma once #include <cstdio> #include <cstdlib> #define ERR_EXIT(m) \ do \ { \ perror(m); \ exit(EXIT_FAILURE); \ } while (0)

共享内存注意事项

1️⃣共享内存的大小必须是4KB的整数倍,就算你查询共享内存大小时返回的是4097B,实际上的大小是4KB * 2。4097B只是你需要的空间大小,系统开辟时会向上4KB取整。

其他进程通信方式(了解即可)

systemV消息队列

提供了一种,一个进程向另一个进程发送有类型数据块的方式。OS内部一样要对这个队列进行管理(先描述,后组织)。由于系统中可能存在多个消息队列,为了保证两进程之间使用的是同一个消息队列,需要用到key(同共享内存)。

接口

和共享内存很相似,因为都是systemV标准。

msgget——创建或获取消息队列
#include <sys/msg.h> int msgget(key_t key, int msgflg);

参数:

key:队列的外部通行证。通常用 ftok() 根据路径和字符生成,或者直接传 IPC_PRIVATE(值为0)创建仅父子进程可见的私有队列。
msgflg:权限标志位,按位或组合:
权限位:0600(读写属主)、0666(所有人读写),和文件权限一样。
创建标志:IPC_CREAT(不存在则创建,存在则返回已有ID);IPC_EXCL(与IPC_CREAT并用,如果队列已存在则报错,防止误用旧队列)。

返回值:

成功返回非负整数队列ID(后续所有操作都用这个ID)。失败返回 -1。

内核行为:
如果key对应的队列不存在且指定了IPC_CREAT,内核会分配一个struct msg_queue结构体,初始化等待队列头、计数器,并将引用计数设为1。注意:这个结构体的生命周期独立于创建它的进程。

msgsnd——发送消息
#include<sys/msg.h> int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数:

msqid:msgget成功时返回的队列id。
msgp:指向用户自定义结构体的指针。强制要求:结构体第一个成员必须是 long mtype(消息类型,>0),后面跟着消息正文。

struct mymsg { long mtype; // 必须 > 0 char mtext[100]; // 正文数据 };

msgsz:正文长度(不包括那个long mtype)。内核只拷贝这msgsz个字节。
msgflg:控制阻塞行为,只有一个常用标志 IPC_NOWAIT

阻塞规则:
默认(msgflg=0):如果队列已满(总字节数达到msgmnb上限),进程进入睡眠状态,直到有空间释放或队列被删除。
IPC_NOWAIT:如果队列满,立即返回 -1,errno设为EAGAIN(不阻塞)。

msgrcv——接收消息
#include<sys/msg.h> ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数:

msgsz:用户缓冲区大小。如果消息正文 > msgsz,根据 msgflg 决定行为(见下)。
msgtyp:灵魂参数,决定了取哪条消息:
> 0:取队列中第一个类型等于该值的消息(不一定是队头,内核会遍历查找)。
== 0:取队列队头第一条消息(FIFO)。
< 0:取队列中类型小于等于该绝对值的所有消息中类型值最小的那条(实现优先级调度,数值越小优先级越高)。
msgflg:三个常用标志,可组合:
IPC_NOWAIT:队列为空或找不到匹配消息时,立即返回 -1,errno=EAGAIN。
MSG_EXCEPT:仅当 msgtyp > 0 时有效,表示取类型不等于 msgtyp 的第一条消息。
MSG_NOERROR:如果消息正文长度 > msgsz,截断消息(丢弃超出部分)并返回实际拷贝长度。如果不设此标志,消息过长会直接报错 E2BIG。

返回值:
成功返回实际接收到的正文字节数(不包括mtype字段)。失败返回 -1。

内核行为:
内核会从链表中摘除匹配的消息,释放对应内核内存,再将数据拷贝到用户空间。注意:如果多个进程同时等待相同类型,内核会按进程优先级唤醒,而非按等待时间。

msgctl——控制与销毁
#include<sys/msg.h> int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数:

cmd:命令。
IPC_RMID:立即删除队列。注意:内核会唤醒所有正在等待该队列的发送/接收进程,让它们返回 -1 并设 errno=EIDRM(标识已删除)。
IPC_STAT:获取队列状态信息,填充到 buf 中(包含当前消息数、总字节数、最近发送/接收时间等)。
IPC_SET:修改队列权限(如 msg_perm.uid)或队列大小上限(msg_qbytes,仅超级用户可扩大)。
buf:一段数据,不同的cmd,对应不同作用的buf。
IPC_STAT(查询)时,内核会把队列的当前状态复制到你传入的buf中;
IPC_SET(修改队列权限时),buf相当于你的配置表,提前填充好你的buff,内核会根据的你的配置来设置队列属性。
如下:

buf.msg_perm.uid = 1000; // 修改所有者用户ID buf.msg_perm.gid = 1000; // 修改所有组ID buf.msg_perm.mode = 0644; // 修改读写权限(如去掉写权限) buf.msg_qbytes = 65536; // 修改队列容量上限(仅root可增大)

IPC_RMID:删除队列不需要buf,填入NULL即可。

systemV信号量

前置概念

·共享内存没有同步的保护机制,所以上面我们用管道来帮助实现同步,像这种需要被保护的共享资源,就叫做临界资源。
· 保护的方式就是互斥和同步互斥:任何时刻只允许一个执行流访问临界资源同步:多个执行流按照一定顺序访问临界资源。
·我们把进程中涉及到互斥临界资源的代码程序段叫做临界区。其他区域叫做非临界区。

当程序访问到临界区前,就要加一个互斥锁,锁本身也要共享。锁保护临界区的安全,为了保证锁本身的安全,要让锁的申请必须是原子性的

信号量

原理

本身是一个计数器,用来表示临界资源中,资源的数量多少。
我们把共享内存内部分成多个不同的区域进行部分使用,只要多个进程不访问同一个位置,就可以让它们并发访问临界资源且不出问题

进程要想访问临界资源的一小块,就必须先申请信号量。这么做的本质是对资源的预订。

申请成功时,信号量--,称为P操作;归还资源,信号量++,称为V操作

信号量只有0和1两种状态的信号量称为二元信号量,这就是互斥。除此之外叫做多元信号量。整体使用临界资源时,我们用二元信号量,部分使用临界资源时,我们用多元信号量。

信号量的简单描述:

struct sem { 锁;//后面再详细解释 int count;//计数器,P或V操作会改变这个值,<0 时,则申请失败。 task_struct *waitqueue;//等待队列,申请失败的进程进入该队列,当有资源时再唤醒。 }
与通信的关系

1.先访问信号量P,每个进程都得先看到同一个信号量
2.不是传递数据的,而是通信IPC,通知,同步互斥也算是IPC通信。

信号量的接口和系统调用
ipcs -s

该命令可以获取当前存在的信号量。

ipcrm -s XX

改名了可以删除指定信号量集合。

semget——创建信号量集合
#include<sys/types.h> #include<sys/ipc.h> #include<sys/sem.h> int semget(key_t key, int nsems, int semflg);

参数:

key:同上,通过ftok获取,作用想法,不多赘述(共享内存、消息队列、信号量都用key来表示唯一性,所以在OS中,这三种IPC都当成同一种资源,即systemV IPC)。
nsems:信号量个数,即设置n元信号量。
semflg:标志位,IPC_CREAT(不存在则创建,存在则返回已有信号量集合ID);IPC_EXCL(与IPC_CREAT并用,如果信号量ID已存在则报错,防止误用旧信号量集合)

semctl——控制信号量
#include<sys/types.h> #include<sys/ipc.h> #include<sys/sem.h> int semctl(int semid, int semnum, int cmd, ···);

参数:

semnum:当你要删除时,该参数无效;当你初始化时,该参数表示
cmd:IPC_RMID,删除信号量;SETVAL,初始化信号量。IPC_STAT:获取信号量状态信息,填充到 buf 中。
···:当初始化信号量时,该处传入一个联合体semun。

union semun { int val;//信号量初始值 struct semid_ds *buf; unsigned short *array; struct seminfo *__buf; };
semop——操作信号量
#include<sys/types.h> #include<sys/ipc.h> #include<sys/sem.h> int semop(int semid, struct sembuf *sops, size_t nsops);

参数:

sops:sembuf数组,通过自行配置,指定某个信号量进行P或V操作。
nsops:sembuf数组大小,即对几个信号量进行操作。

sembuf结构体如下:

struct sembuf { unsigned short sem_num;//信号量编号 short sem_op;//信号量操作,-1为P操作,+1为V操作 short sem_flg;//操作标志位 }

补充

systemV底层把共享内存,消息队列和信号量存储时,其结构体的第一个元素都是都以kern_ipc_perm。对于内核来说,他们是一样的,存放在内核ipc_ids中ipc_id_ary的一个柔性数组p[0]中。不同的IPC就将这个p数组中的元素进行强制类型转换为对于的类型。简单的来说,内核在实现systemV标准时,使用了C语言,靠强制类型转换实现了多态,不同类型的IPC作为子类,kern_ipc_perm作为基类。

❤~~本文完结!!感谢观看!!接下来更精彩!!欢迎来我博客做客~~❤