【Linux】进程间通信
目录
一、进程间通信概念
二、进程间通信的发展
三、进程间通信的分类
四、管道
4.1 什么是管道
4.2 匿名管道
4.2 基于匿名管道设计进程池
4.3 命名管道
4.4 用命名管道实现server&client通信
五、system V共享内存
5.1 system V共享内存的引入
5.2 共享内存的原理
5.3 共享内存函数
5.4 使用共享内存的步骤
5.5 基于共享内存的进程间通信示例
5.6 共享内存的特点
5.7 共享内存数据结构
六、简述system V消息队列和system V信号量
6.1 system V消息队列
6.2 system V信号量
七、回顾共享内存数据结构
一、进程间通信概念
进程虽然具有独立性,但是进程和进程之间是可能进行协作的。协作的前提是进程之间可以传递信息,即需要进程间通信。
Linux中进程间通信(Inter-Process Communication,IPC)是指为了协调进程之间的行为,不同进程之间进行信息交换和资源共享的机制。
进程间通信的目的包括:
- 数据传输:允许一个进程将数据发送给另一个进程。
- 资源共享:允许多个进程访问相同的资源,如文件、内存区域等。
- 通知事件:一个进程可以向另一个或一组进程发送消息,通知它(它们)某个事件的发生,如进程终止时通知父进程。
- 进程控制:允许一个进程完全控制另一个进程的执行。例如调试进程需要拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
(“陷入”通常指的是程序的执行被操作系统或其他进程强制暂停,以便处理系统调用或硬件中断。)
进程间通信如何实现呢?之前讲到进程具有独立性,那么A进程的数据要交给B进程,不能直接把A进程的数据直接给B进程,因为A进程访问B进程的内存区域把数据拷贝进去,或者B进程访问A进程的内存区域把数据拷贝出来,这两种都不行,会破坏进程的独立性。所以就需要进程通信时的中间媒介。这样既能保持进程的独立性也能实现进程间通信。因此进程间通信的本质就是让不同的进程看到OS中的同一份资源,从而实现数据的传递和共享。(该资源不能由A/B进程提供,但是能由A/B进程申请)
二、进程间通信的发展
进程间通信的发展经历了以下几个阶段:
- 管道:包括匿名管道(pipe)和命名管道(FIFO)。匿名管道只能用于具有亲缘关系的进程间通信,而命名管道可以用于不具有亲缘关系的进程间通信。
- System V进程间通信:包括System V消息队列、System V共享内存、System V信号量等。这些机制提供了更为复杂的IPC功能,支持多种形式的通信和同步。
- POSIX进程间通信:包括POSIX消息队列、POSIX共享内存、POSIX信号量、互斥量、条件变量、读写锁等。POSIX IPC提供了与System V IPC类似的功能,但具有更好的可移植性。
三、进程间通信的分类
Linux中的进程间通信可以分为以下几类:
- 管道:
匿名管道:用于具有亲缘关系的进程间通信。
命名管道:用于不具有亲缘关系的进程间通信。 - System V IPC:
消息队列:用于进程间传递消息。
共享内存:用于进程间共享内存区域。
信号量:用于进程间同步和互斥。 - POSIX IPC:
消息队列:与System V消息队列类似。
共享内存:与System V共享内存类似。
信号量:与System V信号量类似。
互斥量:用于进程间同步。
条件变量:用于进程间同步。
读写锁:用于进程间同步。
四、管道
4.1 什么是管道
管道(Pipe)是Unix系统中用于进程间通信的一种机制,它允许一个进程的输出直接作为另一个进程的输入。管道是一种单向的通信通道,数据只能从管道的一端流向另一端。
回顾文件系统:
【Linux】文件描述符和重定向-CSDN博客 【Linux】文件系统和软硬链接-CSDN博客
如何做到让不同的进程看到了同一个管道文件?
进程是具有独立性的,一个进程的数据,另一个数据是无法直接拿到的。就连父子进程也会因为修改数据而触发写时拷贝。所以不能通过数据传递(这里指命名的变量),而是使用其他方式。
可执行程序加载到内存时,要创建task_struct,其中包含指向files_struct结构体的指针,在该结构体中有一个fd_array指针数组。当加载一个文件到内存时,会创建struct file ,结构体中会包含文件的inode、方法集、文件缓冲区。并将自己链入到fd_array中。
在上层用户使用某个方法向磁盘写入数据时,会打开文件、得到fd、找到struct inode、文件缓冲区、通过方法集的方法将数据刷新到磁盘。
创建子进程,父进程的task_struct 、flies_struct 都要给子进程拷贝一份(flies_struct属于进程部分的数据),flies_struct是浅拷贝,直接拷贝里面的指针。因此父子进程的fd_array[]指向相同的file。struct file不需要重新拷贝,此时不同的进程看到OS中的同一份资源,父进程只需要向自己的文件缓冲区中写入数据,子进程就可以通过它的文件描述符得到该数据。
打开普通文件就要有路径,最终数据刷新到磁盘上。父进程想要给子进程发消息,如果通过这种把数据写到缓冲区里,再写到磁盘中的方式,效率就太低下了(一般文件缓冲区的数据都要刷新到磁盘)。现在就需要这个文件是一个纯内存级的文件,不需要在磁盘中存在,甚至不需要名字,只要保证父子进程能访问到它即可。这种文件就叫做管道文件,所以管道文件也是纯内存级的文件,不需要向磁盘刷新。不需要名字也不需要路径,所以也叫匿名管道。
管道文件有一个特点:实现了资源共享之后,只允许单向通信。
这种单向传递的通信特征很像日常生活中的管道,所以起名叫做管道。例如家里自来水永远都是自来水公司到家里。
在Unix系统中,管道通常通过命令行中的管道符号('|')来创建。例如在命令行中输入以下命令时:
command1 | command2
命令'command1'的输出会被重定向到管道中,而命令'command2'的输入会从管道中读取。这样,'command1'的输出就会成为'command2'的输入,实现了两个进程之间的数据传递。
除了命令行中的管道,Unix系统还提供了两种类型的管道:匿名管道和命名管道。
4.2 匿名管道
匿名管道(pipe)是在命令行中自动创建的,用于具有亲缘关系的进程间通信,如父进程和子进程。它有一个管道文件描述符,分别对应读端和写端。
匿名管道在创建后不能被其他进程打开。
#include <unistd.h>
功能:创建一匿名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
注:fd是输出型参数,返回读写端对应的fd,用来关掉读/写端。
匿名管道的原理:(这里实现父写子读)
- 把一个文件按读方式和写方式打开。
- 创建子进程时,子进程直接拷贝父进程的文件描述符表。
- 父进程关闭fd[0],留下写端,子进程关闭fd[1],留下读端。
- 就形成了单向通信的管道通路。
1. 为什么最开始时把一个文件按读方式和写方式打开?
因为只保留读端或写端,创建子进程时不能保留单向信道。保留读端和写端,子进程也有读端和写端,再进行适当的关闭。就可以实现单向信道,父进程读子进程写,或者父进程写子进程读。
2. 同一个进程把文件分别进行读打开和写打开,在内存里,文件的内容和属性会存在几份?
只用存在一份。这是因为文件的内容和属性(如权限、所有者、大小、创建和修改时间等)都存储在文件的 inode 结构体中,而 inode 结构体在文件系统中被唯一标识。
3. 同一个进程把文件分别进行读打开和写打开,需要几个struct file结构体?
需要两个struct file结构体。struct file内部有一个字段f_pos ,表示当前的操作位置,相当于文件内部的偏移量。文件读和写打开时,它读位置和写位置不一样。同一个进程把文件分别进行读打开和写打开,需要创建两个struct file结构体,一个用来读取、一个用来写入。只不过这两个struct file结构体会指向同样的一个inode、同一个方法集、同一个缓冲区。
4. 进程结束时,文件会被直接关闭吗?
不会。创建子进程时,由于files_struct是浅拷贝,所以指向相同的struct file结构体。形成管道时父子进程关闭各自的读/写端。struct file 中有一个引用计数的字段f_count,用于跟踪有多少个进程正在使用这个文件。当进程打开该文件时,f_count 会增加;当进程关闭文件时,f_count 会减少。所以进程关闭读/写端的实质是把文件描述符表内部指向struct file 的指针清空,然后依次将引用计数f_count--,此时进程就认为把文件关了,但最后文件是否关闭是由操作系统决定的,要判断f_count是否减到0。最终,是否关闭文件由操作系统决定,它会在所有引用计数减到0时释放与文件相关的资源。
5. 引用计数f_count和硬链接数的不同
硬链接是在磁盘中用来统计有多少文件名和我的文件inode产生映射关系的;但是上面的引用计数f_count是用来记述内核数据结构struct file被多少进程文件描述符表指向的。两者虽然都是引用计数,但引用的场景不同

