Linux系统学习——进程间的通信方式

导读:本篇文章讲解 Linux系统学习——进程间的通信方式,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

Linux系统学习——进程间的通信方式

进程间的通信又称为IPC,指不同的进程间传送消息和交换消息

IPC的方式通常有管道(包括无名管道和命名管道)消息队列信号量共享存储SocketStreams等。其中Socket和Streams支持不同主机上的两个进程IPC。

一、管道

管道通常指的是无名管道

1.特点

  1. 无名管道类似于一条水管,水只能从一端流入,另一端流出,而水管内部不储存水,具有固定的读端和写端
  2. 它只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)
  3. 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中

2.函数原型

 #include <unistd.h>
 int pipe(int pipefd[2]);
返回值为:-1即为创建管道失败,0为创建管道成功

当一个管道建立时,会生成两个文件描述符,即fd[0]为读取而打开管道,fd[1]为写入而打开管道,要关闭管道,只要把两个文件描述符用close关闭即可

3.例子

数据想要从父进程流向子进程,只需要关闭在父进程上读端( fd[0] ) 和子进程上的写端 ( fd[1] ),反之从子进程流向父进程,操作相反即可

代码演示:

  1 #include<stdio.h>
  2 #include <unistd.h>
  3 #include<string.h>
  4 #include<stdlib.h>
  5 
  6 int main()
  7 {
  8         int fd[2];  //创建两个文件描述符
  9         int pid; 
 10         char buf[128];
 11 
 12         if(pipe(fd) == -1)  // 检测管道是否创建失败
 13         {
 14                 printf("open guan dao failed\n");
 15         }
 16 
 17         pid = fork();  // 创建子进程
 18  
 19         if(pid < 0)
 20         {
 21                 printf("open jin cheng error\n");
 22         }
 23         else if(pid >0)  //数据在父进程上流向子进程
 24         {
 25                 sleep(3);
 26                 printf("This is father jincheng\n");
 27                 close(fd[0]);  //关闭读入端
 28                 write(fd[1],"liuzheng hen shuai",strlen("liuzheng hen shuai")); //打开写入端
 29                 wait();
 30         }
 31         else{   
 32                 printf("This is child jincheng\n");
 33                 close(fd[1]); //关闭写端
 34                 read(fd[0],buf,128);  //打开读端
 35 
 36                 printf("read father :%s\n",buf);
 37                 exit(0);
 38         }
 39				return 0;
 40 }

二、FIFO(命名管道)

FIFO,称为命名管道,是一种文件类型

1、特点

1. FIFO可以在无关的进程之间交换数据,与无名管道不同。

2. FIFO有路径名与之关联,它以一种特殊设备文件形式存在于文件系统中。

3. 在数据读出时,FIFO管道中同时清除数据,并且"先进先出"。

2.函数原型

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
返回值:创建成功返回 0 ,失败返回 -1,可通过 perror() 打印出 失败原因

其中

1、pathname :文件命名路径
2、 mode参数 和 open函数中的 mode参数相同, 指可以用一般文件I/O函数操作它(0600:可读可写、0777: 可读可写可执行)

特别注意

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  • 若没有指定O_NONBLOCK(默认设置),只读 open 要阻塞到某个其他进程为写而打开此 FIFO(就是你在这边进程读open,此时是阻塞情况不会有数据出现,需要使用另一个进程去写write才能打开这个阻塞)。类似的,只写 open 要阻塞到某个其他进程为读而打开它

  • 若是指定了O_NONBLOCK下,若是只读open,会立刻输出数据,但该数据跟我们设定的可能会不一样,需要在另一端打开写write进程,才能得到我们预先设定的数据结果

3.例子演示

