管道
有名管道:
进程间传递数据
在磁盘上有一个管道文件标识,创建管道文件,但是,交互的数据并不会存储到磁盘中,管道文件不占磁盘 block 空间,只占据了磁盘上的一个 inode,数据缓存在内存上。
管道文件标识符通过操作系统操作,应用层,用户层对运行的要求是:要在同一个操作系统,有权限访问的任意 n 个进程间的通讯。
管道文件表示不需要存储在磁盘的原因:
- 进程间通讯需要效率,如果放在磁盘上,不论是读数据还是写数据都要通过 I/O,I/O 的效率太慢;如果放在内存上,进程本身在内存上,可以直接访问内存,效率就会很快
- 进程间通讯的数据不需要保存,磁盘是永久保存。
A 进程 open 打开管道文件,管道文件上只有 inode 节点,有管道文件映射到内存上的空间,B 进程 open 打开管道文件,管道文件映射到内存上的空间。A 进程将 hello 写到内存上,B 进程通过映射读取内存上的内容。
操作
创建管道文件:
命令:mkfifo filename
函数:int mkfifo (char *filename, int mode)
打开:open 会阻塞运行 至少有一个读进程,一个写进程
写:write 阻塞运行 管道空间满
读:read write 会阻塞 有数据可读或者所有写端关闭
关闭:close
写端
#include <stdio.h>
#include <assert.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
int main()
{
int fd=open("FIFO",O_WRONLY);
assert(fd!=-1);
printf("write fd=%d\n",fd);
while(1)
{
char buff[128]={0};
fgets(buff,128,stdin);
if(strncmp(buff,"end",3)==0)
{
break;
}
write(fd,buff,128);
}
close(fd);
exit(0);
}
读端
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd=open("FIFO",O_RDONLY);
assert(fd!=-1);
printf("read fd=%d\n",fd);
while(1)
{
char buff[128]={0};
int n=read(fd,buff,127);
if(0==n)
{
break;
}
printf("read(n=%d):%s",n,buff);
}
close(fd);
exit(0);
}
无名管道:
借助于父子进程共享文件描述符,通过无名管道完成通讯,不会创建管道文件,只能应用于父子进程之间共享 fork 之前打开的文件描述符。无名管道的数据内容在内 vfrv 存中缓存。
无论是有名管道还是无名管道,在同一时刻,只能一端读,一端写
规定:父子进程执行之前,必须分别关闭一对读写
操作
创建并打开无名管道:
int pipe (int fds[2]); fds[0]读文件描述符 fds[1]写文件描述符
A 进程创建并打开无名管道文件,在内存上开辟一个内存空间。A 进程中的两个文件描述符分别以读和写指向内存。A进程调用 fork 生成 B 进程。B 进程 是 A 进程的子进程,子进程继承父进程打开的文件描述符,A 进程通过 fd[1] 将 hello 写到内存上,B 进程通过 fd[0] 读取内存上的内容。
#include <stdio.h>
#include <assert.h>
#include <fcntl.h>
#include <unistd.h>
int main(int Argc,char *Argv[])
{
int fds[2];
pipe(fds);//创建并打开无名管道
pid_t n=fork();
assert(n!=-1);
if(n==0)
{
//关闭子进程中的写文件描述符
close(fds[1]);
char buff[128]={0};
read(fds[0],buff,127);
printf("child\n");
printf("buff=%s\n",buff);
}
else
{
close(fds[0]);
char buff[128]={0};
fgets(buff,128,stdin);
write(fds[1],buff,127);
printf("father\n");
close(fds[1]);
}
}
当读到一个写端已被关闭的管道时,在所有的数据都被读取后,read 返回 0,以示到达了文件结束处。
如果写一个读端已被关闭的管道,则产生信号 SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则 write 返回 -1,error 设置为 EPIPE。
如果管道有一个写端两个读端:两个读端谁抢到算谁的。
管道进行写一次,读一次管道是否还在
信号量
信号量不负责发送数据,只是进程同步控制。类似于一个计数器,当其值大于 0 时,记录临界资源的个数。等于 0 时,没有临界资源可用,申请 P 操作,则进程会被阻塞。其他需要临界资源的进程就必须阻塞,控制进程同步运行的一种机制
进程同步:进程协同工作,进程必须按照规定的顺序一步步执行
进程异步:进程独立运行,互不干扰,当一个进程异步调用发出时,调用者不能立刻得到结果,进程不会阻塞。需要内核机制来做通知
临界资源:同一时刻只能被一个进程访问的资源,临界资源必须有多份
临界区:访问临界资源的代码区域
原子操作:不能被打断的操作,一旦开始操作,必须等待他结束
P,V操作:P-1 申请资源 V+1 释放资源
当进程访问临界资源前,对信号量进行 P 操作,访问临界资源后,对信号量进行 V 操作,P 操作 V 操作必须是原子操作
信号量的内核对象–》是多个进程能访问到同一个信号量
内核维护的内核对象记录的是一个信号量集。
操作
1、创建或者获取信号量:
int semget ((key_t) key, int nsems, int flag);
nsems: 信号量集中信号量的个数
2、如果是新创建的信号量,必须做初始化设置:
int semctl (int semid, int semnum, int cmd, union semun arg)
semnum: 操作信号量集中的那一个信号量
cmd = SETVAL
arg.val = 信号量的初始值
3、如果是获取的,则可以直接使用:
id=semget();//仅仅是获取信号量
if(id==-1)
{
id=semget();//创建
//初始化
}
id
4、P 操作:
V 操作:
int semop (semid, struct sembuf[ ], size_t size)
struct sembuf
{
short sem_num;指定本变量操作的信号量集中信号量的下标
short sem_op; -1 P操作 1 V操作:
short sem_flg; SEM_UNDO
};
5、释放内核对象:
int semctl (int semid, int semnum, int cmd); //cmd = IPC_RMID
//立即删除
获取:int SemGet (int key, int val[ ], int nsems);
P 操作:int SemWait (int semid, int sems[ ], int len);
V 操作:int SemPost (int semid, int sems[ ], int len);
释放:int SemDel (int semid);
单方面 A 进程给 B 进程提供服务
进程对普通文件的 read 是非阻塞的,A 进程将数据写入到文件中,B 进程从文件中读取写入的数据,并将其打印
A 进程不需要阻塞,信号初始值为 0,B 进程 read 之前必须阻塞,等待 A 进程将数据写入文件
sem.h
#pragma once
typedef union semun
{
int val;
}SemUn;
int SemGet(int key, int initval[], int nsems);
//key 获取的信号量
//nsems 信号量的个数
//initval 信号量的初始值
int SemP(int semid, int sems[], int len);
//信号量
//信号量下标
int SemV(int semid, int sems[], int len);
int SemDel(int semid);
sem.c
#include "sem.h"
#include <stdio.h>
#include <malloc.h>
#include <assert.h>
#include <sys/sem.h>
int SemGet(int key, int initval[], int nsems)
{
//获取信号量
int semid = semget((key_t)key, 0, 0664);
//创建并初始化信号量
if(-1 == semid)
{
semid = semget((key_t)key, nsems, 0664 | IPC_CREAT);
if(-1 == semid)
{
perror("create sem fail ");
return -1;
}
int i = 0;
for(; i < nsems; ++i)
{
SemUn sem;
sem.val = initval[i];
semctl(semid, i, SETVAL, sem);
}
}
return semid;
}
int SemP(int semid, int sems[], int len)
{
struct sembuf *buf = (struct sembuf*)malloc(sizeof(struct sembuf) * len);
assert(buf != NULL);
int i = 0;
for(; i < len; ++i)
{
buf[i].sem_num = sems[i];
buf[i].sem_op = -1;
buf[i].sem_flg = SEM_UNDO;
}
if(-1 == semop(semid, buf, len))
{
perror("P fail ");
return -1;
}
free(buf);
return 0;
}
int SemV(int semid, int sems[], int len)
{
struct sembuf *buf = (struct sembuf*)malloc(sizeof(struct sembuf) * len);
assert(buf != NULL);
int i = 0;
for(; i < len; ++i)
{
buf[i].sem_num = sems[i];
buf[i].sem_op = 1;
buf[i].sem_flg = SEM_UNDO;
}
if(-1 == semop(semid, buf, len))
{
perror("V fail ");
return -1;
}
free(buf);
return 0;
}
int SemDel(int semid)
{
if(-1 == semctl(semid, 0, IPC_RMID))
{
perror("Delete sem fail ");
return -1;
}
return 0;
}
A进程
A 进程获取用户输入当输入完成后,B 进程开始运行,接收用户输入 P
#include "sem.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <string.h>
int main()
{
int sem = 0;
int semid = SemGet(1234, &sem, 1);
assert(semid != -1);
int fd = open("file.txt", O_WRONLY | O_CREAT, 0664);
assert(fd != -1);
while(1)
{
printf("please input: ");
char buff[128] = {0};
fgets(buff, 128, stdin);
if(strncmp(buff, "end", 3) == 0)
{
break;
}
write(fd, buff, strlen(buff) - 1);
}
close(fd);
int index = 0;
SemV(semid, &index, 1);
}
B进程
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <fcntl.h>
#include "sem.h"
int main()
{
int sem = 0;
int semid = SemGet(1234, &sem, 1);
assert(semid != -1);
int index = 0;
SemP(semid, &index, 1);
int fd = open("file.txt", O_RDONLY);
assert(fd != -1);
while(1)
{
char buff[128] = {0};
int n = read(fd, buff, 127);
if(n <= 0)
{
break;
}
printf("%s", buff);
}
printf("\n");
close(fd);
}
FCPrintStr.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include "sem.h"
int main()
{
int sem = 1;
int semid = SemGet(1234, &sem, 1);
assert(semid != -1);
pid_t pid = fork();
assert(-1 != pid);
if(0 == pid)
{
int index = 0;
int i = 0;
for(; i < 5; ++i)
{
SemP(semid, &index, 1);
printf("i am child\n");
sleep(1);
SemV(semid, &index, 1);
}
}
else
{
int index = 0;
int i = 0;
for(; i < 5; ++i)
{
SemP(semid, &index, 1);
printf("i am father\n");
sleep(1);
SemV(semid, &index, 1);
}
}
}
消息队列
消息:类型(标识符)+ v 数据 一条消息 发送的消息都是独立的
队列:一种先进先出的数据结构
消息队列:
- 在同一类型上符合先进先出的规定。
- 对于整个消息队列,是类优先级队列
内核对象:进程间通讯需要内核对象来共享东西。内核对象是一个结构体变量,里面存储的是一些属性,并且指向了一个内存空间,该内存空间在内核中,有内核维护,把内存画在用户空间是因为内核空间不够,就把内存映射到用户空间。
标识符:内核对 IPC 结构(内核对象)的标识 id
键:key,是用户层次对于内核中的对象的标识
两个进程需要访问同一个内核对象,必须给一个相同的 key 值来找到 IPC 结构。
通过键值来找到对应的内核对象的标识符(ID)
进程 A 和进程 B 通过消息队列通讯。两个进程通过内核空间的内核对象同时访问。如果 A 进程发送信息时,先访问内核对象,如果内核对象不存在,就创建内核对象,该内核对象指向内存空间,A 进程的信息直接存入内存空间中。B 进程直接获取内核对象,通过内核对象访问内存空间,读取 A 发送的数据。
数据的定向发送与接受
(1)接收消息的进程只接受指定类型的消息其他消息一概不管。发送端就可以指定数据发送给哪个进程。
(2)消息队列不是准确的先进先出,在特定类型上是先进先出。
操作
ipcs -q 查看消息队列
ipcrm -q id 删除消息队列
1、创建或者获取内核对象 msgget
int msgget ((key_t)key, int flag)
key:用户标识符
flag:权限 IPC_CREAT
返回值:失败返回 -1,成功返回内核对象的内核 ID
2、通过内核对象完成通讯 A 进程发送数据 msgsnd B进程接受数据 msgrcv
(1) A进程发送数据 msgsnd
int msgsnd (int msgid, void *ptr, size_t nbytes, int flag);
ptr 指向的结构:
struct msgdata
{
long mtype; 类型
char mtext[128]; 数据
}
btytes:数据部分的实际长度
flag:一个标记
(2) B进程接收数据 msgrcv
int msgrcv (int msgid, void *ptr, int nbytes, long type, int flag);
ptr 指向 msgdata 结构,用于保存接收到的消息
nbytes:指定接收数据的缓冲区的大小
type:本次接收数据的类型。
3、释放内核对象 msgctl
int msgctl (int msgid, int cmd, struct msgid_ds *buf)
cmd: IPC_STAT IPC_SET IPC_RMID
发送方
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <sys/msg.h>
typedef struct msgdata
{
long mtype;
char mtext[128];
}MsgData;
int main()
{
int msgid = msgget((key_t)1234, 0664 | IPC_CREAT);
assert(msgid != -1);
MsgData data;
memset(&data, 0, sizeof(data));
data.mtype = 100;
strcpy(data.mtext, "hello");
msgsnd(msgid, &data, strlen(data.mtext), 0);
memset(&data, 0, sizeof(data));
data.mtype = 200;
strcpy(data.mtext, "world");
msgsnd(msgid, &data, strlen(data.mtext), 0);
//不能发送完后直接删除消息队列
//msgctl(); 删除立即生效
}
接收方
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <sys/msg.h>
typedef struct msgdata
{
long mtype;
char mtext[128];
}MsgData;
int main()
{
int msgid = msgget((key_t)1234, 0664 | IPC_CREAT);
assert(msgid != -1);
MsgData data;
memset(&data, 0, sizeof(data));
msgrcv(msgid, &data, 127, 100, 0);
printf("data.type: %d\n", data.mtype);
printf("data.text: %s\n", data.mtext);
}
共享内存
是最快的IPC(进程间通讯),系统上的进程,每个进程都有 4G 虚拟地址空间,而进程的真实的物理空间都是独立的。在物理内存上找一块空间,使得多个进程都能访问这块空间。
- 创建内核对象,并且申请物理内存
- 各进程中,分别将自己的虚拟地址通过内核对象映射到开辟物理内存上。链接
- 分别访问这块内存 通过 ptra ptrb
- 删除内核对象
A 进程和 B 进程分别保存两个虚拟地址,这两个地址可以相同,也可以不相同但是其映射的物理内存是相同的
消息队列和共享内存的区别
消息队列:创建和后期发送接收数据时访问通过内核对象访问内存空间
共享内存创建的时候创建内核对象,内核对象直接指向内存上的一块区域(物理内存),后期通过一系列的操作将 ptra 返回虚拟地址,A 的虚拟地址通过二级页面映射直接映射到共享内存,B 同理。
共享内存是一种最快的 IPC
- 共享内存一旦使进程映射到此共享内存区域,后续操作时,不需要内核态切换用户态
- 共享内存在使用时,会比其他的通讯方式少两次数据的拷贝。管道、消息队列等发送数据时先在自己的进程空间里定义一个 buff,把数据写在 buff 里面,然后通过 write 等函数将数据写到内存上,因此把用户态切换内核态
共享内存的缺点
消息队列操作时必须通过操作系统的控制,共享内存时指针直接指向内存,意味着没有通过操作系统控制
指针 A 和指针 B 直接指向内存,B 读取数据时不知道 A 是否把数据写入内存。因此,使用共享内存进行进程间通讯必须要使用信号量进行进程同步,使 B 进程的执行在 A 进程之后。
共享内存对于多进程就是一块临界资源。多进程使用共享内存空间时就必须做到同步控制—信号量
管道代码:
char buff[128];
fgets();
write(fd,buff,strlen(buff));
共享内存:
ptra 指向物理内存空间
fgets (ptra, 128, stdin);
操作
创建或获取
int shmget ((key_t)key, int size, int flag)
size: 申请的共享内存空间大小
将进程中虚拟地址映射到共享的内存区域
void *shmat (int shmid, const void *attr, int flag);
返回链接的虚拟地址 失败返回 -1
断开链接
int shmdt (void *addr);
删除内核对象
int shmctl (int shmid, int cmd, struct shmid_ds *buf);
不会立即删除,但是其他进程就不能通过 shmat 与该段链接
A
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include <sys/shm.h>
#include "sem.h"
int main()
{
int shmid = shmget((key_t) 1234, 128, IPC_CREAT | 0664);
assert(shmid != -1);
int semVal = 1;
int semid = SemGet(1000, &semVal, 1);
char *ptr = (char*)shmat(shmid, NULL, 0);
assert(ptr != (char*)-1);
while(1)
{
int index = 0;
SemP(semid, &index, 1);
printf("please input: ");
fgets(ptr, 128, stdin);
SemV(semid, &index, 1);
if(strncmp(ptr, "end", 3) == 0)
{
break;
}
}
shmdt(ptr);
}
B
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include <sys/shm.h>
#include "sem.h"
int main()
{
int shmid = shmget((key_t) 1234, 128, IPC_CREAT | 0664);
assert(-1 != shmid);
int semVal = 1;
int semid = SemGet(1000, &semVal, 1);
char *ptr = (char *)shmat(shmid, NULL, 0);
assert((char*)-1 != ptr);
while(1)
{
int index = 0;
SemP(semid, &index, 1);
if(strncmp(ptr, "end", 3) == 0)
{
break;
}
printf("lenth = %d : %s", strlen(ptr) - 1, ptr);
sleep(2);
SemV(semid, &index, 1);
}
shmdt(ptr);
}
内容 | 效率 | 共享实质 | 进程数量 | 阻塞机制 | |
---|---|---|---|---|---|
有名管道 | 数据 | 低 | 磁盘上文件标识 | 两个 | read |
无名管道 | 数据 | 低 | 父子进程共享文件描述符 | 父子两个 | read |
消息队列 | 消息 | 低 | 内核对象 | n | msgrcv |
信号量 | 同步 | - | 内核对象 | n | P操作 |
共享内存 | 数据 | 低 | 内核对象 | n | P操作 |
有名管道
通过管道文件,完成多进程的通讯,一般用于两个进程间的通讯
无名管道
借助于父子进程共享 fork 之前打开的文件描述符,只能应用于父子进程之间
消息队列
发送带有类型的数据,可以真正实现多进程间通讯
共享内存
使得两进程间共享同一块物理内存区域,实现数据传递,但是必须使用信号量控制进程同步
共享内存是最快的一种IPC
信号量
完成进程同步控制,用于多进程访问同临界资源时
信号
完成进程间消息通知的。