现让父进程创建一个管道文件,进行父读子写,即父进程关闭写端,子进程关闭读端
#include <iostream>
#include <unistd.h>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>#define MAX_SIZE 1024int main()
{int pipefd[2] = {0};int ret = pipe(pipefd);assert(ret == 0); //防止编译器告警,意料之中的错误用assert,意料之外的错误用if(void)ret;pid_t id = fork();if(id < 0){perror("fork");return 1;}if(id == 0){//子进程写close(pipefd[0]);//关闭读端int n = 5;while(n--){char buffer[MAX_SIZE];snprintf(buffer, sizeof(buffer),"child progress,pid: %d, n: %d\n",getpid(),n);write(pipefd[1], buffer,strlen(buffer));sleep(1);}exit(0);}else{//父进程读close(pipefd[1]);//关闭写端char buffer2[MAX_SIZE];while(true){ssize_t n = read(pipefd[0],buffer2,sizeof(buffer2)-1);if(n > 0){buffer2[n] = 0;std::cout << getpid() << ", child words: "<<buffer2 << std::endl;}else {break;}}}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id){std::cout << "wait success" << std::endl;}return 0;
}

注:
- 系统调用的接口是C语言的,为了更好地适应某些极端场景,可以使用C语言的接口,例如示例中使用了snprintf接口。
- sizeof()-1是为了传递字符串时预留一个\0,虽然大部分场景也会预留\0,甚至字符串截断也会预留\0,但在某些场景还是要sizeof()-1。例如read,它不知道传进来的是二进制还是字符串还是其它类型。\0结尾是字符串的标准,读写文件没有义务在数据后面预留\0,所以需要我们自己预留维护。
a. 管道的4种情况
- 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据)。
- 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)。
- 写端关闭,读端继续读取,它将读到管道中的所有数据,直到read返回值为0, 表示读到文件结尾。
- 读端关闭,写端写入时,OS会直接杀掉写端进程,通过向目标进程发送SIGPIPE(13)信号,终止写端进程。
b. 管道的5种特性
- 匿名管道,可以允许具有血缘关系的进程之间进行进程间通信。(父子、爷孙...)
- 匿名管道,默认给读写端要提供同步机制 --- 了解现象:读端和写端是顺序进行的,它们之间不会同时进行。
- 面向字节流 --- 现象:不关心数据的格式,只关心数据的大小和顺序,按字节一次性将数据获取。管道可以传输任何类型的数据
- 管道的生命周期是随进程的。当创建管道的进程结束时,管道也随之消失。
- 管道是单向通信的,半双工通信的一种特殊情况
补充:如果 read 成功读取数据,它会返回实际读取的字节数。如果 read 调用失败,它将返回 -1 并设置 errno 以指示错误。如果到达文件末尾,read 将返回 0。
例如命令: sleep 1000 | sleep 2000 | sleep 3000
操作系统创建了3个进程,两个管道。
使用管道之后,原本向标准输出输出的内容将重定向到管道文件中。原本从标准输入获得的内容将重定向到从管道文件中获取。
4.2 基于匿名管道设计进程池
进程池的概念:
一个进程可以创建很多进程,通过管道与每个进程相连。正常情况,如果管道没有数据了,读端必须等待,直到有数据为止。这样就可以通过对特定的管道传输数据实现唤醒特定的进程。
创建进程会消耗时间和空间资源,如果要处理一个任务要等到任务来到时再处理,进行创建进程、分配资源,这样就有些耽误时间,如果提前把进程创建好,等任务来到时让已经创建好的进程完成任务,这样就可以节省创建进程的时间。这些提前创建好的进程就叫做进程池。
补充内存池的概念:
调用系统调用是有成本的。调用自己的函数也有成本,所以才有了宏函数、内联函数。调用系统调用时操作系统会做很多事情,比如申请内存,如果内存不足,操作系统就要执行内存管理算法协调内存,释放、调整、置换挂起等等。一次性申请100MB内存比申请十次10MB内存效率更高。在C++标准模板库(STL)中,有一个参数为内存配置器,它是一个模板类,用于指定用于存储容器元素的内存管理策略。它定义了如何分配内存、如何构造新元素、如何释放内存以及如何管理内存池等。在申请内存时它会额外多申请一部分,这样在需要扩容时就可以减少系统调用,这种多申请内存的方法就叫做内存池。
模拟实现进程池
Task.hpp如下:
#pragma once#include <iostream>
#include <vector>
#include <unistd.h>
#include <functional>
#include <ctime>typedef std::function<void()> task_t;
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;// 任务集合std::vector<task_t> tasks;public:Init(){tasks.push_back(Download);tasks.push_back(PrintLog);tasks.push_back(PushVideoStream);srand(time(nullptr) ^ getpid());}// 检查任务码bool CheckCode(int code){if (code >= 0 && code < tasks.size())return true;elsereturn false;}// 运行任务void RunTask(int code){return tasks[code]();}// 随机选择任务int SelectTask(){return rand() % tasks.size();}// 描述任务码对应的任务名称std::string ToDesc(int code){switch (code){case g_download_code:return "Download";case g_printlog_code:return "PrintLog";case g_push_videostream_code:return "PushVideoStream";default:return "Unknow";}}
};
Init init;
ProcessPool.cc如下:
#include <iostream>
#include <unistd.h>
#include <string>
#include <cassert>
#include <vector>
#include "Task.hpp"
#include <sys/types.h>
#include <sys/wait.h>static int number = 0; // 管道的编号
const int count = 5; // 子进程和管道个数// 用来确定有哪些任务
class Channel
{
public:Channel(int fd, pid_t workerid): _fd(fd), _workerid(workerid){_name = "channel: " + std::to_string(number++);}public:// 管道fd 子进程pid 管道名int _fd;pid_t _workerid;std::string _name;
};void Work()
{while (true){int code = 0; // 用来规定buffer,读取必须是4个字节,得到任务码ssize_t n = read(0, &code, sizeof(code)); // 已经完成输入重定向// read读到数据长度n必须等于sizeof(code)if (n == sizeof(code)) // 读到正确的code{if (!init.CheckCode(code)) // 不合法直接continuecontinue;init.RunTask(code); // 合法,执行任务,相当于init.tasks[code]()}else if (n == 0) // 写端关闭,读端继续读取,它将读到管道中的所有数据,直到read返回值为0{break;}else{}}std::cout << "child quit" << std::endl;
}void PrintFd(const std::vector<int> &fds)
{std::cout << getpid() << " close fds: ";for (auto fd : fds){std::cout << fd << " ";}std::cout << std::endl;
}// 传参形式:
// 1. 输入参数:const &
// 2. 输出参数:*
// 3. 输入输出参数:&
void CreatChannel(std::vector<Channel> *c)
{// bug// 父进程在不断创建管道时,创建第一个进程,父进程的信道写端已经在文件描述符里,// 再创建第二个管道和进程时,除了建立正常的通信信道以外,上一个信道在父进程的写端也会被下一个进程继承,// 再创建第三个管道和进程时,这个子进程的文件描述符表将包含指向三个信道。// 一直创建管道和进程,只有最后一个创建的管道只有一个写端指向,其它的管道都有多个写端指向。// 所以回收时要关闭全部信道写端再wait,如果close和wait同时进行,关闭信道写端从上往下关,关闭后还有无数个进程指向该信道,引用计数不为0,管道不释放,read读不到0,也就阻塞了std::vector<int> old;for (int i = 0; i < count; i++){// 1. 定义并创建管道int pipefd[2];int n = pipe(pipefd);assert(n == 0);(void)n;// 2. 创建进程pid_t id = fork();assert(id != -1);// 3. 构建单向信道if (id == 0) // 子进程{if (!old.empty()){for (auto fd : old){close(fd); // 把不属于自己的管道的写端关闭}PrintFd(old);}close(pipefd[1]);dup2(pipefd[0], 0); // 使用dup2后就不用给Work传参了,只用从标准输入拿数据即可Work();exit(0); // 会自动关闭自己打开的所有的fd}// 父进程close(pipefd[0]);c->push_back(Channel(pipefd[1], id)); // 之后对信道的增删查改就变成了对该vector的增删查改old.push_back(pipefd[1]); // 记录父进程的管道写端}
}void SendCommand(const std::vector<Channel> &c, bool flag, int num = -1)
{int pos = 0;while (true){// 1. 选择任务,得到任务码,4字节int taskcode = init.SelectTask();// 2. 选择信道(进程),轮询或随机,较为平均地将任务给进程,要考虑子进程完成任务的负载均衡const auto &channel = c[pos++];pos %= c.size();// debug 查看任务发送给谁了std::cout << "send taskcode " << init.ToDesc(taskcode) << "[" << taskcode << "]"<< " in "<< channel._name << " worker is : " << channel._workerid << std::endl;// 3. 发送任务write(channel._fd, &taskcode, sizeof(taskcode));// 4. 判断是否退出if (!flag){num--;if (num <= 0)break;}sleep(1);}std::cout << "SendCommand done..." << std::endl;
}void ReleaseChannel(const std::vector<Channel> &c)
{// 父进程退出了,与信道写端对应的文件描述符自动关闭// 写端关闭,读端继续读取,它将读到管道中的所有数据,直到read返回值为0for (const auto &channel : c){close(channel._fd);waitpid(channel._workerid, nullptr, 0);}// for (const auto &channel : c)// {// pid_t rid = waitpid(channel._workerid, nullptr, 0);// if (rid == channel._workerid)// {// std::cout << "wait child: " << channel._workerid << " success" << std::endl;// }// }// 还有一种方法,不用使用old关闭不属于自己的写端:倒状回收// int pos = c.size();// for (; pos >= 0; pos--)// {// close(c[pos]._fd);// waitpid(c[pos]._workerid, nullptr, 0);// }
}int main()
{std::vector<Channel> channels;// 创建信道、创建进程CreatChannel(&channels);// 向不同的管道发送不同任务const bool g_always_loop = true;// SendCommand(channels,g_always_loop);SendCommand(channels, !g_always_loop, 10);// 回收资源,子进程退出、释放管道ReleaseChannel(channels);return 0;
}
4.3 命名管道
命名管道(也称为FIFO)在Linux中是一种特殊的文件类型,它允许不同进程之间通过一个命名的管道进行通信。命名管道在文件系统中有一个可见的名称,可以像普通文件一样访问,但它们的操作方式与匿名管道不同。
命名管道是通过系统调用'mkfifo'创建的,可以用于不具有亲缘关系/毫不相关的进程进行进程间通信。它是一个文件,通常具有特定的扩展名(如'.fifo' 点表示匿名文件),但它实际上并不是文件系统中的普通文件,而是一个特殊的文件。
创建命名管道
- 命名管道可以从命令行上创建:
$ mkfifo filename- 命名管道也可以从程序里创建:
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode); pathname是命名管道的路径名。 mode是设置命名管道的权限模式,与open函数的mode参数类似。注意与umask的运算 成功返回0,失败返回-1。
命名管道文件是创建出来的磁盘级的符号,实际在进行数据通信时,由于该文件是管道文件,被打开时数据也不会向磁盘刷新。命名管道文件有路径和文件名,因为路径是具有唯一性的,所以,我们可以使用路径+文件名,来唯一的让不同进程看到同一份资源!
创建名为filename的命名管道,使用ll命令,发现命名管道文件类型为p,即管道文件。
匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一旦这些工作完成之后,它们具有相同的语义。(原理和特征一样)
4.4 用命名管道实现server&client通信
文件:comm.h client.cc server.cc Makefile
Makefile如下:
.PHONY:all
all:clientPipe serverPipeclientPipe:client.ccg++ -o $@ $^ -std=c++11serverPipe:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f clientPipe serverPipe
comm.h如下:
#pragma once#define FILENAME "fifo"
client.cc如下:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include "comm.h"
#include <fcntl.h>
#include <cstring>
#include <unistd.h>
#include <string>int main()
{// 打开命名管道int fifo_wfd = open(FILENAME, O_WRONLY);if (fifo_wfd < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;exit(0);}std::cout << "open fifo success-------write" << std::endl;// 向管道写入数据std::string message;while (true){std::cout << "Please Enter# ";std::getline(std::cin, message);ssize_t num = write(fifo_wfd, message.c_str(), message.size());if (num < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;break;}}close(fifo_wfd);std::cout << "close fifo success..." << std::endl;return 0;
}
server.cc 如下:
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include "comm.h"
#include <fcntl.h>
#include <cstring>
#include <unistd.h>// 创建命名管道
bool MakeFifo()
{int n = mkfifo(FILENAME, 0666);if (n < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;return false;}std::cout << "mkfifo success-------read" << std::endl;return true;
}
int main()
{
Start:// 不管有没有管道,直接打开命名管道,有管道就会返回fifo_rfdint fifo_rfd = open(FILENAME, O_RDONLY);if (fifo_rfd < 0)//没有管道就创建,然后再次打开{std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;if(MakeFifo()) goto Start;else return 1;}std::cout << "open fifo success-------read" << std::endl;// version 1 命名管道创建后再运行serverPipe会提示管道文件已存在// // 创建命名管道// int n = mkfifo(FILENAME, 0666);// if (n < 0)// {// std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;// exit(0);// }// std::cout << "mkfifo success-------read" << std::endl;// // 打开命名管道// int fifo_rfd = open(FILENAME, O_RDONLY);// if (fifo_rfd < 0)// {// std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;// exit(0);// }// std::cout << "open fifo success-------read" << std::endl;// 从管道读数据char buffer[1024];while (true){ssize_t num = read(fifo_rfd, buffer, sizeof(buffer) - 1);if (num > 0){buffer[num] = 0; // 或等于'\0'std::cout << "Client say: " << buffer << std::endl;}else if (num == 0){std::cout << "client quit, server quit too!" << std::endl;break;}}close(fifo_rfd);std::cout << "close fifo success..." << std::endl;return 0;
}
五、system V共享内存
5.1 system V共享内存的引入
管道不是为了通信而专门设置的一套方案,而是为了通信复用了之前的代码。而实际上OS在通信时场景很多,只有一种通信方式是不够的,因此,操作系统提供了多种IPC机制,包括但不限于:
- 管道(Pipe)和命名管道(FIFO):用于单向数据流通信。
- 消息队列(Message Queue):允许一个或多个进程向队列中写入消息,其他进程则可以读取队列中的消息。
- 信号量(Semaphore):用于同步进程间的访问共享资源。
- 共享内存(Shared Memory):允许多个进程共享一段内存区域,是最快的IPC方式,因为它不需要数据复制。
- 套接字(Socket):提供了在网络上的不同主机间进行通信的能力,也可以用于同一主机上的不同进程间通信。
System V共享内存是操作系统中提供的一种IPC机制,它允许不同的进程访问同一块内存区域,从而实现数据共享。
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
5.2 共享内存的原理
- 共享内存允许多个进程共享一段内存区域,而且共享内存段是物理内存中的一部分。
- 在物理内存新增共享内存段时,要对共享内存段先描述再组织,即使用struct shmid_ds描述了共享内存段的属性,如大小、访问权限、创建者信息等。通过链表进行对共享内存段的管理。
- 共享内存的创建是进程发起的。每个进程在Linux内核中都有一个task_struct结构来表示,这个结构包含了进程的所有信息,其中包括它的地址空间。地址空间被分为多个部分,包括代码段、数据段、堆、栈、共享区等。
- 每个进程都有自己的页表,通过页表可以将虚拟地址翻译成物理地址。在新增共享内存时,要在页表中进行映射,共享内存被映射到进程地址空间的共享区中,并向上层返回所在共享区的起始地址,使得进程可以通过地址空间,像访问自己的内存一样访问共享内存。
- 在使用System V共享内存时,每个共享内存段都有一个唯一的键(key),用于在进程间标识和访问共享内存段。内核使用这个键来查找或创建对应的struct shmid_ds。
OS中会存在很多进程,这些进行都有可能申请和使用共享内存,OS一定会允许系统中同时存在多个共享内存。共享内存,也要被操作系统管理,管理的方法就是先描述再组织,即上面讲到的struct shmid_ds结构体。但是上面的步骤只是一个进程创建共享内存,那么如何保证第二个之后的参与通信的进程,看到的就是同一个共享内存呢?
注意,进程不能直接给另一个进程直接传值,因为如果这样就说明已经能通信了,就不需要共享内存来传递消息了。所以进程不能将key传给另一个进程。方法:提前进行约定,让使用同一块共享内存的进程使用相同的key,这个key可以用户自己定义,也可以使用库方法,只要保证key唯一即可。
5.3 共享内存函数
shmget函数:既能创建也能获取
shmget函数用于创建一个新的共享内存段或者获取一个已经存在的共享内存段的标识符。
原型:
#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);
参数:
- key:一个键值,用于唯一标识共享内存段。在创建共享内存时就将key加载到其中。
- size:共享内存的大小。
- shmflg:一个标志位,用于控制共享内存的创建和访问权限。(使用方法类似open的flags标志位)
返回值:
成功时,返回共享内存段的标识符shmid;失败时,返回-1。
补充:
1. shmflg参数中常用的标志位
- IPC_CREAT:如果这个标志位被设置,并且共享内存段不存在,那么shmget函数会创建一个新的共享内存段。如果共享内存段已经存在,shmget会返回已存在的共享内存段的标识符。
- IPC_EXCL:这个标志位必须与IPC_CREAT标志位一起使用。如果IPC_CREAT和IPC_EXCL都被设置,并且共享内存段不存在,shmget函数会创建一个新的共享内存段。如果共享内存段已经存在,shmget函数会失败,并返回-1。用来保证共享内存段是新创建的。
- mode:这个值通常作为shmflg参数的低位部分,它表示共享内存段的权限模式。例如0666。
- 示例:int shmid = shmget(11223344, 4096, IPC_CREAT | 0666);
2. ftok函数来生成一个键值
- #include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);- 用户可以通过ftok函数来生成一个键值,这个键值通常基于一个路径名和一个项目ID。ftok函数返回一个整数,这个整数就是用于shmget函数的key参数。
- 示例:
key_t key;
key = ftok("path/to/file", 1); // "path/to/file"是文件路径,1是项目ID
int shmid = shmget(key, 4096, IPC_CREAT | 0666);- 因为用户定义的key不容易保证唯一性,所以使用ftok函数获取key。(相同的参数相同的算法,最终得到相同的值)
注意:
- key和shmid的区别
key是操作系统用来区分共享内存段的,shmid是用户用来进行对共享内存段的操作的。下面的shmat、shmctl都是使用shmid来对指定的共享内存段操作。包括命令行指令也是通过shmid进行操作。- 共享内存(IPC资源)的生命周期是随内核的!共享内存需要用户主动释放,除非重启OS
ipcs -m shmid 命令,查看有多少共享内存
ipcrm -m 命令,删除指定的共享内存
shmat函数:at->attach建立关联
shmat函数用于将共享内存段连接到进程的地址空间。
原型:
#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- shmid:共享内存段的标识符。
- shmaddr:指定连接的地址,如果为NULL,内核将自动选择地址。
- shmflg:连接标志,可以指定读写权限等。
返回值:
成功时,返回指向共享内存的指针,即映射到地址空间的起始虚拟地址;失败时,返回-1。说明:
- shmaddr为NULL,核心自动选择一个地址
- shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
- shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
- shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
shmdt函数:dt->detach去关联
shmdt函数用于将共享内存段与当前进程的地址空间脱离,即解除映射。
原型:
#include <sys/types.h>
#include <sys/shm.h>int shmdt(const void *shmaddr);
参数:
- shmaddr:由shmat返回的指针。
返回值:
成功时,返回0;失败时,返回-1。注意:将共享内存段与当前进程脱离不等于删除共享内存段。只是将页表中与共享内存段的映射清空。
什么时候删除共享内存?
struct shmid_ds中有shm_nattch字段,它是一个引用计数器,表示有多少个进程正在使用这个共享内存段。当一个进程使用shmat函数将共享内存段映射到自己的地址空间时,shm_nattch的值会增加;当进程使用shmdt函数将共享内存段从自己的地址空间脱离时,shm_nattch的值会减少。
当shm_nattch的值降至0时,意味着没有进程在使用这个共享内存段。在这种情况下,内核会考虑删除共享内存段,但还需要满足其他条件,比如共享内存段没有被其他进程以只读方式映射。只有当所有使用该共享内存段的进程都调用了shmdt函数后,操作系统才会删除共享内存段。
shmctl函数:ctl->control
shmctl函数用于控制共享内存,如删除共享内存段、改变共享内存的权限等。
原型:
#include <sys/ipc.h>
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
- shmid:共享内存段的标识符。
- cmd:将要采取的动作,如删除(IPC_RMID)、改变权限(IPC_SET)等。
IPC_STAT:获取共享内存段的当前状态,并将其存储在buf指向的struct shmid_ds结构体中。
IPC_SET:设置共享内存段的当前状态,并从buf指向的struct shmid_ds结构体中读取信息。
IPC_RMID:删除共享内存段,释放系统资源。(remove id 或remove immediately)- buf:指向一个'struct shmid_ds'结构体,该结构体包含共享内存的属性信息。
返回值:
成功时,返回0;失败时,返回-1。
5.4 使用共享内存的步骤
- 生成key:通过ftok函数来生成一个键值,基于一个路径名和一个项目ID。
- 创建共享内存段:使用shmget函数,指定key和共享内存的大小及其他属性来创建一个新的共享内存段或者获取一个已经存在的共享内存段的标识符。内核会创建一个struct shmid_ds来描述这个共享内存段,并在文件系统中创建一个对应的特殊文件。
- 映射共享内存段:使用shmat函数,将共享内存段映射到进程的地址空间中。内核会更新进程的页表,将共享内存的虚拟地址映射到物理内存的页面。
- 访问共享内存:进程可以使用指针操作来读取和写入共享内存中的数据。当进程访问共享内存时,它的页表会将虚拟地址翻译成物理地址,从而访问共享内存的物理页面。
- 解除映射:当进程完成共享内存的使用后,应该使用shmdt函数来解除映射。内核会更新进程的页表,取消共享内存的虚拟地址到物理地址的映射。
- 删除共享内存段:如果共享内存不再需要,可以使用shmctl函数来标记删除。内核会删除对应的struct shmid_ds,并在文件系统中删除对应的特殊文件。
5.5 基于共享内存的进程间通信示例
文件:comm.hpp client.cc server.cc Makefile
Makefile如下:
.PHONY:all
all:clientPipe serverPipeclientPipe:client.ccg++ -o $@ $^ -std=c++11serverPipe:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f clientPipe serverPipe
comm.hpp如下:
#pragma once#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cstring>
#include <unistd.h>const char *pathname = "/home/zzx/2024/0604/shm";
const int projectID = 1111; // 项目ID
const int Size = 4096; // 文件大小
const char *filename = "fifo"; // 命名管道key_t GetKey()
{return ftok(pathname, projectID);
}// int CreateShm(key_t key)
// {
// int shmid = shmget(key, Size, IPC_CREAT | 0666);
// if(shmid < 0)
// {
// std::cerr << "errno" << errno << ",errnostring: " << strerror(errno) << endl;
// exit(2);
// }
// return shmid;
// }int __CreateOrGetShm(key_t key, int flag)
{int shmid = shmget(key, Size, flag);if (shmid < 0){std::cerr << "errno" << errno << ",errnostring: " << strerror(errno) << std::endl;exit(2);}return shmid;
}int CreateShm(key_t key)
{return __CreateOrGetShm(key, IPC_CREAT | IPC_EXCL | 0666);
}int GetShm(key_t key)
{return __CreateOrGetShm(key, IPC_CREAT /*0也可以*/);
}bool MakeFifo()
{int n = mkfifo(filename, 0666);if (n < 0){std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;return false;}std::cout << "mkfifo success... read" << std::endl;return true;
}
client.cc如下:
#include "comm.hpp"int main()
{// 使用共享内存key_t key = GetKey();int shmid = GetShm(key);std::cout << "GetShm success --- client" << std::endl;char* shmaddr = (char*)shmat(shmid, nullptr,0);std::cout << "attach success --- client" << std::endl;int fd = open(filename,O_WRONLY);char c = 'a';while (c < 'z'){shmaddr[c-'a'] = c;std::cout << "write: " << shmaddr << std::endl;sleep(1);int code = 1;//只是通知作用,用来同步write(fd,&code,sizeof(code));c++;}shmdt(shmaddr);close(fd);return 0;
}
server.cc如下:
#include "comm.hpp"class Init
{
public:Init(){// 创建管道文件,复用同步机制bool r = MakeFifo();if (!r)return;// 创建共享内存key_t key = GetKey();shmid = CreateShm(key); // 封装了底层接口,其它函数也可以这样实现,在此不作实现std::cout << "CreateShm success --- server" << std::endl;// 与进程地址空间进行关联shmaddr = (char *)shmat(shmid, nullptr, 0);std::cout << "shmat success --- server" << std::endl;fd = open(filename, O_RDONLY);}~Init(){// 与进程地址空间去关联shmdt(shmaddr);std::cout << "shmdt success --- server" << std::endl;// 删除共享内存shmctl(shmid, IPC_RMID, nullptr);std::cout << "shmctl success --- server" << std::endl;}public:int fd;int shmid;char *shmaddr;
};int main()
{Init init;while (true){int code = 0;ssize_t n = read(init.fd, &code, sizeof(code));if (n > 0){std::cout << "共享内存的内容:" << init.shmaddr << std::endl;}else if (n == 0){break;}}return 0;
}
5.6 共享内存的特点
- 共享内存的通信方式,不会提供同步机制,共享内存是直接裸露给所有的使用者的,一定要注意共享内存的使用安全问题。
- 共享内存是所有进程间通信,速度最快的。
- 共享内存可以提供较大的空间
共享内存通信速度快是因为它减少了数据拷贝次数。在使用管道传递数据时要先创建管道,然后不同端向管道写入或读取数据,调用write或read等系统调用。在计算机中,凡是数据迁移,都是对数据的拷贝。用户通过进程A将数据写到管道,进程B从管道读出数据写入显示器,用户把数据传给进程A,进程B把数据打印到显示器文件也都用到了拷贝,拷贝也有代价。
使用共享内存,用户把数据传给进程A,就直接传到了共享内存中,数据一旦进入共享内存,进程B立即就能知道(因为没有同步机制),进程B直接共享区数据传给显示器,中间就至少减少两次系统调用(write, read)。
简而言之,在传统的IPC机制中,如管道,数据需要经过以下步骤:
- 用户空间到内核空间:用户通过系统调用(如write)将数据从用户空间拷贝到内核空间。
- 内核空间到内核空间:数据在内核空间之间传递,可能需要通过网络堆栈、文件系统等。
- 内核空间到用户空间:数据从内核空间拷贝到用户空间,通过系统调用(如read)被进程读取
这个过程涉及了多次数据拷贝,并且每次拷贝都会带来一定的开销。
相比之下,共享内存通信的过程是这样的:
- 用户空间到共享内存:用户进程将数据写入共享内存。
- 共享内存到用户空间:另一个进程从共享内存中读取数据。
在这个过程中,只有两次数据拷贝
5.7 共享内存数据结构
上面讲到的shmid_ds结构体,包括buf参数也使用一个指向shmid_ds结构的指针,shmid_ds结构体在<sys/shm.h>中定义如下:
struct shmid_ds {struct ipc_perm shm_perm; /* Ownership and permissions */size_t shm_segsz; /* Size of segment (bytes) */time_t shm_atime; /* Last attach time */time_t shm_dtime; /* Last detach time */time_t shm_ctime; /* Last change time */pid_t shm_cpid; /* PID of creator */pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */shmatt_t shm_nattch; /* No. of current attaches */...
};
ipc_perm结构定义如下(突出显示的字段可以使用IPC_SET设置):
struct ipc_perm {key_t __key; /* Key supplied to shmget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions + SHM_DEST andSHM_LOCKED flags */unsigned short __seq; /* Sequence number */
};
从中可以看到shmid_ds结构体的首元素是一个结构体ipc_perm,它包含创建共享内存段时提供的键值。
要想了解shmid_ds和ipc_perm就要介绍一下system V消息队列和system V信号量
六、简述system V消息队列和system V信号量
6.1 system V消息队列
消息队列的特性:
- 消息队列提供了一个从一个进程向另外一个进程发送一个数据块的方法。这个数据块也叫消息。
- 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值。接收者进程可以指定它只接收特定类型的消息。这允许不同的消息可以同时存在于队列中,而不需要接收者知道队列中有哪些类型的消息。例如进程A要求进程B能看到,类型就设置为B。
- 每个消息队列都有一个唯一的标识符msqid,用于在系统中标识和访问该队列。
- 与共享内存段类似,消息队列也可以通过键来标识,用于在系统中唯一标识消息队列。
- 与System V的其他IPC资源一样,消息队列需要显式地删除,否则不会自动清除,除非重启,所以system V 消息队列资源的生命周期随内核。
- 系统中可以同时存在多个消息队列,消息队列在内核中管理,也要先描述,再组织,因此消息队列=队列+队列的属性。
System V消息队列函数:
msgget:创建或获取一个消息队列标识符。
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数:
- key:用于标识消息队列的键值,可以是一个已存在的键值或者通过ftok函数生成的键值。
- msgflg:标志位,用于控制消息队列的创建和访问权限。
返回值:成功时返回消息队列标识符,失败时返回-1。
注意:msgflg参数可以设置权限标志,如IPC_CREAT(创建消息队列)、IPC_EXCL(创建时检查消息队列是否存在)等。用法与System V共享内存shmget函数的shmflg参数相同。
msgctl:控制消息队列。
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
- msqid:消息队列标识符。
- cmd:操作命令,如IPC_STAT(获取消息队列状态)、IPC_SET(设置消息队列属性)、IPC_RMID(删除消息队列)等。
- buf:指向struct msqid_ds的指针,用于存储消息队列的状态信息。
返回值:成功时返回0,失败时返回-1。
msgsnd:向消息队列发送消息。
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数:
- msqid:消息队列标识符。
- msgp:指向消息的指针。
- msgsz:消息的大小。
- msgflg:标志位,用于控制发送操作的行为。
返回值:成功时返回0或消息大小,失败时返回-1。
msgrcv:从消息队列接收消息。
原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
- msqid:消息队列标识符。
- msgp:指向接收消息缓冲区的指针。
- msgsz:接收缓冲区的大小。
- msgtyp:接收消息的类型值。
- msgflg:标志位,用于控制接收操作的行为。
返回值:成功时返回接收到的消息大小,失败时返回-1。
msgsnd和msgrcv函数的msgflg参数可以设置阻塞标志,如MSG_EXCEPT(接收除指定类型外的消息)、MSG_NOERROR(如果接收消息失败,返回-1而不是设置错误码)等。
msqid_ds数据结构定义如下:
struct msqid_ds {struct ipc_perm msg_perm; /* Ownership and permissions */time_t msg_stime; /* Time of last msgsnd(2) */time_t msg_rtime; /* Time of last msgrcv(2) */time_t msg_ctime; /* Time of last change */unsigned long __msg_cbytes; /* Current number of bytes inqueue (nonstandard) */msgqnum_t msg_qnum; /* Current number of messagesin queue */msglen_t msg_qbytes; /* Maximum number of bytesallowed in queue */pid_t msg_lspid; /* PID of last msgsnd(2) */pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
ipc_perm结构定义如下:
struct ipc_perm {key_t __key; /* Key supplied to msgget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions */unsigned short __seq; /* Sequence number */
};
6.2 system V信号量
System V信号量函数也有semget、semctl、semop函数,在此不讲述。它们用法也和共享内存、消息队列类似,因为都是system V系列的。
semid_ds结构体定义如下:
struct semid_ds {struct ipc_perm sem_perm; /* Ownership and permissions */time_t sem_otime; /* Last semop time */time_t sem_ctime; /* Last change time */unsigned long sem_nsems; /* No. of semaphores in set */
};
ipc_perm定义如下:
struct ipc_perm {key_t __key; /* Key supplied to semget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions */unsigned short __seq; /* Sequence number */
};
信号量的本质是一组计数器。信号量主要用于同步和互斥。
为了让进程间能够通信,就要让多个进程看到同一份资源,这份资源称为公共资源,使用公共资源就可能导致并发访问、数据不一致问题,例如读的时候另一个进程读、读的时候另一个进行写、写的时候。。。所以就需要在一个进程使用资源的时候,将这份资源保护起来,所有进程按顺序使用,这就是互斥和同步。
互斥:任何一个时刻只允许一个执行流(进程)访问公共资源,(加锁实现的)
同步:多个执行流执行时,按照一定的顺序执行。
临界资源:被保护起来的公共资源。(不是临界资源的就是非临界资源)
临界区:访问该临界区的代码。(维护临界资源就是维护临界区)
原子性:只有两态,要么没做,要么做完。
比如在电影院买票,电影院和内部座位就是多人共享的资源 --- 公共资源(可能被拆分为多份资源)。我们买票的本质:是对资源的预订机制。可以看成,电影院有一个计数器用来表示公共资源的个数。别人买票时要先看计数器内还有没有剩余的座位,有的话就分配,计数器--,没有就让那人等着。
如果公共资源没有被拆分只有一份,用二元信号量int sem =1表示互斥锁来完成互斥功能,在临界区前面和后面加上维护代码,检测sem是否有剩余,如果有剩余就允许继续临界区的代码、sem--,没有剩余就继续等待,直至有一个临界区完成并sem++。其实这个信号量也可以看作一个结构体,里面有一个计数器和一个等待队列,没有剩余就将进程放入等待队列中,知道有一个sem++,就执行等待队列的下一个进程。
信号量:表示资源数目的计数器,每一个执行流想访问公共资源内的某一份资源,不应该让执行流直接访问,而是先申请信号量资源,其实就是先对信号量计数器进行--操作。本质上,只要--成功,完成了对资源的预订机制,如果申请不成功,执行流被挂起阻塞。
七、回顾共享内存数据结构
在看到共享内存、消息队列和信号量的数据结构后,发现它们都使用了ipc_perm结构体,而且都是位于对应数据结构的第一个,这是因为在底层中,在系统层面有一个类型为kern_ipc_perm *p[0]的柔性指针数组,通过该数组管理所有的IPC资源。例如创建一个共享内存的数据结构shmid_ds,在柔性指针数组中加上对应的ipc_perm结构体的地址,将来对shmid_ds进行管理时,由于ipc_perm结构体是shmid_ds第一个元素,所以只需要对它进行类型转换,就可以变成shmid_ds的地址,就可以对它的数据成员进行操作。例如(shmid_ds *)p[1] 、 (msqid_ds*)p[2]。
相关文章:
【Linux】进程间通信
目录 一、进程间通信概念 二、进程间通信的发展 三、进程间通信的分类 四、管道 4.1 什么是管道 4.2 匿名管道 4.2 基于匿名管道设计进程池 4.3 命名管道 4.4 用命名管道实现server&client通信 五、system V共享内存 5.1 system V共享内存的引入 5.2 共享内存的…...
UI与前端:揭秘两者的微妙差异
UI与前端:揭秘两者的微妙差异 在数字化时代的浪潮中,UI设计和前端开发已成为塑造用户体验的两大核心力量。然而,这两者之间究竟有何区别?本文将深入剖析UI设计与前端开发的四个方面、五个方面、六个方面和七个方面的差异…...
idea如何根据路径快速在项目中快速打卡该页面
在idea项目中使用快捷键shift根据路径快速找到该文件并打卡 双击shift(连续按两下shift) -粘贴文件路径-鼠标左键点击选中跳转的路径 自动进入该路径页面 例如:我的实例路径为src/views/user/govType.vue 输入src/views/user/govType或加vue后缀src/views/user/go…...
探索成功者的特质——俞敏洪的观点启示
在人生的舞台上,我们常常对成功者充满好奇与敬仰,试图探寻他们成功的奥秘。俞敏洪指出,成功者都具备七个特质,而这些特质与家庭背景和大学的好坏并无直接关系。让我们深入剖析这七个特质,或许能从中获得对我们自身成长…...
MCU的环形FIFO
fifo.h #ifndef __FIFO_H #define __FIFO_H#include "main.h"#define RINGBUFF_LEN (500) //定义最大接收字节数 500typedef struct {uint16_t Head; // 头指针 指向可读起始地址 每读一个,数字1uint16_t Tail; // 尾指针 指…...
使用proteus仿真51单片机的流水灯实现
proteus介绍: proteus是一个十分便捷的用于电路仿真的软件,可以用于实现电路的设计、仿真、调试等。并且可以在对应的代码编辑区域,使用代码实现电路功能的仿真。 汇编语言介绍: 百度百科介绍如下: 汇编语言是培养…...
【漏洞复现】Apache OFBiz 路径遍历导致RCE漏洞(CVE-2024-36104)
0x01 产品简介 Apache OFBiz是一个电子商务平台,用于构建大中型企业级、跨平台、跨数据库、跨应用服务器的多层、分布式电子商务类应用系统。是美国阿帕奇(Apache)基金会的一套企业资源计划(ERP)系统。该系统提供了一整套基于Java的Web应用程序组件和工具。 0x02 …...
数据库表中创建字段查询出来却为NULL?
起因: 今天新创建了一张表,其中一个字段命名为"word_num"带下划线,我在前端页面怎么也查询不出来word_num的值,后来在后端接口处打印了一下数据库查询出来的数据,发现这个字段一直为NULL,然后我就想到是不是…...
缓存方法返回值
1. 业务需求 前端用户查询数据时,数据查询缓慢耗费时间; 基于缓存中间件实现缓存方法返回值:实现流程用户第一次查询时在数据库查询,并将查询的返回值存储在缓存中间件中,在缓存有效期内前端用户再次查询时,从缓存中间件缓存获取 2. 基于Redis实现 参考1 2.1 简单实现 引入…...
【十大排序算法】快速排序
在乱序的世界中,快速排序如同一位智慧的园丁, 以轻盈的手法,将无序的花朵们重新安排, 在每一次比较中,沐浴着理性的阳光, 终使它们在有序的花园里,开出绚烂的芬芳。 文章目录 一、快速排序二、…...
linux系统ubuntu中在命令行中打开图形界面的文件夹
在命令行中打开当前路径,以文件管理器的形式打开: 命令 # 打开文件管理器 当前的路径 nautilus .nautilus 是一个与 GNOME 桌面环境集成的文件管理器的命令行启动程序。在 Linux 系统中,特别是使用 GNOME 作为桌面环境时,用户经…...
【C++11数据结构与算法】C++ 栈
C 栈(stack) 文章目录 C 栈(stack)栈的基本介绍栈的算法运用单调栈实战题LC例题:[321. 拼接最大数](https://leetcode.cn/problems/create-maximum-number/)LC例题:[316. 去除重复字母](https://leetcode.cn/problems/remove-duplicate-letters/) 栈的基…...
pdf文件如何防篡改内容
PDF文件防篡改内容的方法有多种,以下是一些常见且有效的方法,它们可以帮助确保PDF文件的完整性和真实性: 加密PDF文档: 原理:通过设置密码来保护PDF文档,防止未经授权的访问和修改。注意事项:密…...
QT 音乐播放器【二】 歌词同步+滚动+特效
文章目录 效果图概述代码解析歌词歌词同步歌词特效 总结 效果图 概述 先整体说明一下这个效果的实现,你所看到的歌词都是QGraphicsObject,在QGraphicsView上绘制(paint)出来的。也就是说每一句歌词都是一个图元(item)。 为什么用QGraphicsView框架&…...
关于怎么用Cubemx生成的USBHID设备实现读取一体的鼠标键盘设备(改进版)
主要最近做了一个要用STM32实现读取鼠标键盘一体的那种USB设备,STM32的界面上要和电脑一样的能通过这个USB接口实现鼠标移动,键盘的按键。然后我就很自然的去参考了正点原子的例程,可是找了一圈,发现正点原子好像用的库函数&#…...
Soildworks学习笔记(二)
放样凸台基体: 自动生成连接两个物体两个面的基体: 2.旋转切除: 3.剪切实体: 4.转换实体引用: 将实体的轮廓线转换至当前草图使其成为当前草图的图元,主要用于在同一平面或另一个坐标中制作草图实体或其尺寸的副本。 …...
Linux配置uwsgi环境
Linux配置uwsgi环境 1.进入虚拟环境 source /envs/django_-shop-system/bin/activate2.安装uwsgi pip install uwsgi3.基于uwsgi运行项目 – 基于配置文件 在项目目录下创建配置文件 #socket 0.0.0.0:8005 http 0.0.0.0:8005 # http120.55.47.111:8005 chdir/opt/www/djang…...
Nagios的安装和使用
*实验* *nagios安装和使用* Nagios 是一个监视系统运行状态和网络信息的监视系统。Nagios 能监视所指定的本地或远程主机以及服务,同时提供异常通知功能等. Nagios 可运行在 Linux/Unix 平台之上,同时提供一个可选的基于浏览器的 WEB 界面以方便系统管…...
Numba 的 CUDA 示例(4/4):原子和互斥
本教程为 Numba CUDA 示例 第 4 部分。 本系列第 4 部分总结了使用 Python 从头开始学习 CUDA 编程的旅程 介绍 在本系列的前三部分(第 1 部分,第 2 部分,第 3 部分)中,我们介绍了 CUDA 开发的大部分基础知识…...
【机器学习】机器学习引领AI:重塑人类社会的新纪元
📝个人主页🌹:Eternity._ 🌹🌹期待您的关注 🌹🌹 ❀机器学习引领AI 📒1. 引言📕2. 人工智能(AI)🌈人工智能的发展🌞应用领…...
2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...
从零实现STL哈希容器:unordered_map/unordered_set封装详解
本篇文章是对C学习的STL哈希容器自主实现部分的学习分享 希望也能为你带来些帮助~ 那咱们废话不多说,直接开始吧! 一、源码结构分析 1. SGISTL30实现剖析 // hash_set核心结构 template <class Value, class HashFcn, ...> class hash_set {ty…...
ios苹果系统,js 滑动屏幕、锚定无效
现象:window.addEventListener监听touch无效,划不动屏幕,但是代码逻辑都有执行到。 scrollIntoView也无效。 原因:这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作,从而会影响…...
tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
webpack面试题
面试题:webpack介绍和简单使用 一、webpack(模块化打包工具)1. webpack是把项目当作一个整体,通过给定的一个主文件,webpack将从这个主文件开始找到你项目当中的所有依赖文件,使用loaders来处理它们&#x…...
Python的__call__ 方法
在 Python 中,__call__ 是一个特殊的魔术方法(magic method),它允许一个类的实例像函数一样被调用。当你在一个对象后面加上 () 并执行时(例如 obj()),Python 会自动调用该对象的 __call__ 方法…...
Docker、Wsl 打包迁移环境
电脑需要开启wsl2 可以使用wsl -v 查看当前的版本 wsl -v WSL 版本: 2.2.4.0 内核版本: 5.15.153.1-2 WSLg 版本: 1.0.61 MSRDC 版本: 1.2.5326 Direct3D 版本: 1.611.1-81528511 DXCore 版本: 10.0.2609…...
【多线程初阶】单例模式 指令重排序问题
文章目录 1.单例模式1)饿汉模式2)懒汉模式①.单线程版本②.多线程版本 2.分析单例模式里的线程安全问题1)饿汉模式2)懒汉模式懒汉模式是如何出现线程安全问题的 3.解决问题进一步优化加锁导致的执行效率优化预防内存可见性问题 4.解决指令重排序问题 1.单例模式 单例模式确保某…...