read.c

  1 #include<stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 #include<errno.h>
  6 
  7 int main()
  8 {
  9         char buf[30] = {0};
 10 
 11         int nread = 0;
 12         int cnt = 0;
 13 
 14         if( (mkfifo("./file1",0600)==-1) && errno != EEXIST) //创建fifo ,EEXIST 检查 文件是否已经存在
 15         {
 16                 printf("create file1 failed\n");
 17                 perror("why:"); //创建fifo失败,则打出错误原因
 18         }
 19 									// O_RDONLY 只读打开文件
 20         int fd = open("./file1",O_RDONLY|O_NONBLOCK); // 设置O_NONBLOCK,只要open读打开,会立马获取数据
 21 
 22         printf("open success!\n");
 23 
 24         while(1)
 25         {
 26                 nread = read(fd,buf,30);
 27                 sleep(2);
 28 
 29                 printf("read %d from fifo , context fifo is %s\n",nread,buf);
 30 
 31                 cnt++;
 32                 if(cnt == 10)
 33                 {
 34                         break;
 35                 }
 36         }
 37         close(fd); //关闭文件
 38         return 0;

write.c

  1 #include<stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/stat.h>
  4 #include <fcntl.h>
  5 #include<string.h>
  6 int main()
  7 {
  8         int cnt = 0;
  9         char *str = "Liuzheng from fifo";
 10 
 11         int fd = open("./file1",O_WRONLY); //O_WRNOLY 文件只写打开
 12 
 13         printf("write open success!\n");
 14         while(1)
 15         {
 16                 write(fd,str,strlen(str)); //写入数据,流向open端,在open读那边打开
 17                 sleep(2);
 18                 cnt++;
 19                 if(cnt == 10)
 20                 {
 21                         break;
 22                 }
 23 
 24         }
 25         close(fd);
 26         return 0;
 27 }

运行结果:

(1)设置了O_NONBLOCK情况下:

没有打开write端之前:
在这里插入图片描述open端由于设置了O_NONBLOCK 没有阻塞

打开write端后

在这里插入图片描述
在这里插入图片描述
(2)没有设置O_NONBLOCK

打开read
在这里插入图片描述
没有数据,此时是阻塞状态

打开write

在这里插入图片描述

在这里插入图片描述

三、消息队列

消息队列,是消息的链接表,存放在内核中。一个消息队列由一个标识符 (即队列ID) 来标识

1.特点:

 1. 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
 2. 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除
 3. 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取

2.函数原型:

//头文件
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/ipc.h>


// 创建或打开消息队列: 成功返回队列ID,失败返回-1
int msgget(key_t key, int flag);

// 添加消息: 成功返回0,失败返回-1
int msgsnd(int msqid, const void *ptr, size_t size, int flag);

// 读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid, void *ptr, size_t size, long type, int flag);

// 控制消息队列: 成功返回0,失败返回-1
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid : 队列id
flag :文件权限(0777:可读可写可执行)
msgrcv、msgsnd 中 flag :默认为0,没有对应类型的数据就会阻塞在此
cmd :一般防止消息队列一直在内核堆积,使用 IPC_RMID 去移除队列

3.ftok函数:

  • 系统建立IPC通讯 (消息队列、信号量和共享内存) 时必须指定一个ID值。通常情况下,该ID值通过 ftok 函数得到

(1)头文件

#include <sys/types.h>

#include <sys/ipc.h>

(2)函数原型

key_t ftok( const char * fname, int id )
//	fname就是你指定的文件名(已经存在的文件名),一般使用当前目录

//如:
key_t key;
key = ftok(".", 1);	// 这样就是将fname设为当前目录

服务端程序一直在等待特定类型的消息,当收到该类型的消息以后,发送另一种特定类型的消息作为反馈,客户端读取该反馈并打印出来。

4.代码演示:

msgGet.c

  1 #include<stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/ipc.h>
  4 #include <sys/msg.h>
  5 #include<string.h>
  6 
  7 struct massage{			//消息队列的结构
  8 
  9         long mtype;
 10         char mtext[127];
 11 
 12 };
 13 
 14 int main()
 15 {
 16         struct massage readbuf;
 17 
 18         key_t key;
 19         key = ftok(".",'z'); //创建队列ID
 20 
 21         printf("key = %x\n",key);
 22 
 23         int msgID = msgget(key,IPC_CREAT|0777);  //创建或者打开消息队列
 24         if(msgID == -1)
 25         {
 26                 printf("The msg failed\n");
 27         }
 28 
 29 //读取另一端消息
 30         msgrcv(msgID,&readbuf,sizeof(readbuf.mtext),666,0); //读取消息
 31         printf("read from Sent %s\n",readbuf.mtext);
 32 
 33 //添写消息给另一端输出
 34         struct massage sentbuf = {998,"The massage from Sent"};
 35 
 36         msgsnd(msgID,&sentbuf,strlen(sentbuf.mtext),0);  //添加消息
 37       
 38 
 39         msgctl(msgID,IPC_RMID,NULL); //移除已经存在内核中的消息队列,防止堆积
 40         return 0;
 41 }

msgSent.c

  1 #include<stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/ipc.h>
  4 #include <sys/msg.h>
  5 #include<string.h>
  6 
  7 struct massage{		//消息队列的结构
  8 
  9         long mtype;
 10         char mtext[127];
 11 
 12 };
 13 
 14 int main()
 15 {
 16 
 17         struct massage sentbuf = {666,"this is massage from get"};
 18         
 19         key_t key;
 20         key = ftok(".",'z'); //获取队列ID号
 21         
 22         printf("key = %x\n",key);
 23         
 24         int msgID = msgget(key,IPC_CREAT|0777); // 创建 / 打开消息队列
 25         if(msgID == -1) 
 26         {
 27                 printf("The msg failed\n");
 28         }       
 29 // 添加消息到 另一端 666 内容
 30         msgsnd(msgID,&sentbuf,strlen(sentbuf.mtext),0); //添加消息
 31         printf("Sent success\n");
 32         
 33 // 读取另一端添加进 998的内容
 34         struct massage readbuf;
 35         msgrcv(msgID,&readbuf,sizeof(readbuf.mtext),998,0); //读取消息
 36         printf("return from get %s\n",readbuf.mtext);
 37         
 38         msgctl(msgID,IPC_RMID,NULL);  //移除已经存在内核中的消息队列
 39 
 40         return 0;
 41 }

运行展示:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
相同的key,说明在同一个消息队列中

四、共享内存

可以进行双向传输。顾名思义,就是申请一块特殊的内存空间,挂载在两个进程上,使两个进程都能对这块空间进行操作。(这块空间是独立与两个进程的物理内存上的)。

共享内存方式是指: 两个或者多个进程,在同一块内存区域共享信息操作

思路:

1、创建共享内存
2、将共享内存映射到创建的进程
3、读写数据到共享内存
4、释放共享内存的空间
5、干掉共享内存

1.原型:

shmget 函数
flag :

  • 有IPC_CREAT 、IPC_EXCL 、SHM_HUGETLB、SHM_NORESERVE这几个宏定义,最常用的是IPC_CREAT即创建共享内存。其他的宏定义有其他的应用场景作为初学者学会IPC_CREAT就可以了
  • size:申请空间的大小,注意必须以M为单位
#include <sys/shm.h>
#include <sys/ipc.h>

// 创建或获取一个共享内存:成功返回共享内存ID,失败返回-1


int shmget(key_t key, size_t size, int flag);

// 连接共享内存到当前进程的地址空间:成功返回指向共享内存的指针,失败返回-1
void *shmat(int shm_id, const void *addr, int flag);

// 断开与共享内存的连接:成功返回0,失败返回-1
int shmdt(void *addr);

// 控制共享内存的相关信息: 成功返回0,失败返回-1
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);

