学习目标
理解进程间通信
掌握管道
掌握共享内存
了解消息队列、信号量
通信之前,让不同的进程看到同一份资源(文件、内存块)
资源不同,决定了不同种类的通信方式
内存级的通信
IPC
进程间通信(Inter-Process Communication,IPC)是指操作系统提供的一组机制和技术,用于不同进程之间进行数据传输、信息交换和协调操作的方式。在多进程或分布式系统中,不同的进程可能需要共享信息、进行协作处理、传递数据等,而IPC机制提供了可靠和有效的方式来实现这种通信。
通信之前,让不同的进程看到同一份资源(文件、内存块),而资源不同决定了不同种类的通信方式
1、ipc指令
-
ipcs -q
:查看消息队列的信息。 -
ipcrm -q msqid
:删除指定的消息队列,其中msqid是消息队列的标识符。 -
ipcs -s
:查看信号量的信息。 -
ipcrm -s semid
:删除指定的信号量,其中semid是信号量的标识符。 -
ipcs -l
:列出所有的IPC资源的详细信息。 -
ipcrm -a
:删除所有的IPC资源。 -
ipcs -m
用于查看共享内存的信息。 -
ipcrm -m shmid
: 删除共享内存
2、相关概念
1)临界资源:
在操作系统中,临界资源是指一次只能由一个进程或线程访问的共享资源。临界资源可以是共享内存区域、文件、设备、数据结构等。当多个进程或线程需要访问同一个临界资源时,需要通过同步机制来协调它们的访问。
2)临界区:
临界区是指程序中访问临界资源的那段代码或代码段。临界区的目的是确保对临界资源的访问是原子的、互斥的,即同一时间只有一个进程或线程可以进入临界区进行访问。
3)原子性:
原子性是指一个操作在执行过程中不会被中断的特性。原子操作是不可再分的,要么完全执行,要么完全不执行,没有中间状态。在多线程或多进程环境中,如果多个线程或进程同时访问共享资源,如果对共享资源的操作不是原子的,就可能导致数据不一致或竞态条件的问题。
4)互斥:
互斥是一种同步机制,用于保护共享资源,确保在同一时间只有一个进程或线程可以访问共享资源。互斥机制通过对临界区的访问进行互斥,即同一时间只允许一个进程或线程进入临界区,其他进程或线程需要等待。常见的互斥机制包括互斥锁(Mutex)和信号量(Semaphore)。互斥机制可以防止多个进程或线程同时对共享资源进行访问,从而避免数据不一致或竞态条件的问题。
匿名管道:
1、原理:子进程继承父进程的文件标识符和文件描述表
2、特点
半双工通信:匿名管道是一种单向通信机制,只能在一个方向上传递数据。通常,管道被创建时,会产生两个文件描述符,一个用于读取数据(pipeid[0]),另一个用于写入数据(pipeid[1])。
亲缘进程通信:匿名管道通常用于具有父子关系的进程之间进行通信。
共享内核缓冲区:匿名管道使用内核中的缓冲区作为数据传输的中介,并不会将管道刷新至磁盘。
进程同步机制:匿名管道读写的阻塞特性可以用于进程之间的同步。
生命周期:随进程结束。
3、相关函数
1)pipe函数
#include <unistd.h> int pipe(int pipefd[2]);
pipefd[2]:该数组用于存储创建的管道的文件描述符。pipefd[0]表示读端,pipefd[1]表示写端
返回值:函数成功时返回0,失败时返回-1
4、简单实操:
1)父子进程的传递数据
#include <iostream> #include <cerror> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> int main() { int pipefd[2]; if(pipe(pipefd)!=0)//创建管道 { cerr<<"pipe error"<<strerror(errno)<<endl;//失败返回-1 exit(1); } pid_t id=fork();//创建子进程 if(id==0)//子进程进行单向读操作 { close(pipefd[1]);//保持单向关闭写端 while(true) { char buffer[1024]; ssize_t n=read(pipefd[0],buffer,sizeof(buffer)-1);//从管道读取内容,给数组留'\0'的位置 if(n>0)//返回读取的字符 { buffer[n]='\0'; printf("%s",buffer); } else if//写端关闭返回0,读取错误返回-1 { break; } } close(pipefd[0]);//关闭写端 exit(1);//子进程退出 } //父进程进行读操作 close(pipeid[0]);//关闭读端 while(true) { sleep(2); char s[1024]; fgets(s,sizeof(s),stdin); write(pipeid[1],s,strlen(s));//给管道写入内容 } close(pipeid[1]);//关闭写端 if(waitpid(id,nullptr,0)>0) cout<<"wait sucess"<<endl; }
2)进程池
#include <unordered_map> #include <time.h> #include <vector> #include <assert.h> #include <iostream> #include <unistd.h> #include <cstring> #include <sys/types.h> #include <sys/wait.h> #include <string> #include <stdio.h> using namespace std; typedef void(*functor)(); vector <functor> functors; unordered_map<size_t,string> info; vector<pair<size_t,size_t>> assignTask; void f1() { cout << "这是一个处理日志的任务, 执行的进程 ID [" << getpid() << "]" << "执行时间是[" << time(nullptr) << "]" << endl; } void f2() { cout << "这是一个备份数据任务, 执行的进程 ID [" << getpid() << "]" << "执行时间是[" << time(nullptr) << "]" << endl; } void f3() { cout << "这是一个处理网络连接的任务, 执行的进程 ID [" << getpid() << "]" << "执行时间是[" << time(nullptr) << "]" << endl; } void Loading() { functors.push_back(f1); info.insert({functors.size(),"处理日志"}); functors.push_back(f2); info.insert({functors.size(),"备份数据"}); functors.push_back(f3); info.insert({functors.size(),"网络连接"}); } void sendTask(vector<pair<size_t,size_t>>&processFds) { while(true) { sleep(2); size_t pickProcess = rand() % processFds.size(); size_t pickTask = rand() % functors.size(); write(processFds[pickProcess].second, &pickTask, sizeof(size_t)); cout<<"给子进程pid"<<processFds[pickProcess].first<<"分配:"<<info[pickTask]<<endl; } } void operateTask(size_t blockFd) { while(true) { size_t Task=0; srand((long long)time(nullptr)); ssize_t s=read(blockFd,&Task,sizeof(size_t)); if(s==0||s==-1) { break; } assert(s==sizeof(size_t)); (void)s;//assert在debug模式下有效,在release无效会因定义未使用报错 if(Task<functors.size()) { functors[Task](); } } } int main() { Loading(); int processNum=5; for(int i=0;i<processNum;i++) { int pipefd[2]; pipe(pipefd); pid_t id = fork(); if (id == 0) { close(pipefd[1]); operateTask(pipefd[0]); close(pipefd[0]); exit(1); } close(pipefd[0]); assignTask.push_back({id,pipefd[1]}); } sendTask(assignTask); for(int i=0;i<processNum;i++) { close(assignTask[i].second); waitpid(assignTask[i].first,nullptr,0); } return 0; }
命名管道:
命名管道(Named Pipe),也称为FIFO(First In, First Out),是一种在进程间进行通信的方法。它允许不相关的进程通过一个共享的命名管道来交换数据。
1、原理
在文件系统中创建一个特殊的文件,进程可以通过读写这个文件来进行通信。
2、特点
命名管道是匿名管道的改良,解决了只有血缘关系的进程才能通信的问题
共享性:多个进程可以共享同一个命名管道,从而实现进程间的数据交换。不同进程可以通过读写同一个管道文件进行通信。
3、相关函数
1)mkfifo
在文件系统中创建一个特殊的文件,用于命名管道的数据传输
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int mkfifo(const char *pathname, mode_t mode);
pathname
:指定要创建的命名管道的路径和名称。
mode
:指定创建的管道的权限。返回值:返回0表示成功,-1表示失败,并通过
errno
变量来指示具体的错误原因。
2)unlink
在文件系统中删除指定的路径文件
#include <unistd.h> int unlink(const char *pathname);
pathname
:指定要删除的文件的路径和名称。返回值:返回0表示删除成功,-1表示删除失败,并通过
errno
变量来指示具体的错误原因。
4、简单实操:
这是一个简单的使用命名管道进行进程间通信的示例代码。
代码分为
serverFifo.cpp
和clientFifo.cpp
两个文件,以及一个共享的头文件comm.h
。下面对代码进行说明:
1)comm.h
#pragma once #include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <cstring> #include <cstdlib> #include <fcntl.h> #define IPC_PATH "./.fifo"//定义了一个宏`IPC_PATH`,表示命名管道的路径。
2)serverFifo.cpp
#include "comm.h" using namespace std; int main() { if(mkfifo(IPC_PATH,0600)!=0)//在程序开始处,通过`mkfifo`函数创建了一个命名管道,路径为`IPC_PATH`。 { cerr<<"mkfifo error"<<strerror(errno)<<endl; exit(1); } int fd=open(IPC_PATH,O_RDONLY);//打开命名管道,以只读方式打开。 while(true)//进入一个循环,不断读取从客户端写入的数据。 { char buffer[1024]; ssize_t s=read(fd,buffer,sizeof(buffer)-1); if(s>0)//如果读取到数据,将其打印输出。 { buffer[s]='\0'; printf("接收到命令:%s",buffer); } else//如果读取到的数据长度为0或者错误,说明客户端已经关闭了写端,退出循环。 { break; } } close(fd); return 0; }
3)clientFifo.cpp
#include "comm.h" int main() { int fd=open(IPC_PATH,O_WRONLY);//打开命名管道,以只写方式打开。 while(true)//进入一个循环,读取用户输入的命令,并将其写入到命名管道中。 { printf("请输入命名:"); fflush(stdout); char s[1024]; fgets(s,sizeof(s),stdin); write(fd,s,strlen(s)); } close(fd); return 0; }
这个示例展示了一个简单的服务器-客户端模型,使用命名管道进行进程间通信。服务器创建了一个命名管道,并等待客户端写入数据。客户端可以输入命令,并将其写入到命名管道中,服务器则读取管道中的数据并进行处理。这种方式可以实现简单的进程间通信,用于传输命令、数据等。
需要注意的是,这个示例中的命名管道是单向的,即只能从客户端向服务器传输数据。如果需要双向通信,可以创建两个命名管道,分别用于客户端向服务器发送数据和服务器向客户端发送数据。
共享内存
共享内存是一种用于实现进程间通信的机制,它允许多个进程在它们的地址空间中共享同一块内存区域。这种方式可以实现高效的数据交换,因为进程可以直接访问共享内存,而无需进行复制或通过中间介质传输数据。
1、原理
创建共享内存,进程可以通过对共享内存的访问来进行通信
2、特点
高效性:由于共享内存允许进程直接访问共享数据,而无需复制或传输,因此在进程间交换大量数据时非常高效。
实时性:共享内存可以提供实时的数据共享,因为数据在共享内存中的修改可以立即被其他进程看到,无需等待数据传输或同步操作。也就意味着没有进程同步机制,直接共享数据而没有提供任何保护机制。
灵活性:共享内存可以用于各种数据结构和数据类型,因为它仅提供一块内存区域,对数据的组织和管理完全由应用程序自己决定。
生命周期:随内核,创建共享内存后,进程退出时,共享内存依旧存在,其内存是随内核的,需要显示删除
3、相关函数
1)shmget
shmget
是一个用于创建或获取共享内存段的系统调用
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
key
:用于标识共享内存段的键值,可以是一个正整数或使用ftok
函数生成的键值。key_t ftok(const char *pathname, int proj_id);
size
:要创建或获取的共享内存段的大小(字节数)。内存在操作系统按页(4KB)管理,size一般设为页的整数倍
shmflg
:控制共享内存的权限和其他选项的标志。
IPC_CREAT|IPC_EXCL:保证shmget调用成功,一定是一个全新的共享内存
返回值:返回一个非负整数shmid的共享内存标识符,该标识符用于后续操作
注意:
对于不同的进程,要访问同一个共享内存段,需要使用相同的键值和权限标志。
2)shmctl
shmctl
函数用于控制和操作共享内存段的属性。例如获取和修改共享内存的权限、大小、状态等。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
:共享内存标识符,由shmget
函数返回的共享内存段的标识符。
IPC_STAT
:获取共享内存的信息,并将其存储在buf
指向的结构体中。
IPC_SET
:修改共享内存的权限和其他属性,修改的属性由buf
指向的结构体中的相应字段指定。
IPC_RMID
:删除共享内存段。
cmd
:控制命令,指定对共享内存的操作。
buf
:指向struct shmid_ds
结构的指针,用于存储共享内存的信息。返回值:如果返回值为-1,表示操作失败,可以通过查看
errno
变量来获取错误信息。
注意事项:
-
使用
IPC_STAT
命令获取共享内存的信息时,需要提前分配好存储信息的结构体,并将其指针传递给buf
参数。 -
使用
IPC_SET
命令修改共享内存的属性时,需要在struct shmid_ds
结构中指定要修改的属性。 -
使用
IPC_RMID
命令删除共享内存段时,需要确保没有进程正在使用该共享内存段,否则删除操作将失败。
shmctl
函数是操作System V共享内存的关键函数之一,通过它可以灵活地管理和控制共享内存段的属性和状态。
3)shmat
shmat
用于将共享内存段附加到进程的地址空间中,使得进程可以访问共享内存中的数据。
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
shmid
:共享内存标识符,由shmget
函数返回。
shmaddr
:共享内存段希望附加到的地址。如果shmaddr
为NULL
,表示让系统自动选择一个合适的地址进行附加。
shmflg
:附加标志,用于指定共享内存段的访问权限。
返回值:
成功时,返回附加后的共享内存段的起始地址。类似于malloc的使用
失败时,返回
-1
,并设置相应的错误码。
4)shmdt
shmdt
用于将共享内存段从进程的地址空间中分离
int shmdt(const void *shmaddr);
参数说明:
shmaddr
:共享内存段的起始地址。
返回值:
成功时,返回
0
。失败时,返回
-1
,并设置相应的错误码。
4、简单实操:
1)comm.h
#include <iostream> #include <cstring> #include <cstdlib> #include <cerrno> #include <cassert> #include <unistd.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> #include <sys/stat.h> #include <fcntl.h> #include <cstring> #define PATH_NAME "/home/lxy/" #define PROJ_ID 0x15 #define MEM_SIZE 4096 using namespace std; key_t CreatKey() { key_t key=ftok(PATH_NAME,PROJ_ID);//这个键值在不同的进程中是唯一的,可以用于进程间通信的 IPC 对象的标识 if(key<0) { cout<<"ftok error"<<strerror(errno)<<endl; exit(1); } return key; }
2)serverShm.cpp
#include "comm.hpp" const int flags = IPC_CREAT | IPC_EXCL;//保证shmget调用成功,一定是一个全新的共享内存 int main() { //创建 key_t key=CreatKey(); int shmid=shmget(key,MEM_SIZE,flags|0666);//注意需要给定读写权限 if(shmid<0) { cout<<"shmget error"<<strerror(errno)<<endl; exit(1); } //连接 char *s=(char*)shmat(shmid,nullptr,0);//类似于malloc的使用 //通信 while(true) { cout<<s<<endl;//可以直接进行访问,而不需要系统调用 sleep(1); } //断连 shmdt(s); //删除 shmctl(shmid,IPC_RMID,nullptr); return 0; }
3)clientShm.cpp
#include "comm.hpp" int main() { //创建 key_t key=CreatKey(); int shmid=shmget(key,MEM_SIZE,IPC_CREAT); if(shmid<0) { cout<<"shmget error"<<strerror(errno)<<endl; exit(1); } //关联 char *s=(char*)shmat(shmid,nullptr,0); //通信 while(true) { printf("Please Enter# "); fflush(stdout); fgets(s,MEM_SIZE,stdin);//从键盘获取 } //断联 shmdt(s); return 0; }
消息队列
信号队列(Signal Queue)是一种在操作系统中用于进程间通信的机制。它允许进程通过发送和接收信号来进行通信和同步操作。
信号队列通过维护一个队列来存储进程发送的信号。每个信号都包含一个特定的标识符和相关的数据。当一个进程发送一个信号时,它会将信号添加到队列的末尾。接收进程可以从队列的头部获取信号并进行相应的处理。
信号量
信号量(Semaphore)是一种在操作系统中用于进程间同步和互斥的机制。它允许多个进程共享一个计数器,并通过对该计数器进行操作来实现同步和互斥的控制。
特点
计数器:信号量维护一个计数器,该计数器的初值可以是任意非负整数。计数器的值表示可用的资源数量或者某个共享资源的状态。
P操作:当进程需要访问一个共享资源时,它首先执行P(等待)操作,该操作会将信号量的计数器减1。如果计数器的值大于等于0,表示资源可用,进程可以继续访问资源;如果计数器的值小于0,出现互斥情况,表示资源不可用,进程需要等待。
V操作:当进程使用完共享资源时,它执行V(释放)操作,该操作会将信号量的计数器加1。这样,其他等待资源的进程就有机会继续执行。