目录
- 管道
- pipe匿名管道
- 接口介绍
- 示例代码
- fifo命名管道
- 接口介绍
- 代码示例
- 匿名管道与命名管道的区别
- shm共享内存
- 接口介绍
- 相关指令
- 代码示例
- 特点总结
- 信号
- 信号量
- socket套接字
管道
管道是一种较老的,半双工通信方式,即数据只能向一个方向流动(即一个进程进行写操作,一个进程进行读操作);
如果要进行双向通信,则需要建立起两个管道。
管道分为两种,匿名管道和命名管道;
pipe匿名管道
匿名管道就是具有血缘关系的进程进行通信,常见于父子进程之间。
父子进程创建匿名管道(半双工)的过程如下:
可以看到,匿名管道通信的原理就是某个父进程在他的fd文件描述符数组中维护了匿名管道文件,子进程继承之后双方用于通信;
匿名管道文件的实质和标准IO类似,是在内核中的一片特定缓冲区; 因此数据交互的时候,涉及到用户态和内核态之间的数据拷贝,效率不高的;
(后面要讲的共享内存shm是直接映射到共享内存区,不需要进行拷贝,高效)
接口介绍
#include <unistd.h>
功能:创建一匿名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
示例代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
int pipe_fds[2];
int ret = pipe(pipe_fds); // 父亲创建管道,0为读端,1为写端
if (ret == -1)
{
perror("pipe");
return 1;
}
int pid = fork(); // 创建子进程
if (pid < 0)
{
// error
perror("fork");
return 2;
}
else if (pid == 0)
{
// child
close(pipe_fds[1]); // 子进程关闭写端
char buf[128];
// 子进程从管道中读取数据
read(pipe_fds[0], buf, sizeof(buf) - 1);
printf("%s\n", buf);
close(pipe_fds[0]);
}
else
{
// father
close(pipe_fds[0]); // 父进程关闭读端
// 父进程往管道内写数据
const char *msg = "I am father.\n";
write(pipe_fds[1], msg, strlen(msg));
close(pipe_fds[1]);
}
return 0;
}
运行结果
fifo命名管道
FIFO,也叫做命名管道,它是一种文件类型。
- FIFO可以在无关的进程之间交换数据,与匿名管道不同;
- FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。(这个文件仅用于双方通信)
接口介绍
#include <sys/types.h>
#include <sys/stat.h>
//创建一个命名管道
int mkfifo(const char *pathname, mode_t mode);
参数:
第一个参数为这个特殊文件的路径;
第二个参数mode 与 open 函数中的 mode 相同,设置标志位。
当 open 一个 FIFO 时,这个FIFO是否设置非阻塞标志(O_NONBLOCK)的区别:
1.若没设置 O_NONBLOCK(默认),只读的一方 open打开 这个FIFO 要 阻塞到 某个其他进程为写 而打开它
类似的,只写的一方 open打开 这个FIFO 要 阻塞到 某个其他进程为读 而打开它
2.若设置了 O_NONBLOCK,则只读 open打开时会立即返回。(执行下面的代码,如果有人向这个fd写了,那么这边就能拿到,不用阻塞等待处理)
只写 open 打开时,如果没有进程已经为读而打开该将出错返回 -1 其 errno 置 ENXIO,否则打开成功,直接就可以写入数据了;
下面是借助fifo通信的模型:
结论:
1、数据 还是存储在内核的缓冲区当中的(fifo创建的管道文件只是一个特殊文件,不存东西的文件,底层机制和匿名管道一样的 都是拿内核缓冲区做中介)
2、管道文件的作用是为了让不同的进程可以找到这块缓冲区 (这点匿名管道做不到)
代码示例
write.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
// int mkfifo(const char *pathname, mode_t mode);
int main()
{
if(mkfifo("myfifo",0600) == -1 && errno != EEXIST)//创建命名管道
{
printf("mkfifo failed\n");
perror("why");
}
int nread;
char buf[30] = "message from myfifo";
int fd = open("./myfifo",O_WRONLY);//以 只写 的方式打开管道,程序阻塞在这,直到其他进程为读而打开它
if(fd < 0)
{
printf("write open failed\n");
}
else
{
printf("write open success\n");
}
while(1)//不断的通过管道(open的fd)给read端发送数据发送
{
sleep(1);
write(fd,buf,strlen(buf));
}
close(fd);
return 0;
}
先运行write端,则命名管道就会创建好,然后等待着read端的到来,进行管道通信;
read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int nread;
char buf[30] = {'\0'};
int fd = open("./myfifo",O_RDONLY);//以 只读的形式打开管道,程序阻塞在这,直到有另一个进程对其执行写操作
if(fd < 0)
{
printf("read open failed\n");
}else
{
printf("read open successn\n");
}
while(1)//反复收取数据并打印出来
{
nread = read(fd,buf,sizeof(buf));
printf("read %d byte,context is:%s\n",nread,buf);
}
close(fd);
return 0;
}
运行结果:
可以看到,当write端创建命名管道之后不断通过管道给read端发送数据,read端通过命名管道收到数据并打印出来;
匿名管道与命名管道的区别
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义。
shm共享内存
共享内存根据其名字就可以推测与内存中的共享区有关。实际上,共享内存的使用要比管道的简单
接口介绍
1.key_t ftok(const char *pathname, int proj_id);
功能:用来生成System V IPC密钥,key是用来唯一标识共享内存块的值 file to key
参数
pathname:共享内存文件的给定路径名
proj_id:project id
这两个参数可以随意设置,只不过要保证使用共享内存的进程这两个参数设置需一样。
返回值:成功返回生成的key值,失败返回-1
2. int shmget(key_t key, size_t size, int shmflg); //get创建
功能:用来创建共享内存句柄
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
3. void *shmat(int shmid, const void *shmaddr, int shmflg);//attach 链接
功能:将共享内存段连接到进程地址空间
参数
shmid: 共享内存标识码
shmaddr:指定连接的地址
shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值:成功返回一个指针,指向共享内存第一个字节;失败返回-1
4. int shmdt(const void *shmaddr);//detach 脱离
功能:将共享内存段与当前进程脱离
参数
shmaddr: 由shmat所返回的指针
返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离 不等于 删除共享内存段
5. int shmctl(int shmid, int cmd, struct shmid_ds *buf);//control 控制
功能:用于控制实际的共享内存
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值,如下表)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
shmctl中cmd的几种命令:
对于共享内存的key和shmid,我们可以类比文件中的inode与fd的关系。
虽然文件系统一inode唯一标识文件,但在实际使用中仍是以fd文件描述符去操作文件。\
相关指令
ipcs -m:查看当前共享内存的信息 //ipc == (Inter-Process Communication,进程间通信)
ipcrm -m shmid:删除对应shmid的共享内存块
代码示例
server进程创建共享内存,获取key值及shmid;
client进程通过shmid去挂接共享内存,然后观察两个进程通过共享内存进行通信的现象:
server.c
#define _SVID_SOURCE 1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
//创建key时的两个参数,s c需要统一
#define PATH_NAME "/home/lyl/2022-3-20"
#define PROJ_ID 0x6666
#define SIZE 4097
int main()
{
//获取key值
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key == -1)
{
perror("ftok");
return 1;
}
printf("key: %x\n", key);
//获取shmid
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0644);//若不存在则创建共享内存,若存在则报错
if(shmid == -1)
{
perror("shmget");
return 2;
}
printf("shmid:%d\n",shmid);
//让进程挂接共享内存,形成关联
char* addr = (char*)shmat(shmid, NULL, 0);
printf("server attached on shared memory\n");
if(addr == (char*)-1)
{
perror("shmat");
return 3;
}
printf("addr:%p\n", addr);
//从共享内存首地址读数据 并打印
while(1)
{
printf("%s\n", addr);
sleep(1);
}
//解除关联
shmdt(addr);
printf("server attached off shared memory\n");
shmctl(shmid, IPC_RMID, NULL);
printf("server deleted shared memory\n");
return 0;
}
client.c
#define _SVID_SOURCE 1
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
//创建key时的两个参数,s c需要统一
#define PATH_NAME "/home/lyl/2022-3-20"
#define PROJ_ID 0x6666
#define SIZE 4097
int main()
{
//获取key值
key_t key = ftok(PATH_NAME, PROJ_ID);
if(key == -1)
{
perror("ftok");
return 1;
}
//获取shmid
int shmid = shmget(key, SIZE, IPC_CREAT);//不需要自己创建共享内存,server创建好了,直接获取shmid即可
if(shmid == -1)
{
perror("shmget");
return 2;
}
//让进程挂接共享内存,形成关联
char* addr = (char*)shmat(shmid, NULL, 0);
printf("client attached on shared memory\n");
//TODO
const char* msg = "I am process client\n";//等会用于通信的数据
//逐字符向共享内存写数据
for(size_t i = 0; i < strlen(msg); i++)
{
addr[i] = *(msg + i);
sleep(1);
}
//接触关联
shmdt(addr);
printf("client attached off shared memory\n");
return 0;
}
client进程向共享内存中不断写入数据,然后server进程从共享内存中读取数据并打印。
通过分析,共享内存区别于管道借助阻塞式read和write进行通信而是直接对同一块内存进行操作!
因此共享内存通信的两个进程独立,不像管道会存在阻塞现象。
而且使用之后的共享内存中数据不会自动清除,下次使用还能拿到上次通信的数据,因此每次进程结束后都需要主动释放共享内存,否则再次执行进程时会报错;
特点总结
- 共享内存不存在同步与互斥机制,使用的进程相互独立。因此对共享内存的操作是非进程安全的。
- 共享内存只有在当前映射链接数为0时,才能被被真正删除。
- 共享内存由于不需要 内核态与用户态的 拷贝数据,因此时进程通信中最快的形式
- 共享内存的生命周期随内核,只要不主动删除,其就会随内核一直存在,除非重启系统。
信号
信号一般用于一些异常情况下的进程间通信,是一种异步通信,它的数据结构一般就是一个数字。
在Linux操作系统中,为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过kill -l命令,查看所有的信号。
运行在shell终端的进程
,我们可以通过键盘输入某些组合键的时候,给进程发送信号。例如
Ctrl+C产生 SIGINT 信号,表示终止该进程;
Ctrl+Z产生 SIGTSTP 信号,表示停止该进程,但还未结束;
如果进程在后台运行
,可以通过kill命令的方式给进程发送信号,但前提需要知道运行中的进程PID号,例如:
kill -9 1050,表示给PID为1050的进程发送SIGKILL 信号,用来立即结束该进程(例如:win下在任务管理器右键结束进程);
所以,信号事件的来源主要有硬件来源(如键盘Cltr+C)和软件来源(如kill命令)。
信号是进程间通信机制中唯一的异步通信机制
进程需要为信号设置相应的监听处理,当监听到特定信号时,接着执行相应的操作,类似很多编程语言里的通知机制。
关于信号的更多理解和操作,可以移步这篇博客
信号量
关于进程间信号量的通信的原理,移步本人这篇文章;
将其与本文解耦的原因是,信号量作为同步的一种重要机制,并且是保证临界区资源正确被访问的重要手段(通过计数器方式),需要系统理解和学习;
同时,信号量自身也是邻接资源,它内部的PV原语保证了他的操作原子性;
socket套接字
关于进程间socket套接字的通信原理,移步本人这篇文章;
将其与本文解耦的原因是,socket是一个庞大的学习内容,除了能本地进程间通信之外,也能跨网络进程间通信,并且是学习TCP,UDP协议的重要知识点;需要系统理解和学习