shmat函数参数:

shmid	即为申请的共享内存id。
shmaddr 	如果设置为0将会由内核自动分配地址(推荐使用)
flg		如果配置为SHM_RDONLY将会设置为只读,否则会设置为读写

shmdt函数:

参数为共享内存的地址。作用是解除挂解进程,释放共享内存空间。

shmctl函数:
参数解释:

shmid	为共享内存的id号
cmd		为命令,在解除挂载的时候使用IPC_RMID就好了。
buf		用于存放各种信息,不关心这些信息的话直接写0或者NULL就好

2.代码例子:

shmw.c

  1 #include <sys/ipc.h>
  2 #include <sys/shm.h>
  3 #include<stdio.h>
  4 #include <sys/types.h>
  5 #include <unistd.h>
  6 #include<string.h>
  7 #include<stdlib.h>
  8 //       int shmget(key_t key, size_t size, int shmflg);
  9 //      int shmdt(const void *shmaddr);
 10 //      int shmctl(int shmid, int cmd, struct shmid_ds *buf);
 11 //       void *shmat(int shmid, const void *shmaddr, int shmflg);
 12 int main()
 13 {
 14 
 15         char *shmdr;
 16         int shmid;
 17         key_t key;
 18         key = ftok(".",1);
 19         
 20         shmid = shmget(key,1024*4,IPC_CREAT|0666); //创建一个共享内存ID号,申请共享内存,如果不存在就创建并权限为可读可写
 21         if(shmid == -1)
 22         {
 23                 printf("create error\n");
 24                 exit(-1);
 25         }       
 26         
 27         
 28         
 29         shmdr = shmat(shmid,0,0); // 映射共享内存数据到 各自内存上
 30         printf("shmat success\n");
 31         
 32         strcpy(shmdr,"longchuangchenyixun"); //通过共享内存空间,数据交换
 33         sleep(5); 			// 等待 5s 另一个进程读取
 34         shmdt(shmdr);		//	释放共享内存空间
 35         shmctl(shmid,IPC_RMID ,0);	// 干掉共享内存
 36         printf("stop\n");
 37         return 0;
 38 }

