欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (bingbingsupercool) - Gitee.comhttps://gitee.com/bingbingsurercool
系列文章推荐
目录
前言
我们知道在生活中“管道”是用来传递资源的,那么在操作系统中,如果两个进程之间需要传递资源,那么也需要“管道”进行传输。进程与进程之间传递资源的行为其实就是进程间通信的行为,而管道与共享内存就是进程间通信的一种手段。下面我们就详细介绍一下进程间通信的过程与管道,共享内存等手段的使用。
1.进程间通信
进程之间进行通信其实并不是那么简单,之前我们学习进程的时候都知道,进程之间具备独立性。每个进程都有自己独特的数据与资源,都有自己独有的task_struct结构体。因此进行进程间通信的前提是先让不同的进程看到同一份资源,然后在进行数据的交换。
为什么需要进程间通信?
单进程无法使用并发能力,无法实现多进程的协同,进程间通信是实现多进程协同的手段。当一个任务需要多个进程进行共同协同的时候据必然需要进程之间进行数据的交互。每个进程都是独立的,但是进程之间需要进行数据传输(一个进程需要将它的数据发送给另一个进程)、资源共享(多个进程之间共享同样的资源)、通知事件(一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件)、进程控制(有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变)。而这些都需要进程间通信进行完成。
进程间怎么进行通信呢?
如前文所述,进行进程间通信的前提是确保不同的进程能够看到同一块“内存”(特地的结构组织)。但是这一块特定的内存并不属于任何一个进程,而是进程之间共享的。
随着技术的发展,进程间通信逐渐发展出了以下的标准。1.管道:Linux系统原生提供的通信方式。2.SystemV IPC:进行多进程的单机通信,有共享内存,消息队列,信号量等方式。3.POSIX IPC:多进程通信,通常进行网络的通信。
2.管道
什么是管道?管道是Unix中最古老的通信方式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。管道是单向通信的工具,一个进程将数据写入管道,另一个进程将数据从管道中提取出来。
2.1匿名管道
管道分为匿名管道和命名管道,匿名管道通常进行具有血缘关系的进程之间进行通信,常用于父子进程。
父子进程之间进行通信也需要确保父子进程首先看到同一份资源,那么如何确保父子进程能够看到呢?每个进程都有自己的task_struct结构体,当父进程创建子进程的时候,子进程将会拷贝一份父进程的task_struct结构体,作为自己独有的数据。父进程内部的file_struct结构体里面的fd_array[]数组中保存的打开的文件操作符也会被子进程继承,子进程同样会复制一份。但是文件操作符指向的文件信息并不会被重新复制一份,两个进程中的文件操作符指向的还是同一份文件,此时我们发现,父子两个进程通过自己的文件操作符fd看到了同一份文件了。
两个进程都看到了同一份文件,我们只需要父进程向3号文件描述符指向的文件进行写入操作,子进程从3号文件描述符指向的文件进行读取操作,那么此时就进行了父子进程的通信。此时,这个共同看到的文件我们称之为匿名管道文件。这个文件的数据没必要刷新到磁盘中,在内存中进行数据的交换即可。
因此父子进程之间进行通信的方式我们可以做出以下总结:
(1)父进程以读写的方式打开文件
(2)fork创建子进程
(3)父子进程关闭各自不需要的文件操作符
下面我们进行代码的实现,完成父子进程的通信操作。
首先我们需要让父进程以读写的方式打开一个匿名管道文件,我们可以调用pipe函数实现。
创建匿名管道文件:int pipe(int fd [2]);
头文件:#include<unistd.h>
参数:fd:文件描述符数组,fd[0]表示读取的方式访问打开的文件,fd[1]表示写入的方式访问文件。
返回值:int类型,创建管道文件成功返回0,失败返回-1。
执行下列代码后我们就完成了管道文件的创建,接下来就需要创建子进程,并且子进程关闭写端,父进程关闭读端。这样我们就能进行管道的单向通信,父进程向管道文件内部进行写入,子进程进行数据的读取操作。
使用fork函数创建子进程,并且使用close函数关闭不需要的文件操作符。随后父进程拿着fd[1]文件操作符使用write函数将数据写入到管道文件中,子进程则拿着fd[0]使用read函数将数据从管道文件中进行读取,最终完成两个进程的通信操作。
具体代码如下:
int main()
{
//1.创建管道
int pipefd[2]={0};
int n = pipe(pipefd);
assert(n!=-1);
(void)n;
#ifdef DEBUGE
cout<<"pipefd[0]:"<<pipefd[0]<<endl;
cout<<"pipefd[1]:"<<pipefd[1]<<endl;
#endif
//2.创建子进程
pid_t id =fork();
assert(id!=-1);
//3.构建单向通信,父进程写入,子进程读取
if(id==0)
{
//子进程--读取
//关闭写入端
close(pipefd[1]);
char buffer[1024*8];
int count2=0;
while(1)
{
sleep(1);
//写入的一方,fd没有关闭,如果管道有数据就读,没有就等
//写入的一方,fd关闭,读取的一方read会返回0,表示读到文件结尾
ssize_t s= read(pipefd[0],buffer,sizeof(buffer)-1);
count2++;
if(count2==5)
{
cout<<"读端关闭"<<endl;
break;
}
if(s>0)//读取成功
{
buffer[s]=0;
cout<<"child get a message["<<getpid()<<"]"<<"Father#"<<buffer<<endl;
}
else if(s==0)
{
cout<<"writer quit(child)"<<endl;
break;
}
}
close(pipefd[0]);
exit(0);
}
//父进程--写入
//关闭读取端
close(pipefd[0]);
string message="我是父进程,我正在给子进程发消息";
int count=0;
char send_buffer[1024];
while(true)
{
snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
ssize_t m=write(pipefd[1],send_buffer,strlen(send_buffer));
sleep(1);
cout<<count<<endl;
if(count==10)
{
cout<<"writer quit(father)"<<endl;
break;
}
}
close(pipefd[1]);
pid_t ret=waitpid(id,nullptr,0);
assert(ret>0);
(void)ret;
return 0;
}
在这个示例代码中我们可以看到管道的以下特点:
管道是一个文件,具备读写权限,因此管道文件具备一定的访问控制。管道是基于文件的,文件的生命周期是随进程的,管道的生命周期也是随进程的,进程结束,管道也就关闭了。管道文件也具备一定的大小,写入的块,读取的慢,那么当管道文件写满的时候,管道文件将不能进行写入,需要读取数据后才能写入。而如果是写慢读块,当管道内没有读取的数据时,读取端将会等待写入端的写入。当写入端关闭,读取端将会读到0,表示文件结尾。当读取端关闭的时候,写入端还在写的时候,操作系统将会关闭写入端。
管道写入数据量的大小最好是不大于PIPE_BUF,这样会保证写入的原子性,当要写入的数据量大于PIPE_BUF时,Linux将不再保证数据的原子性。 管道通常都是半双工的,数据只能流向一个方向,如果需要双向通信则需要两个管道。
2.2命名管道
匿名管道只能进行具有共同祖先的进程间通信,当我们想在不相关的进程间进行通信时,此时匿名管道就没有作用了,我们需要命名管道进行通信。
命名管道其实就是一个特殊的文件,我们在学习文件系统的时候提到过,命名管道本质就是一个管道文件,该文件只存在于内存中,不会像磁盘中进行刷新。
命名管道与匿名管道的通信方式类似,两个进程需要先看到同一个管道文件,然后才能进行通信,这就需要我们在一个进程中创建管道文件。在命令行中,我们可以使用mkfifo+管道文件名称进行管道文件的创建。
但是我们不能在进程通信前手动创建管道文件进行通信,我们需要在进程中进行管道文件的创建。 这就需要我们使用mkfifo函数在进程中进行管道文件的创建。
创建命名管道文件:int mkfifo(const *filename,modr_t mode);
头文件:#include <sys/types.h> #include <sys/stat.h>
参数:filename为管道文件的路径和文件名称,mode为创建管道文件后文件的权限
返回值:成功创建管道文件返回0,失败返回-1
命名管道创建后在使用方式与使用普通文件类似,我们需要使用open函数打开文件才能进行数据的读写操作。如果是以读的方式打开,进程将会阻塞,等待写入进程的数据写入管道。如果是以写入打开管道文件,那么将会等待读取端打开。
使用命名管道时需要注意进程执行的顺序,创建管道文件的进程需要先执行,后面的进程才能使用管道文件。
命名管道实现客户端与服务端的通信代码:命名管道和共享内存 · 1a650d7 · 冰冰棒/Linux - Gitee.com
3.共享内存
共享内存是除了管道外另一种进程间执行通信的方式。管道是基于文件的方式进行通信,两个进程通过看到同一个文件,并对该文件进行读取和写入的操作来完成通信。共享内存是通过映射同一块物理空间来进行通信的。
每个进程都有自己独特的task_struct结构体,并且有自己的虚拟内存地址,并通过自己的页表映射到物理内存中。共享内存就是操作系统提供的一块内存空间,并且通过进程的页表将该空间映射到每个进程自己的虚拟地址空间中,这样每个进程就可以拿着这一块公共的地址进行数据的通信。
那么如何创建共享内存呢?这就需要调用函数shmget。
创建共享内存:int shmget(key_t key, size_t size, int shmflg);
头文件:#include<sys/shm.h>
参数:key,关键码,两个进程通过唯一的key找到同一块共享内存
size,共享内存的大小(最好是4096字节的倍数,如果不是系统申请的实际空间是倍数,
返回的空间则是你申请的大小)
shmflg,状态,含有两个参数IPC_CREAT,IPC_EXCL 。一般情况下填写0,表示直接
获取已经存在的共享内存。当单独使用IPC_CREAT时,表示创建共享内存,如果底层
已经存在,则直接获取它并返回,如果不存在就创建并返回。两个参数都使用则表示如
果底层不存在则创建并返回,如果底层存在则返回错误,这将保证我们获得的共享内存
一定是新创建的。单独使用IPC_EXIT没有意义。一般情况下我们还需要添加共享内存
的权限。
返回值:失败返回-1,成功返回共享内存的标识符。
通过函数我们知道,获取共享内存并不能单独的使用shmget函数进行获取,我们需要提前得到一个key,让不同的进程通过唯一的key来获得同一块共享内存。 系统提供了获取key的函数:
创建唯一的key:key_t ftok(const char *pathname, int proj_id);
头文件:#include <sys/types.h> #include <sys/ipc.h>
参数:pathname,文件路径,该路径并没有特殊的含义,我们只需要传入一个文件的路径即
可,函数将会根据路径的inode值进行计算得到key,传入的路径最好是自己有访问权
限。一定要确保该路径存在。
proj_id,函数会根据该id与路径一起创建一个唯一的key,该数值可以随意定义,通常取
值为1~255。
返回值:当函数执行成功,则会返回key_t键值,否则返回-1。
我们只需要在不同的进程中使用同一个ftok的参数进行创建,就会得到同一个key值。然后调用shmget就可以获得同一块共享内存空间。
共享内存创建后,我们需要获取到共享内存的地址才能进行进程间的通信,在通信完成后需要将进程与共享内存之间在断开连接,并且将共享内存释放掉。
连接共享内存的函数为shmat
将共享内存块连接到进程地址空间:
void *shmat(int shmid, const void *shmaddr, int shmflg);
头文件:#include <sys/ipc.h>
参数:shmid: 共享内存标识
shmaddr:指定连接的地址,默认为NULL
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY,默认为0shmaddr为NULL,核心自动选择一个地址 shmaddr不为NULL且shmflg无SHM_RND标
记,则以shmaddr为连接地址。 shmaddr不为NULL且shmflg设置了SHM_RND标记,
则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr %
SHMLBA) shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
返回值:成功返回一个指针,指向共享内存起始地址;失败返回-1。
挂接完成后我们将得到一个地址,我们可以将该地址类比为使用malloc函数申请的空间地址,我们可以在里面进行写入和读取。
操作完毕后需要将共享内存断开连接,需要使用shmdt函数。
将共享内存段与当前进程脱离 int shmdt(const void *shmaddr);
头文件:#include <sys/ipc.h>
参数: shmaddr: 由shmat所返回的指针。
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段。
当共享内存使用完毕后,我们需要将共享内存删除掉,如果没有删除共享内存,那么我们在进行运行的时候会报错。
例如进程异常退出后,共享内存并没有被删除,我们在运行该进程时会获取共享内存失败。
当我们使用ipcs -m 命令查看共享内存时,发现共享内存依然存在,并没有被删除。
我们可以手动使用命令ipcrm -m + shmid进行删除,也可以使用函数在进程中调用删除。
使用shmctl函数进行删除:
用于控制共享内存 int shmctl(int shmid, int cmd, struct shmid_ds *buf);
头文件:#include <sys/ipc.h>
参数: shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
切记,我们在那个进程中创建的共享内存就在那个进程中删除共享内存,其他的进程不需要删除。
通过以上的操作我们就完成了使用共享内存进行进程间通信的例子。
代码连接:命名管道和共享内存 · 1a650d7 · 冰冰棒/Linux - Gitee.com
小结:
key与shmid是不同的,可以值是在创建共享内存的时候用来在系统内部标识同一块内存的标识符,只在创建的时候才会用到。shmid是共享内存的身份id,进行操作的基本都是shmid。共享内存属于用户空间,并不需要使用系统调用就可以直接访问。使用共享内存通信,一方向内存中写入数据,另一方就会立刻看到,共享内存不需要过多的拷贝环节,是通信最快的。但是共享内存缺乏访问控制,会带来并发问题,我们可以使用管道进行控制。
进程间通信的前提都是让不同的进程看到同一份共同的资源,共享内存会带来一些时序问题,造成数据的不一致问题。我们把多个进程看到的公共资源称为临界资源。我们把自己进程访问临界资源的代码称为临界区。为了更好的保护临界区,可以让多个执行流在任何时候都只有一个执行流进入临界区,这就是互斥。在非临界区,资源之间互不干扰。
因此数据不一致的本质原因在于多个执行流互相运行时对临界资源不加保护的进行了访问,若想避免这一现象就得需要信号量来对进程访问的资源进行保护。
ipcs和ipcrm命令详解:
ipcs命令为查看共享内存,消息队列,信号量 | |
命令 | 功能 |
ipcs 或 ipcs -a | 查看消息队列、共享内存、信号量的使用情况 |
ipcs -s | 查看信号量(signal) |
ipcs -m | 查看共享内存(memory) |
ipcs -q | 查看消息队列(queue) |
ipcs -t | 输出详细时间变化 |
ipcs -p | 输出正在ipc的进程号 |
ipcs -c | 输出ipc对应信息的创建者 |
ipcs -l | 输出该系统ipc的限制信息 |
ipcs -u | 输出该系统ipc各种状态信息 |
ipcrm命令为删除消息队列、共享内存、信号量 | |
ipcrm -M shmkey | 移除shmkey创建的共享内存段 |
ipcrm -m shmid | 移除用shmid标识的共享内存段 |
ipcrm -m shmid | 移除用shmid标识的共享内存段 |
ipcrm -Q msgkey | 移除用msgkey创建的消息队列 |
ipcrm -q msqid | 移除用msqid标识的消息队列 |
ipcrm -S semkey | 移除用semkey创建的信号量集 |
ipcrm -s semid | 移除用semid标识的信号量集 |
4.信号量
信号量主要用于同步和互斥,是对临界资源的预定机制。每一个进程想进入临界资源,访问临界资源的一部分都需要先去申请信号量,信号量如同看电影时购买的电影票,电影票购买成功,电影院就会预料给你的位置,那么你去看电影位置就不会被其他人占据。信号量就是临界资源的门票,只要申请信号量成功,临界资源内部一定给进程预留了进程想要获取的资源,此时就不会出现数据访问混乱的问题。
信号量的本质是一个“计数器”,当进程进行资源申请时,计数器执行减减操作,当进程释放时,信号量将会进行加加操作。但是该“计数器”并不能简单的理解为一个全局变量,因为CPU在执行指令时有可能会随时进行切换,如果是一个全局变量,有可能进程1对其进行了加加操作,还没来得及写入就被切走,进程2对其进行减减操作,此时全局变量就会出现混乱,因此该计数器应该是原子性的,即没有中间状态,要么不做,要么做完。