本文目录
前述
我们可以在系统上使用ipcs
命令查看当前正在使用的进程通信方式。下面的内容是因为我自己本文章的写代码时所创建的。
一、linux 进程之间的通信种类
序号 | 通信方式 | 描述 |
---|---|---|
1 | 管道(无名管道、有名管道) | 无名管道允许亲缘关系进程间的通信。有名命名管道还允许无亲缘关系进程间通信。 |
2 | 信号 signal | 在软件层模拟中断机制,通知进程某事发生。它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一样的。 |
3 | 消息队列 Messge Queue | 是消息的链接表,包括 posix 消息队列和 SystemV 消息队列。它克服了前两种通信方式中信息量有限的缺点。 |
4 | 共享内存 Shared memory | 可以说是最有用的进程间通信方式,是最快的可用 ipc 形式。是针对其他通信机制运行效率较低而设计。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。 |
5 | 信号量Semaphore | 进程间同步。主要作为进程之间以及同一进程的不同线程之间的同步和互斥手段。 |
6 | 套接字 socket | 用于网络中不同机器间进程通信。 |
二、管道
1. 管道的概述
管道好比一条水管,有两个端口,一端进水,另一端出水。 管道是 Linux 进程间通信的一种方式,如管道命令 ls -l | grep anaconda3
,意思是从ls -l中搜索含有anaconda3的文件内容。
2. 什么是管道文件?
我们软件的管道文件也有两个端口,分别是读端和写端。进水可看成数据从写端被写入,出水可看数据从读端被读出。
3. 管道的特点
(1)管道通信是单向的,有固定的读端和写端;
(2)数据被进程在管道读出后,管道中的数据就不存在了;
(3)当进程去读取空管道的时候,进程会阻塞;
(4)当进程往满管道写入数据时,进程会阻塞;
(5)管道容量为 64KB;
4. 管道类型
管道类型分为无名管道、命名(有名)管道两类。无论是哪种管道我们都用 read、 write 函数来对管道进行读写。对于不同的管道类型有不同的方法。
对于无名管道,由于读端和写端处于血缘关系的进程中(同一个main函数中),所以必须要知道读写两端分别对应的文件描述符。
而对于命名(有名)管道,读端和写端处于毫无关系的两个进程中,所以需要创建一个文件作为管道,来对文件进行读写操作。注意:有名管道不支持创建在共享目录下,因为共享目录里属于windows系统,不支持此类文件。且管道文件大小为0,并不会存储内容,只是作为介质存在。
(1)无名管道(pipe)
无名管道用于在一个main里的进程中,必须是父子进程或兄弟进程(一个父进程创建的多个子进程之间的关系)。
问题:那么既然在同一个main函数里进程通信为什么一定要用管道呢?直接定义一个全局变量的文件进行读写不行吗?要知道使用fork创建进程时,会复制一份完全一样的内容,子进程对文件的读写并不会影响父进程中文件的状态。那既然这样使用vfork创建进程不就行了吗?子进程和父进程共享同一份文件。但是使用vfork创建的进程,必须子进程结束后,才会执行父进程。如果想要读写两端同时进行的话,这种方式显然不行。所以就引入了无名管道进行这类进程之间的通信。
例如创建无名管道时,我们使用pipe()
来创建无名管道。对于无名管道的读写文件描述符我们通常保存在一个有两个整型元素的数组中,如 int fds[2]。然后调用函数 pipe(fds),这个函数会创建一个管道,并且数组 fds 中的两个元素会成为管道读端和写端对应的两个文件描述符。即 fds[0]为读端文件描述符, fds[1]为写端文件描述符。
无名管道的特点:
① 只能在亲缘关系进程间通信(父子或兄弟)。
② 半双工(固定的读端和固定的写端)。
③ 它是特殊的文件,可以用 read、write 等函数操作,这种文件只能在内存中。
管道两端的关闭是有先后顺序的。如果先关闭写端则从另一端读数据时,read 函数将返回 0,表示管道已经关闭;但是如果先关闭读端,则从另一端写数据时,将会使写数据的进程接收到 SIGPIPE 信号,如果写进程不对该信号进行处理,将导致写进程终止,如果写进程处理了该信号,则写数据的 write 函数返回一个负值,表示管道已经关闭。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc,char **argv)
{
int rec;
int pipefd[2]; //pipefd[0]读 pipefd[1]写
rec= pipe(pipefd);
if(rec < 0)
{
printf("error!\n");
}
pid_t pid;
pid=fork(); //创建进程
int i=0;
if(pid==0)
{ //子进程写
char buff[64];
while(1)
{
i++;
memset(buff,0,strlen(buff)); //清空数组
fgets(buff, sizeof(buff),stdin); //从终端获取字符给buff
write(pipefd[1], buff, sizeof(buff)); //将buff数组写入管道
if(i==3) break;
}
close(pipefd[1]); //关闭管道
close(pipefd[0]);
exit(0); //退出子进程
}
else if(pid>0)
{ //父进程读
char data[64];
int n;
wait(&n); //等待子进程结束,防止成为僵尸进程。
while(1)
{ i++;
memset(data,0,strlen(data));
read(pipefd[0], data, sizeof(data)); //从管道中读取内容传给data数组
printf("%s",data);
if(i==3) break;
}
close(pipefd[1]);
close(pipefd[0]);
exit(0);
}
else
{
printf("error!\n");
}
}
(2)有名(命名)管道(fifo)
无名管道只能在亲缘关系的进程间通信,这大大限制了管道的使用,有名管道突破了这个限制,通过指定路径名的形式实现不相关进程间的通信。
这里我们使用两个不相关的进程分别来进行通信,即使用两个命令窗口分别运行管道读端的程序和写端的程序。首先我们需要在写端使用命令mkfifo
创建管道。其第一个参数为创建的管道文件名,第二个参数为文件的权限。
FIFO管道必须读写两端都要打开。打开管道文件时默认选择为阻塞模式,则没有进程打开 FIFO 进行读取,写操作将会被阻塞,直到有进程打开 FIFO 进行读取为止,反之也一样。但是FIFO 本身有一个内核缓冲区,写入的小量数据可能会暂存在缓冲区中,等待读端读取。如果选择非阻塞模型打开(O_NONBLOCK
),则会open调用会立即返回失败。
问题:既然是创建文件作为管道,那么这个管道文件和普通文件有什么区别呢?
答:普通文件:用于存储数据,数据可以随机访问,可以读写多次,数据在文件关闭后依然存在。命名管道:用于进程间通信,数据是流式的,主要用于一次性读写,数据在被读取后即被移除,不持久存储。数据以流的形式传输,写入的数据只能按顺序读取,通常一个进程写入数据后,另一个进程立即读取。
●有名管道写端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
// 有名管道:用于两个无亲属关系的进程间的通信。 例如fifo_w 和fifo_r 间的通信。
int main(int argc, char **argv)
{
int fd;
char buff[64];
//创建一个有名管道文件,文件权限为可读可写
mkfifo("/tmp/fifo.cmd",0666);
//系统io来打开文件 ,不是标准io。打开有名管道的写端。
fd=open("/tmp/fifo.cmd",O_WRONLY);
if(fd < 0) printf("error!\n");
while(1)
{
memset(buff, 0, sizeof(buff));
fgets(buff, sizeof(buff), stdin); //从终端获取字符给buff
write(fd, buff, strlen(buff));
}
close(fd);
//删除有名管道文件
// unlink("/tmp/fifo.cmd");
return 0;
}
●有名管道读端
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
//管道文件不支持创建在共享目录下,因为共享目录也属于windows
//有名管道:无亲属关系的进程间通信(不在同一个main函数中的进程)
// fopen:标准io open:系统io
int main(int argc, char **argv)
{
int fd;
char buff[64];
//打开有名管道的读端(因为写端已经创建了管道文件,所以读端只需要打开就行)
fd = open("/tmp/fifo.cmd", O_RDONLY);
if(fd < 0) printf("error!\n");
while(1)
{
memset(buff,0,sizeof(buff));
read(fd, buff, sizeof(buff)); //read 必须用 sizeof!!
printf("%s",buff);
}
close(fd);
//删除有名管道文件
// unlink("/tmp/fifo.cmd");
}
三、信号(signals)
在Linux中,信号是一种进程间通信机制,用于通知进程某些事件的发生。信号是一种异步的通知机制,当一个进程接收到信号时,可以选择处理该信号、忽略它或执行默认的操作。信号在系统编程中非常重要,常用于控制进程行为、处理异常情况和执行进程间通信。使用kill -l
查看所有的信号。
部分信号如下:
对信号进行处理时,使用信号名称和使用信号编号效果相同。
1. 信号发送相关函数
(1)向指定进程发送信号
在函数中可以使用下面代码,在命令行中可以使用kill -信号编号 进程PID号
,来向指定的进程发送指定的信号。我们可以在命令行使用ps -ef
来查看当前所有进程的详细信息。
int kill(pid_t pid, int sig);
/*当 pid>0 将信号发送给指定进程;
当 pid==0 时,将信号发送给同组进程;
当 pid<0 时,将信号发送给进程组 ID 等于 pid 绝对值的进程;
当 pid==-1 时,将信号发送给所有进程;
int sig:信号指令(类型),如 SIGQUIT
*/
(2) 向进程自己发送信号
int raise(int sig);
(3)挂起调用该函数的进程,直到捕获到了一个信号。
int pause(void);
2. 信号接收处理
该函数用于将一个函数与信号进行绑定。即当接收到某个信号时,会执行信号绑定的函数,而不是之前默认的处理行为。举例:假如我们将Ctrl+c的信号与输出hello的函数进行绑定后,那么我们进程接收到Ctrl+c信号后并不会结束进程,而是执行绑定的函数。如果不进行绑定操作,则接收到信号后,使用默认的处理行为。
sighandler_t signal(int signum, sighandler_t handler);
//int signum:要捕获或处理的信号编号。
//sighandler_t handler :接收到指定信号后要执行的函数。
使用例程:当按下Ctrl+c时会触发自定义函数,打印出signal:2。可以使用Ctrl+z结束该进程。
#include <stdio.h>
#include <signal.h>
void fun(int arg) //自定义函数
{
printf("signal:%d\n",arg);
}
int main(int argc,char **argv)
{
//当进程收到2号信号时,执行自定义的处理函数,Ctrl+c信号编码为2。
signal(2, fun);
while(1)
{
}
}
四、消息队列
1. Linux中的消息队列有两种类型
System V消息队列(传统消息队列)
POSIX消息队列(现代消息队列)
2. 消息队列与有名管道(FIFO)的异同点
(1)相同点:
消息队列与 FIFO 很相似,都是一个队列结构,都可以有多个进程往队列里面写信息,多个进程从队列中读取信息。
(2)不同点:
FIFO 需要读、写的两端事先都打开,才能够开始信息传递工作。而消息队列可以事先往队列中写信息,需要时再打开读取信息。每读取一次,消息队列内容-1。
3. System V IPC 机制消息队列相关函数
(0)包含的头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
(1)定义信息包结构体
//数据结构体
typedef struct{
char buff[10];
int number;
float sorce;
} my_data;
//信息包结构体
struct msgbuf{
long mtype; //消息的类型。
my_data data; //消息包
};
(2)创建密钥-关键字
用于生成一个唯一的键(key),它基于文件路径和一个项目标识符(proj_id)来生成这个键值。通常用于创建System V IPC对象,如共享内存、信号量和消息队列。 成功的时候,返回密钥值。失败返回-1。
key_t ftok(const char *pathname, int proj_id);
//const char *pathname :文件的路径全称且文件必须存在。
// int proj_id :非0的唯一识别键值id,不要重复。
(3)创建和访问一个消息队列
成功的时候,返回一个消息队列的唯一标识符 id(跟进程 ID 是一个类型)。失败返回-1。
如果是亲缘关系的进程,就不需要步骤2创建密钥,直接把key改为IPC_PRIVATE
即可。
int msgget(key_t key, int msgflg);
//key_t key:上一步生成的密钥。
//msgflg:指明队列的访问权限和创建标志。创建标志的可选值为 IPC_CREAT 和 IPC_EXCL。队列权限自定义。
(3)将消息添加到消息队列中
int msgsnd(int msqid, struct msgbuf * msgp, size_t msgsz, int msgflg);
//int msqid :步骤3返回的消息队列的id。
//struct msgbuf * msgp :发送信息的结构体
//size_t msgsz :消息包的大小
//int msgflg:可以为 0(通常为 0)或 IPC_NOWAIT。
(4)从消息队列中读取消息
ssize_t msgrcv(int msqid, struct msgbuf * msgp, size_t msgsz, long msgtyp, int msgflg);
//int msqid :步骤3返回的消息队列的id。
//struct msgbuf * msgp :发送信息的结构体
//size_t msgsz :消息包的大小
// long msgtyp :要接收的消息类型。如果指定为0,则接收队列中的第一条消息。如果大于0,则接收队列中第一个类型字段等于 msgtyp 的消息。如果小于0,取绝对值。通常,msgtyp 是一个正整数,用于区分不同类型的消息。
//int msgflg:可以为 0(通常为 0)或 IPC_NOWAIT。
(4)删除消息队列
int msgctl(int msqid, int cmd, struct msqid_ds * buf);
/*
msqid 是由 msgget 返回的消息队列标识符。
cmd 通常为 IPC_RMID 表示删除消息队列。
buf 通常为 NULL 即可。
*/
4. 使用例程
●消息队列写端:每执行一次该程序就把消息发送出去一次,一直累加到消息队列中,等待读取。
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/msg.h>
//定义 发送消息结构体
typedef struct{
char buff[10];
int number;
float score;
} my_data;
struct msgbuf{
long mtype; //消息的类型。
my_data data; //消息包
};
int main(int argc, char ** argv)
{
key_t key;
int ret;
struct msgbuf mbuf[2]; //发送两个消息包
int msgid;
//创建密匙(文件的路径全称,文件必须存在)
key=ftok("key.txt", 1);
if(key <0){
perror("error\n");
return -1;
}
//创建或访问消息队列
//消息队列的id,密钥,不存在时创建|读写权限
msgid = msgget(key,IPC_CREAT | 0666);
if(msgid <0){
perror("msgget error\n");
return -1;
}
//消息的类型,用于区分消息包的。
mbuf[0].mtype=3;
//填充消息体
mbuf[0].data.number=121;
mbuf[0].data.score=12.31;
strcpy(mbuf[0].data.buff,"i love you!");
// 填充第二个消息体
mbuf[1].mtype = 6;
mbuf[1].data.number = 11;
mbuf[1].data.score = 1.31;
strcpy(mbuf[1].data.buff, "you!");
// 循环发送每个消息
for (int i = 0; i < 2; i++) {
ret = msgsnd(msgid, &mbuf[i], sizeof(mbuf[i].data), 0);
if (ret < 0){
perror("msgsnd error\n");
return -1;
}
}
//删除消息队列
//msgctl(msgid, IPC_RMID, NULL);
return 0;
}
执行一次写端后,我们查看消息队列的详细i信息发现待读取消息数目变为了2。因为我们每次写入两个数据包。
●消息队列读端:每执行一次,就从消息队列中读取一次数据,消息队列中消息-1,直至读完。
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/msg.h>
//定义 接收消息结构体
typedef struct{
char buff[10];
int number;
float score;
} my_data;
struct msgbuf{
long mtype; //消息的类型。
my_data data; //消息包
};
//用命令:ipcs 查看消息队列内容
int main(int argc, char ** argv)
{
key_t key;
int ret;
struct msgbuf mbuf;
int msgid;
//创建密匙(文件的路径全称,文件必须存在)
key=ftok("key.txt", 1);
if(key <0){
perror("error\n");
return -1;
}
//创建或访问消息队列
//消息队列的id,密钥,不存在时创建|读写权限
msgid = msgget(key,IPC_CREAT | 0666);
if(msgid <0){
perror("msgget error\n");
return -1;
}
//消息队列的id,读取消息的缓冲区,消息体的大小,读取类型为3的消息包,0
ret=msgrcv(msgid, &mbuf, sizeof(mbuf.data), 6 , 0 );
if(ret < 0){
perror("msgsnd error\n");
return -1;
}
printf("%d\n",mbuf.data.number);
printf("%f\n",mbuf.data.score);
printf("%s\n",mbuf.data.buff);
//删除消息队列
//msgctl(msgid, IPC_RMID, NULL);
return 0;
}
执行一次读端代码,因为我们读取的是信息类型为6的数据包,所以获取的数据包信息如下:
此时查看发现,待读取数据包数量变为了1。
如果此时我们再次执行读取程序时,发现程序堵塞。这是因为我们信息包类型为6的数据已经被读取完了。
五、共享内存
共享内存也是进程间(进程间不需要有继承关系)通信的一种常用手段。一般操作系统( OS) 通过内存映射与页交换技术,使进程的内存空间映射到不同的物理内存,这样能保证每个进程运行的独立性,不至于受其它进程的影响。但可以通过共享内存的方式,使不同进程的虚拟内存映射到同一块物理内存,一个进程往这块物理内存中更新的数据,另外的进程可以立即看到这块物理内存中修改的内容。多个进程可以直接读写共享的内存区域,不需要进行数据的复制或者传递。
我们可以先申请共享内存的大小,然后就可以往这个内存里写各种类型的数据,写入数据时确保数据在共享内存中的位置和布局是已知的,从而能够正确地读取这些数据。读取共享内存中的数据不会导致数据的删除或修改,除非显式地进行了删除或修改操作。
1. 共享内存原理
(1)进程间需要共享的数据被放在该共享内存区域中。
(2)所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。
(3)这样一个使用共享内存的进程可以将信息写入该空间,而另一个使用共享内存的进程又可以通过简单的内存读操作获取刚才写入的信息,使得两个不同进程之间进行了一次信息交换,从而实现进程间的通信。
(4)共享内存允许一个或多个进程通过同时出现在它们的虚拟地址空间的内存进行通信,而这块虚拟内存的页面被每个共享进程的页表条目所引用,同时并不需要在所有进程的虚拟内存都有相同的地址。
(5)进程对象对于共享内存的访问通过 key(键)来控制,同时通过 key 进行访问权限的检查。
2. 共享内存特点
共享内存是最快的一种通信方式,适合大量数据的传输。只要创建的密钥一样,就可以共享内存空间。如果没有亲缘关系的进程使用共享文件,则需要密钥。如果有亲缘关系的进程使用共享文件,不需要创建密钥。把key改为IPC_PRIVATE
即可。
3. 相关函数
(0)包含的头文件
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
(1)创建密钥-关键字
用于生成一个唯一的键(key),它基于文件路径和一个项目标识符(proj_id)来生成这个键值。通常用于创建System V IPC对象,如共享内存、信号量和消息队列。 成功的时候,返回密钥值。失败返回-1。
注意:如果两个进程使用同一个共享内存,那么要保证文件路径和id都一样时,才能共享同一片内存地址。
key_t ftok(const char *pathname, int proj_id);
//const char *pathname :文件的路径全称且文件必须存在。
// int proj_id :非0的唯一识别键值id,不要重复!
(2)创建/打开共享内存
成功则返回一个该共享内存段的唯一标识号(唯一的标识了这个共享内存段)。否则返回-1。
如果是亲缘关系的进程,就不需要步骤2创建密钥,直接把key改为IPC_PRIVATE
即可。
int shmget(key_t key, int size, int shmflg);
/*key : 是一个与共享内存段相关联的关键字。
Size: 指定共享内存段的大小,以字节为单位。
Shmflg:是一掩码合成值,可以是访问权限值与(IPC_CREAT 或 IPC_EXCL)的合成。
IPC_CREAT 表示如果不存在该内存段,则创建它。
IPC_EXCL 表示如果该内存段存在,则函数返回失败结果(-1)。
*/
(3)映射到进程空间地址
这里函数返回的是空类型的指针,即可以转换为任意类型的指针。因此,你可以根据共享内存段中存储的数据类型,将返回值转换为相应类型的指针。如果调用成功,返回映射后的进程空间的首地址,否则返回(void*)-1。
void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
Shmid:共享内存段的标识 通常应该是 shmget 的成功返回值。
Shmaddr:共享内存连接到当前进程中的地址位置。通常是 NULL,表示让系统来选择共享内存出现的地址。
Shmflg:一组位标识,通常为 0 即可。
*/
(4)向共享内存读/写数据。
这里写入数据的大小不要超过申请的共享内存的大小,写入的数据最好按内存存储的顺序写入,但是这不是必须的,你可以第一个数据写到内存开头,第二个数据写到内存结尾处。只要你能找到数据在内存中的具体位置即可。读取时按存储时的具体位置进行读取即可。
(5)共享内存段与进程空间分离
当本进程对该共享内存操作完毕后,记得要与本进程空间分离。如果进程不分离共享内存,那么操作系统会认为进程仍然在使用该内存段,即使实际上不再需要它。这会导致内存资源不能被重新利用,导致资源泄漏。
将共享内存分离并没删除它,只是使得该共享内存对当前进程不再可用。 成功返回 0,失败时返回-1
int shmdt(const void *shmaddr);
//shmaddr 为 shmat 的成功返回值。
(6)删除共享内存段
共享内存是系统级的资源。如果进程不分离共享内存并且不删除共享内存段(使用 shmctl 函数的 IPC_RMID 命令),那么即使所有进程都退出,系统仍然保留这段内存。长期下去,会导致系统的可用共享内存资源减少,可能会影响其他需要使用共享内存的应用程序的正常运行。所以当这段共享内存不再使用时,记得删除,
成功返回 0,失败时返回-1。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
Shmid:共享内存段标识 通常应该是 shmget 的成功返回值。
Cmd:对共享内存段的操作方式。
可选为 IPC_STAT,IPC_SET,IPC_RMID。
通常为 IPC_RMID,表示删除共享内存段。
Buf:共享内存段的信息结构体数据,通常为 NULL。
*/
4. 使用例程
我们将共享内存填入下面不同格式的数据,完成读写操作。
●写入共享内存
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
typedef struct {
int id;
char name[50];
} MyStruct;
#define SHM_SIZE 1024 //定义共享内存的大小
int main(int argc, char **argv)
{
key_t key;
int shmid;
void *shared_memory;
//1. 创建密匙(文件的路径全称,文件必须存在)
key=ftok("1.txt", 11);
if(key <0){
perror("error\n");
return -1;
}
//2. 所创建/打开的共享内存的id,(密匙,空间大小,打开的空间不存在时创建)
shmid=shmget(key, SHM_SIZE, IPC_CREAT|0644);
if(shmid<0) {
perror("error\n");
return -1;
}
//3. 将共享内存段附加到进程的地址空间
shared_memory = shmat(shmid, NULL, 0);
if (shared_memory == (void *) -1) {
perror("shmat");
exit(1);
}
// 4. 将数据存储到共享内存中
int *int_ptr = (int *)shared_memory;
*int_ptr = 42;
float *float_ptr = (float *)(shared_memory + sizeof(int));
*float_ptr = 3.14;
char *string_ptr = (char *)(shared_memory + sizeof(int) + sizeof(float));
strcpy(string_ptr, "Hello, Shared Memory!");
MyStruct *struct_ptr = (MyStruct *)(shared_memory + sizeof(int) + sizeof(float) + 100); //+100是因为确保在此结构体之前已经留出足够的空间完成字符串的写入。
struct_ptr->id = 1;
strcpy(struct_ptr->name, "Shared Memory Struct");
//5.将共享内存空间从本进程中分离
shmdt(shared_memory);
//6. 删除共享内存空间
// shmctl(shmid,IPC_RMID,NULL);
return 0;
}
●读取共享内存
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
typedef struct { //对齐机制,这个结构体占56个字节的大小。
int id;
char name[50];
} MyStruct;
#define SHM_SIZE 1024 //定义共享内存的大小
int main(int argc, char **argv)
{
key_t key;
int shmid;
void *shared_memory;
//1. 创建密匙(文件的路径全称,文件必须存在)
key=ftok("1.txt", 11);
if(key <0){
perror("error\n");
return -1;
}
//2. 所创建/打开的共享内存的id,(密匙,空间大小,打开的空间不存在时创建)
shmid=shmget(key, SHM_SIZE, IPC_CREAT);
if(shmid<0) {
perror("error\n");
return -1;
}
//3. 将共享内存段附加到进程的地址空间
shared_memory = shmat(shmid, NULL, 0);
if (shared_memory == (void *) -1) {
perror("shmat");
exit(1);
}
// 4. 从共享内存中按顺序读取数据
int *int_ptr = (int *)shared_memory;
printf("Integer: %d\n", *int_ptr);
float *float_ptr = (float *)(shared_memory + sizeof(int));
printf("Float: %f\n", *float_ptr);
char *string_ptr = (char *)(shared_memory + sizeof(int) + sizeof(float));
printf("String: %s\n", string_ptr);
MyStruct *struct_ptr = (MyStruct *)(shared_memory + sizeof(int) + sizeof(float) + 100);
printf("Struct ID: %d, Name: %s\n", struct_ptr->id, struct_ptr->name);
//5. 分离共享内存段
if (shmdt(shared_memory) == -1) {
perror("shmdt");
exit(1);
}
return 0;
}
读取到的内容:
六、信号量
信号量(也叫信号灯)是一种用于提供不同进程间或一个给定进程的不同线程间同步手段的原语。一般还用来对某个共享资源的进行访问控制。信号量是进程/线程同步的一种方式,有时候我们需要保护一段代码,使它每次只能被一个执行进程/线程运行,这种工作就需要一个二进制开关;有时候需要限制一段代码可以被多少个进程/线程执行,这就需要用到关于计数信号量。信号量开关是二进制信号量的一种逻辑扩展,两者实际调用的函数都是一样。
1. 相关函数
(1)头文件
#include <sys/sem.h>
#include <sys/ipc.h>
#include <sys/types.h>
(2)创建密钥。
用于生成一个唯一的键(key),它基于文件路径和一个项目标识符(proj_id)来生成这个键值。通常用于创建System V IPC对象,如共享内存、信号量和消息队列。 成功的时候,返回密钥值。失败返回-1。
key_t ftok(const char *pathname, int proj_id);
//const char *pathname :文件的路径全称且文件必须存在。
// int proj_id :非0的唯一识别键值id,不要重复!
(3)创建信号量集
成功时,返回一个称为信号量集标识符的整数,semop 和 semctl 会使用它;出错时,返回-1。
int semget(key_t key,int nsems,int flag);
//key_t key :密钥
//int nsems :信号量集的个数
//int flag :访问权限和标志。IPC_CREAT|0666。
(4) 改变信号量对象中各个信号量的状态。
int semop(int semid,struct sembuf *sops, size_t nops);
//1)semid:由 semget 返回的信号量标识符 ID。
//2)Sops: 指向一个结构体数组的指针。
//3)nops: 为 sops 指向的 sembuf 结构数组的长度。
下面对该函数的第二个参数解析: 该结构体已经在头文件中被定义,所以不需要我们重新定义。我们需要了解一下这个结构体里有哪些参数以及其作用,
struct sembuf{
short sem_num; //要操作的信号量在信号量集中的编号,第一个信号量的编号是 0。
//通常只会用到两个值,一个是-1,即将信号量数量-1。一个是1,也就是加一操作,将信号量数量+1。
short sem_op; //sem_op 成员的值是信号量在一次操作中需要改变的数值。
//通常设为: SEM_UNDO,程序结束,信号量为 semop 调用前的值。
short sem_flg;
}
(5)用来直接控制信号量信息。
int semctl(int semid, int semnum, int cmd, union semun arg);
//int semid:创建信号量集时返回的id。
//int semnum :要进行操作的集合中信号量的编号,当要操作到成组的信号量时,从 0 开始。
/*int cmd :
IPC_RMID(立即删除信号集,唤醒所有被阻塞的进程)、
GETVAL(根据 semun 指定的编号返回相应信号的值, 此时该函数返回值就是你要获得的信号量的值,不再是 0 或-1)、
SETVAL(根据 semun 指定的编号设定相应信号的值)、
GETALL(获取所有信号量的值, 此时第二个参数为 0, 并且会将所有信号的值存入
semun.array 所指向的数组的各个元素中,此时需要用到第四个参数 union semun arg)、
SETALL(将 semun.array 指向的数组的所有元素的值设定到信号量集中, 此时第二个参数为 0,此时需要用到第四个参数 union semun arg)等。
*/
//union semun arg:是一个 union semun 类型(具体的需要由程序员自己定义)
上面函数的第四个参数需要我们自己定义,并且至少要包含下面的内容。
union semun{
int val; //用于 SETVAL 命令,设置单个信号量的值。
struct semid_ds *buf; 用于 IPC_STAT 和 IPC_SET 命令,获取或设置信号量集的属性
unsigned short *array; 用于 GETALL 和 SETALL 命令,获取或设置信号量集中的所有信号量的值
};
2. 例程
作用:
写端设置信号量的数目,并且设置信号量初值。当在终端输入一次时,信号量数量+1。
读端读取信号量,每读一次使得信号量数量-1。当信号量数量为0时,堵塞进程。等待信号量数量非0时继续读取。
●写端
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/sem.h>
//声明的联合体。至少以下内容:
union semun{
int val; //信号量初值
struct semid_ds *buf;
unsigned short *array;
};
//对信号量+1 操作。
void sem_add(int semid, int num)
{
struct sembuf sem; //这个结构体为头文件里包含的。
sem.sem_num =num; //待操作的信号量在集合中的编号
sem.sem_op = 1; //对信号量+1。
sem.sem_flg =SEM_UNDO; //执行semop时不改变sem的值
semop(semid, &sem, 1); //这里的1为操作信号量的数量。这里我们只操作信号编码为2的信号量,所以为1。
}
int main(int argc, char ** argv)
{
key_t key;
int semid;
char buff[64]={0};
union semun sem_led;
//创建密匙(文件的路径全称,文件必须存在)
key=ftok("key.txt", 2);
if(key <0){
perror("error\n");
return -1;
}
//创建一个信号量集,包含3个信号量。编号为0、1、2。
//密钥,信号量的数量,不存在时创建|只读写权限
semid= semget(key, 3 ,IPC_CREAT |0666);
if(semid < 0){
perror("semget error\n");
return -1;
}
sem_led.val=10; //初始化信号量的初值
semctl(semid, 1, SETVAL, sem_led); //将信号量初值与编号为1信号量进行绑定。因为我们使用SETVAL命令,所以只需要对val操作即可。
sem_led.val=5; //初始化信号量的初值
semctl(semid, 2, SETVAL, sem_led); //将信号量初值与编号为2信号量进行绑定。因为我们使用SETVAL命令,所以只需要对val操作即可。
while(1)
{
fgets(buff,sizeof(buff),stdin); //终端输入一次任意数据后,将编号为2的信号量值加1。
sem_add(semid, 1); //传入id,以及信号量的编号。
sem_add(semid, 2); //传入id,以及信号量的编号。
}
//删除信号量集
//semctl(semid, 0, IPC_RMID, NULL);
return 0;
}
●读端
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <sys/shm.h>
#include <string.h>
#include <sys/msg.h>
#include <sys/sem.h>
#include <unistd.h>
//信号量主要是为了保护共享资源
union semun{
int val;
struct semid_ds *buf;
unsigned short *array;
};
void sem_sub(int semid, int num)
{
struct sembuf sem;
sem.sem_num =num; //待操作的信号量在集合中的编号
sem.sem_op = -1; //访问
sem.sem_flg =SEM_UNDO; //执行semop时不改变sem的值
semop(semid, &sem, 1);
if(num==1) printf("信号量编号1:进入!\n");
else printf("信号量编号2:进入!\n");
}
int main(int argc, char ** argv)
{
key_t key;
int semid;
//创建密匙(文件的路径全称,文件必须存在)
key=ftok("key.txt", 2);
if(key <0){
perror("error\n");
return -1;
}
//打开/创建信号量集。因为我们在写端已经创建,所以这里我们设置信号量数量为0。
semid= semget(key, 0, IPC_CREAT |0666);
if(semid < 0){
perror("semget error\n");
return -1;
}
while(1)
{
sem_sub(semid, 1); //每执行一次,信号量-1. 一直执行到信号量为0停止。
// sem_sub(semid, 2); //每执行一次,信号量-1. 一直执行到信号量为0停止。
sleep(1);
}
//删除信号量集
//semctl(semid, 0, IPC_RMID, NULL);
return 0;
}