shmr.c

  1 #include <sys/ipc.h>
  2 #include <sys/shm.h>
  3 #include<stdio.h>
  4 #include <sys/types.h>
  5 #include <unistd.h>
  6 #include<string.h>
  7 #include<stdlib.h>
  8 //       int shmget(key_t key, size_t size, int shmflg);
  9 //      int shmdt(const void *shmaddr);
 10 //      int shmctl(int shmid, int cmd, struct shmid_ds *buf);
 11 //       void *shmat(int shmid, const void *shmaddr, int shmflg);
 12 
 13 
 14 
 15 int main()
 16 {
 17 
 18         char *shmdr;
 19         int shmid;
 20         key_t key;
 21         key = ftok(".",1); 		
 22 
 23         shmid = shmget(key,1024*4,0);	//创建一个共享内存ID号
 24         if(shmid == -1)
 25         {
 26                 printf("create error\n");
 27                 exit(-1);
 28         }
 29 
 30         shmdr = shmat(shmid,0,0);	// 映射共享内存数据到 各自内存上
 31         printf("shmat ok\n");
 32         printf("data : %s\n",shmdr);	//读取共享内存上的数据并输出
 33 
 34         shmdt(shmdr);	 //释放共享内存空间
 35 
 36         printf("stop\n");
 37         return 0;
 38 }

在这里插入图片描述
在这里插入图片描述

五、信号量

信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

1.特点:

  1. 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
  2. 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
  3. 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
  4. 支持信号量组。

P :可以简单描述为 拿钥匙开锁
V: 可以简单描述为 把钥匙放回

二值信号量:

最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。
而可以取多个正整数的信号量被称为通用信号量。

2.函数原型:

Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

头文件:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
// 创建或获取一个信号量组: 若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);

// 对信号量组进行操作,改变信号量的值: 成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);

// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
semget :当 semget 创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1;如果是引用一个现有的集合,则将num_sems指定为0.

演示:

(1)semget
原型:

// 创建或获取一个信号量组: 若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
key_t key;
int semid;
key = ftok(".",2);
semid = semget(key,1,IPC_CREAT|0666); // 创建 / 获取 信号量
// num_sems = 1 指当前信号量集合中有一个信号量

(2)semctl
原型:

// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
semid : 信号量ID
sem_num : 指当前操作第几个信号量 (信号量也和数组一样,从0开始)
cmd : 设置信号量的指令,一般常用 SETVAL 来设置信号量的值(使用 SETVAL 的话 SETVAL  需要有  4  个参数)
union semun {
		
		int  val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO
                        (Linux-specific) */
};
union semun initsem;
initsem.val = 1; 	//	当前设置一把钥匙
semctl(semid,0,SETVAL,initsem) //	初始化信号量
// 0 指 操作当前第0个信号量
//SETVAL 使用这个IPC指令需要 四 个参数,作用为 设置信号量的值 为 initsem

在semctl函数中的命令有多种,这里就说两个常用的:

  • SETVAL:
    用于初始化信号量为一个已知的值。所需要的值作为联合体semun的val成员来传递。在信号量第一次使用之前需要设置信号量。
  • IPC_RMID:
    删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,他可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

(3)semop

原型:

// 对信号量组进行操作,改变信号量的值: 成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);

