Linux下进程间通信方式及实例
Linux下进程间通信方式及实例
IPC概念
IPC:InterProcess Communication 进程间通信,通过内核提供的缓冲区进行数据交换的机制。
IPC通信方式:
- pipe 管道 – 简单
- FIFO 有名管道
- mmap 文件映射共享IO – 速度最快
- 本地socket 最稳定
- 信号 携带信息量最小
- 共享内存
- 消息队列
管道
管道:半双工通信
管道函数
int pipe(int pipefd[2])
pipefd
读写文件描述符,0-代表读,1-代表写- 返回值:失败返回-1,成功返回0
#include <stdio.h>
#include <unistd.h>
int main(){
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid == 0){
write(fd[1], "hello", 5);
}else if (pid > 0){
char buf[12];
int ret = read(fd[0], buf, sizeof(buf));
if (ret > 0){
write(STDOUT_FILENO, buf, ret);
}
}
}
小例子
父子进程实现pipe通信,实现ps aux|grep bash
功能
#include <stdio.h>
#include <unistd.h>
int main(){
int fd[2];
pipe(fd);
pid_t pid = ford();
if (pid == 0){
//son -> ps
//关闭读端
close(fd[0]);
//重定向
dup2(fd[1], STDOUT_FILENO);//标准输出重定向到管道写端
//execlp
execlp("ps", "ps", "aux", NULL);
}else if (pid > 0){
//关闭写端
close(fd[1]);
//重定向:标准输入->管道读端
dup2(fd[0], STDIN_FILENO);
execlp("grep", "grep", "bash", NULL);
}
return 0;
}
读管道
- 写端全部关闭 – read读到0,相当于读到文件末尾
- 写端没有全部关闭
- 有数据 – read读到数据
- 没有数据 – read阻塞 fcntl函数可以更改非阻塞
写管道
- 读端全部关闭 – 产生一个信号 SIGPIPE ,程序异常终止
- 读端没有全部关闭
- 管道已满 – write阻塞
- 管道未满 – write正常写入
管道的优劣
优点:简单,相比信号,套接字实现进程间通信,简单很多
缺点:1、只能单向通信,双向通信需要建立两个管道。2、只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用FIFO有名管道解决。
FIFO
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。
FIFO是unix基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。备进程可以打开这个文件进行 read/write,实际上是在读写内核通道,这样就实现了进程间通信。
创建管道
- 使用命令:
mkfifo myfifo
- 使用函数:
int mkfifo(const char *pathname, mode_t mode);
成功:0;失败:-1
内核会针对fifo文件开辟一个缓冲区,操作FIFO文件,可以操作缓冲区,实现进程通信。
一旦使用mkfifo
创建了一个FIFO,就可以使用open打开它,常见的文件IO函数都可以用于FIFO。如:close
、read
、write
、unlink
等
例
写端进程:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char * argv[]){
if (argc != 2){
printf("./a.out fifoname\n");
return -1;
}
//若当前目录有一个myfifo文件
//打开FIFO文件
int fd = open(argv[1], O_WRONLY);
//写
char buf[256];
int num = 1;
while(1){
memset(buf, 0x00, sizeof(buf));
sprintf(buf, "xiaoming%04d", num++);
write(fd, buf, strlen(buf));
sleep(1);
}
//关闭
close(fd);
return 0;
}
读端进程:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(itn argc, char* argv[]){
if (argc != 2){
printf("./a.out fifoname\n");
return -1;
}
int fd = open(argv[1], O_RDONLY);
char buf[256];
int ret;
while(1){
memset(buf, 0x00, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
if (ret > 0)
printf("read:%s\n", buf);
}
close(fd);
return 0;
}
注意事项:打开FIFO文件的时候,read端会阻塞等待write端打开open,write端同理,也会阻塞等待另外一端打开。
mmap映射
mmap函数
创建映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
- addr : 映射地址,可以传NULL
- length:映射区的长度
- prot:
- PROT_EXEC pages may be executed.
- PROT_READ pages may be read.
- PROT_WRITE pages may be written.
- PROT_NONE pages may not be accessed.
- flags:
- MAP_SHARED 映射区是共享的,对内存的修改会影响到源文件
- MAP_PRIVATE 映射区是私有的
- fd:文件描述符,open打开一个文件
- offset:偏移量
- 返回值
- 成功:返回可用内存首地址
- 失败:返回MAP_FAILED
释放映射区
int munmap(void *addr, size_t length);
- addr:传mmap的返回值
- length:mmap创建长度
- 返回值:
- 成功:0
- 失败:-1
例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
int main(){
int fd = open("mem.txt", O_RDWR);
//创建映射区
char *mem = mmap(NULL, 8, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
if (mem == MAP_FAILED){
perror("mmap err");
return -1;
}
//拷贝数据
strcpy(mem, "hello");
//释放mmap
munmap(mem,8);
close(fd);
return 0;
}
mmap九问
-
如果更改mem变量的地址,释放的时候 munmap,传入mem还能成功吗?
不能
-
如果对mem越界操作会怎么样?
文件的大小对映射区操作有影响,尽量避免
-
如果文件偏移量随便填个数会怎么样?
offset必须是4k的整数倍
-
如果文件描述符先关闭,对mmap映射有没有影响?
没有影响
-
open的时候,可以新创建一个文件来创建映射区吗?
不可以用大小为0的文件,文件大小为0会引起bus error
-
open文件选择 O_WRONLY,可以吗?
不可以:Permission Denied,因为把文件映射到缓冲区会进行一次读操作,若没有读操作的权限会报错。
-
当选择 MAP_SHARED的时候,open文件选择 O_RDONLY,pot可以选择PROT_READ|PROT_ WRITE吗?
不可以,同样会报Permission Denied,SHARED的时候,映射区的权限 <= open文件的权限
-
mmap什么情况下会报错?
很多情况
-
如果不判断返回值会怎么样?
进程通信例
父子进程通信
子进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/wait.h>
int main(){
int fd = open("mem.txt", O_RDWR);
//创建映射区
int *mem = mmap(NULL, 4, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
if (mem == MAP_FAILED){
perror("mmap err");
return -1;
}
//fork子进程
pid_t pid = fork();
//父进程和子进程交替修改数据
if (pid == 0){
//son
*mem = 100;
printf("child, *mem = %d\n", *mem);
sleep(3);
printf("child, *mem = %d\n", *mem);
}else if (pid > 0){
//parent
sleep(1);
printf("parent, *mem = %d\n", *mem);
*mem = 1001;
printf("parent, *mem = %d\n", *mem);
wait(NULL);
}
munmap(mem,4);
close(fd);
return 0;
}
无血缘关系进程通信
进程1:修改映射区
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/wait.h>
typedef struct _Student{
int sid;
char sname[20];
}Student;
int main(int argc, char *argv[]){
if (argc != 2){
printf("./a.out fifoname\n");
return -1;
}
int fd = open(argv[1], O_RDWR|O_CREAT|O_TRUNC, 0666);
int length = sizeof(Student);
ftruncate(fd, length);
//创建映射区
Student *stu = mmap(NULL, length, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
if (mem == MAP_FAILED){
perror("mmap err");
return -1;
}
int num = 1;
while (1){
stu->sid = num;
sprintf(stu->sname, "xiaaoming-%03d", num++);
sleep(1); //每隔一秒修改映射区内容
}
munmap(mem,length);
close(fd);
return 0;
}
进程2:读取数据
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/wait.h>
typedef struct _Student{
int sid;
char sname[20];
}Student;
int main(int argc, char *argv[]){
if (argc != 2){
printf("./a.out fifoname\n");
return -1;
}
int fd = open(argv[1], O_RDWR);
int length = sizeof(Student);
//创建映射区
Student *stu = mmap(NULL, length, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
if (mem == MAP_FAILED){
perror("mmap err");
return -1;
}
while (1){
printf("sid = %d, sname = %s\n", stu->sid, stu->sname);
sleep(1);
}
munmap(mem,length);
close(fd);
return 0;
}
匿名映射
通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再 unlink、cose掉,比较麻烦。可以直接使用匿名映射来代替。
其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数fags来指定。
使用 MAP_ANONYMOUS(或 MAP_ANON),如:
int *p=mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
"4"随意举例,该位置表大小,可依实际需要填写
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/wait.h>
int main(){
//创建映射区
int *mem = mmap(NULL, 4, PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON, -1, 0);
if (mem == MAP_FAILED){
perror("mmap err");
return -1;
}
//fork子进程
pid_t pid = fork();
//父进程和子进程交替修改数据
if (pid == 0){
//son
*mem = 100;
printf("child, *mem = %d\n", *mem);
sleep(3);
printf("child, *mem = %d\n", *mem);
}else if (pid > 0){
//parent
sleep(1);
printf("parent, *mem = %d\n", *mem);
*mem = 1001;
printf("parent, *mem = %d\n", *mem);
wait(NULL);
}
munmap(mem,4);
close(fd);
return 0;
}
需注意的是, MAP ANONYMOUS和 MAP ANON这两个宏是Lnux操作系统特有的宏
这两个宏在有些Unix系统没有,可以用/dev/zero
做映射
信号
信号的概念
信号在我们的生活中随处可见,如:古代战争中摔杯为号;现代战争中的信号弹;体育比赛中使用的信号枪…他们都有共性:1.简单 2.不能携带大量信息 3.满足某个特设条件才发送。
信号是信息的载体, Linux/UNⅨ环境下,古老、经典的通信方式,现下依然是主要的通信手段。
Unix早期版丰就提供了信号机制,但不可靠,信号可能失 Berkeley和A&T都对信号模型做了更改,增加了可靠信号机制。但彼此不兼容。pOS1对可靠信号例程进行了标准化。
信号的机制
A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断似——异步模式
但信号是软件层面上实现的中断,早期常被称为“软中断”。
信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个廷迟时间非常短,不易察觉
每个进程收到的所有信号,都是由内核负责发送的,内核处理
共享内存
创建共享内存
用到的函数shmget, shmat, shmdt
函数名 | 功能描述 |
---|---|
shmget | 创建共享内存,返回pic key(共享内存id) |
shmat | 第一次创建完共享内存时,它还不能被任何进程访问,shmat()函数的作用就是用来启动对指定id的共享内存的访问,并把共享内存连接到当前进程的地址空间 |
shmdt | 该函数用于将共享内存从当前进程中分离。注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用。 |
函数原型如下:
int shmget(key_t key, size_t size, int flag);
key: 标识符的规则
size:共享存储段的字节数
flag:读写的权限
返回值:成功返回共享存储的id,失败返回-1
//
void *shmat(int shmid, const void *addr, int flag);
shmid:共享存储的id
addr:一般为0,表示连接到由内核选择的第一个可用地址上,否则,如果flag没有指定SHM_RND,则连接到addr所指定的地址上,如果flag为SHM_RND,则地址取整
flag:如前所述,一般为0
返回值:如果成功,返回共享存储段地址,出错返回-1
//
int shmdt(void *addr);
addr:共享存储段的地址,以前调用shmat时的返回值
//
int shmctl(int shmid,int cmd,struct shmid_ds *buf)
shmid:共享存储段的id
cmd:一些命令
IPC_STAT 得到共享内存的状态
IPC_SET 改变共享内存的状态
IPC_RMID 删除共享内存
IPC_RMID 命令实际上不从内核删除一个段,而是仅仅把这个段标记为删除,实际的删除发生在最后一个进程离开这个共享段时。
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
int main(){
int shm;
char* ptr;
shm = shmget(IPC_PRIVATE, 129, IPC_CREAT | 0600);
if(shm < 0){
perror("shmget");
return 1;
}
ptr = (char*)shmat(shm, NULL , 0);
if(atoi(ptr) == -1){
perror("shmat");
return -1;
}
strcpy(ptr, "HELLO");
shmdt(ptr);
return 0;
}
访问共享内存
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
int main(int argc, char* argv[]){
int shm;
char* ptr;
if(argc != 2){
return 1;
}
shm = atoi(argv[1]);
ptr = (char*)shmat(shm, NULL, 0);
if(atoi(ptr) == -1){
perror("shmat");
return 1;
}
printf("string from shared memory : %s\n", ptr);
shmdt(ptr);
return 0;
}
删除共享内存
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
int main(int argc, char* argv[]){
int shm;
shmid_ds sds;
if(argc != 2){
printf("argc is wrong");
return 1;
}
shm = atoi(argv[1]);
if(shmctl(shm, IPC_RMID, &sds) != 0){
perror("shmctl");
return 1;
}
return 0;
}