semop函数 第二个参数使用结构体,这个sembuf结构体系统已经建立,直接可以调用,可通过man 2 semop手册查询

系统以两个信号量为样例:

struct sembuf
{
    short sem_num; // 信号量组中对应的序号, 0~sem_nums-1
    short sem_op; // 信号量值在一次操作中的改变量
    short sem_flg; //IPC_NOWAIT, SEM_UNDO
}
struct sembuf sops[2];
           int semid;

           /* Code to set semid omitted */

           sops[0].sem_num = 0;        /* Operate on semaphore 0 */
           sops[0].sem_op = 0;         /* Wait for value to equal 0 */
           sops[0].sem_flg = 0;

           sops[1].sem_num = 0;        /* Operate on semaphore 0 */
           sops[1].sem_op = 1;         /* Increment value by one */
           sops[1].sem_flg = 0;

           if (semop(semid, sops, 2) == -1) {
               perror("semop");
               exit(EXIT_FAILURE);
           }

这里我以xhl2.c代码演示:

  1 #include<stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/ipc.h>
  4 #include <sys/sem.h>
  5 
  6 //      int semget(key_t key, int nsems, int semflg);
  7 //      int semop(int semid, struct sembuf *sops, unsigned nsops);
  8 //       int semctl(int semid, int semnum, int cmd, ...);
  9 //      int semop(int semid, struct sembuf *sops, unsigned nsops);
 10 
 11 
 12 union semun {
 13                int              val;    /* Value for SETVAL */
 14                struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
 15                unsigned short  *array;  /* Array for GETALL, SETALL */
 16                struct seminfo  *__buf;  /* Buffer for IPC_INFO
 17                                            (Linux-specific) */
 18 };
 19 
 20 void pGetKey(int id)  //拿取钥匙
 21 {
 22         struct sembuf new;
 23 
 24         new.sem_num = 0; //要操作的信号量编号ID,当前是0
 25         new.sem_op = -1;  //initsem.val设置了一把钥匙,所以要拿走一把钥匙,需要-1
 26         new.sem_flg = SEM_UNDO; //如果钥匙被拿走了为0情况下,会进行等待钥匙的归还
 27 
 28         semop(id ,&new, 1); 	//第二个参数是指针,所以需要取地址
 29         printf("Get key\n");
 30 }
 31 
 32 void vPutBackKey(int id)  //放回钥匙
 33 {
 34         struct sembuf new;
 35 
 36         new.sem_num = 0;	//要操作的信号量编号ID,当前是0
 37         new.sem_op = 1;		//由于钥匙被拿走了,所以还钥匙需要+1
 38         new.sem_flg = SEM_UNDO;	//如果钥匙被拿走了为0情况下,会进行等待钥匙的归还
 39
 40         semop(id ,&new, 1); //第二个参数是指针,所以需要取地址
 41         printf("Put key\n");
 42 }
 43 
 44 int main()
 45 {
 46 
 47         key_t key;
 48         int semid;
 49         key = ftok(".",2);
 50 
 51         semid = semget(key,1,IPC_CREAT|0666);
 52         union semun initsem;
 53         initsem.val = 0;  //设置开始的时候没有钥匙
 54 
 55         semctl(semid,0, SETVAL ,initsem); //初始化信号量
 56 
 57         int pid;
 58         pid = fork();
 59 
 60         if(pid > 0)
 61         {
 62                 pGetKey(semid);
 63                 printf("This is father\n");
 64                 vPutBackKey(semid);
 65 
 66         }else if(pid == 0){
 67 
 68 //              pGetKey(semid); 
 69                 printf("This is child\n");
 70                 vPutBackKey(semid);
 71 
 72         }else{
 73 
 74                 printf("fork error\n");
 75         }
 76 
 77         return 0;
 78 }

思想:

刚开始创建进程,因为一开始钥匙设置为0,所以父进程的pGetKey 函数会因没有钥匙而进行阻塞,这时子进程会执行vPutBackKey函数把钥匙放回去,并打印子进程printf,这样放回了钥匙父进程就不会阻塞,继续往下走打印父进程的printf

结果:

在这里插入图片描述

不足的内容后续会补充,用于记录学习,欢迎交流

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68484.html

(0)
newbe的头像newbebm

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!