IPC进程间通信
进程间通信方式:
- 早期通信:匿名管道、有名管道、信号
- system V IPC:共享内存、信号灯集、消息队列
- BSD: socket
这里简要分析进程间通信的六种方式,分别为:匿名管道、有名管道、信号、共享内存、信号灯集、消息队列。
匿名管道
特点
- 只能用于具有亲缘关系的进程间通信。
- 半双工通信方式,具有固定的读端和写端
- 无名管道可以看作一种特殊的文件。对他的读写操作采用文件IO的读写方式read、write
- 管道是一种基于文件描述符的通信方式,当一个管道建立,会自动的创建两个文件描述符fd[0]、fd[1]其中fd[0]是固定的读端,fd[1]是固定的写端
函数接口
int pipe(int fd[2])
功能:创建无名管道
参数:文件描述符 fd[0]:读端 fd[1]:写端
返回值:成功 0
失败 -1
代码实现:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char const *argv[])
{
int fd[2];
pid_t pid;
if (pipe(fd) < 0)
{
perror("pipe err");
return -1;
}
pid = fork();
if (pid < 0)
{
perror("fork err");
return -1;
}
else if (pid == 0)
{
char arr[32] = {0};
int n;
close(fd[1]);
while (1)
{
n = read(fd[0], arr, 32);
if (n == 0)
{
close(fd[0]);
break;
}
printf("%s\n", arr);
}
exit(0);
}
else
{
char arr[32] = {0};
close(fd[0]);
while (1)
{
fgets(arr, 32, stdin);
if (strcmp(arr, "quit\n") == 0)
{
close(fd[1]);
break;
}
write(fd[1], arr, 32);
}
wait(NULL);
exit(0);
}
return 0;
}
有名管道
有名管道也叫FIFO,是一种文件类型。
特点
- 可以用于两个互不相干进程通信,是半双工通信,可过文件IO进行操作。
- 有名管道可以通过路径名指出,在文件系统中可见,但内容存放在内存里。
- 有名管道遵循先进先出原则,不支持lseek操作。
函数接口
int mkfifo(const char *filename,mode_t mode);
功能:创健有名管道
参数:filename:有名管道文件名
mode:权限,与open函数中mode相同
返回值:成功:0
失败:-1,并设置errno号
当管道中无数据时,读操作会阻塞;管道中无数据,将写端关闭,读操作会立即返回
管道中装满(管道大小64K)数据写阻塞,一旦有4k空间,写继续,直到写满为止
只有在管道的读端存在时,向管道中写入数据才有意义,否则,会导致管道破裂。
代码实现:
infifo.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("usage:%s <file1>", argv[0]);
return -1;
}
int fd1 = open(argv[1], O_RDONLY);
if (fd1 < 0)
{
perror("open file1 err");
return -1;
}
if (mkfifo("./fifo", 0666) < 0)
{
if (errno == EEXIST)
printf("fifo file exist\n");
else
{
perror("fifo err");
return -1;
}
}
printf("mkfifo succ\n");
int fd= open("./fifo", O_WRONLY);
if (fd < 0)
{
perror("open fifo");
return -1;
}
printf("open1 success %d\n",fd);
char arr[32];
int n;
while (n = read(fd1, arr, 32))
{
write(fd, arr, n);
}
close(fd);
close(fd1);
return 0;
}
outfifo.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("usage:%s <file1>", argv[0]);
return -1;
}
int fd2 = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd2 < 0)
{
perror("open file1 err");
return -1;
}
if (mkfifo("./fifo", 0666) < 0)
{
if (errno == EEXIST)
printf("fifo file exist\n");
else
{
perror("fifo err");
return -1;
}
}
printf("mkfifo succ\n");
int fd;
if ((fd = open("./fifo", O_RDONLY)) < 0)
{
perror("open fifo");
return -1;
}
printf("open2 success %d\n",fd);
char arr[32];
int n;
while (1)
{
n = read(fd, arr, 32);
if (n == 0)
break;
write(fd2, arr, n);
}
close(fd);
close(fd2);
return 0;
}
信号
对Linux来说,信号是一次软中断,是一种异步通信方式。
信号的种类
每个信号都有一个名字和编号以及相对应的功能,这些名字都以“SIG”开头。我们可以通过命令行命令kill -l来查看信号的名字以及序号。
信号响应方式
信号的处理有三种方法,分别是:忽略、捕捉和默认。
- 忽略信号:对信号不做任何处理,但是有两个信号不能被忽略:SIGKILL SIGSTOP
- 捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。不能被捕捉:SIGKILL SIGSTOP
- 执行默认缺省操作:linux对每种信号都规定了默认操作
函数接口
int kill(pid_t pid, int sig);
功能:信号发送
参数:pid:指定进程
sig:要发送的信号
返回值:成功 0
失败 -1
sighandler_t signal(int signum, sighandler_t handler);
功能:信号处理函数
参数:signum:要处理的信号
handler:信号处理方式
SIG_IGN:忽略信号
SIG_DFL:执行默认操作
handler:捕捉信号 void handler(int sig)
返回值:成功:设置之前的信号处理方式
失败:-1
代码实现司机与售票员问题:
1.售票员捕捉SIGINT(代表开车)信号,向司机发送SIGUSR1信号,司机打印(let's gogogo)
2.售票员捕捉SIGQUIT(代表停车)信号,向司机发送SIGUSR2信号,司机打印(stop the bus)
3.司机捕捉SIGTSTP(代表到达终点站)信号,向售票员发送SIGUSR1信号,售票员打印(please get off the bus)
4.司机等待售票员下车,之后司机再下车。
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
pid_t pid;
void handler1(int sig)
{
switch (sig)
{
case SIGINT:
kill(getppid(), SIGUSR1);
break;
case SIGQUIT:
kill(getppid(), SIGUSR2);
break;
case SIGUSR1:
printf("please get off the bus\n");
raise(SIGKILL);
break;
default:
break;
}
}
void handler2(int sig)
{
switch (sig)
{
case SIGUSR1:
printf("let's gogogo\n");
break;
case SIGUSR2:
printf("stop the bus\n");
break;
case SIGTSTP:
kill(pid, SIGUSR1);
break;
case SIGCHLD:
raise(SIGKILL);
break;
default:
break;
}
}
int main(int argc, char const *argv[])
{
pid = fork();
if (pid < 0)
{
perror("fork err");
return -1;
}
else if (pid == 0)
{
signal(SIGINT, handler1);
signal(SIGQUIT, handler1);
signal(SIGUSR1, handler1);
signal(SIGTSTP, SIG_IGN);
while (1)
pause();
}
else
{
signal(SIGUSR1, handler2);
signal(SIGUSR2, handler2);
signal(SIGTSTP, handler2);
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGCHLD, handler2);
while (1)
pause();
}
return 0;
}
共享内存
共享内存是一种最为高效的进程间通信方式,为了在多个进程间交换信息,内核专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。
使用:ipcs -m: 查看系统中的共享内存,ipcrm -m shmid:删除共享内存
函数接口
key_t ftok(const char *pathname, int proj_id);
功能:产生一个独一无二的key值
参数:
Pathname:已经存在的可访问文件的名字
Proj_id:一个字符(因为只用低8位)
返回值:成功:key值
失败:-1
key值组成:
前两位是字符的ascii值
中间两位是系统编号
后四位是文件对应inode号的后四位。
int shmget(key_t key, size_t size, int shmflg);
功能:创建或打开共享内存
参数:
key 键值
size 共享内存的大小
shmflg IPC_CREAT|IPC_EXCL(判错)|0666
返回值:成功 shmid
出错 -1
void *shmat(int shmid,const void *shmaddr,int shmflg);
功能:映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
参数:
shmid 共享内存的id号
shmaddr 一般为NULL,表示由系统自动完成映射
如果不为NULL,那么由用户指定
shmflg:SHM_RDONLY就是对该共享内存只进行读操作
0 可读可写
返回值:成功:完成映射后的地址,
失败:-1的地址
int shmdt(const void *shmaddr);
功能:取消映射
参数:要取消的地址
返回值:成功0
失败的-1
int shmctl(int shmid,int cmd,struct shmid_ds *buf);
功能:对共享内存进行各种操作(可删除共享内存)
参数:
shmid 共享内存的id号
cmd IPC_STAT 获得shmid属性信息,存放在第三参数
IPC_SET 设置shmid属性信息,要设置的属性放在第三参数
IPC_RMID:删除共享内存,此时第三个参数为NULL即可
返回:成功0
失败-1
用法:shmctl(shmid,IPC_RMID,NULL)
操作步骤
- 创建key值,ftok
- 创建或打开共享内存 shmget
- 映射共享内存到地址空间 shmat
- 撤销映射 shmdt
- 删除共享内存 shmctl
代码实现:
inputshm.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
typedef struct DATA
{
int flag;
char arr[64];
} * data;
int main(int argc, char const *argv[])
{
key_t key = ftok(".", 66);
if (key < 0)
{
perror("ftok err");
return -1;
}
int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0600);
if (shmid < 0)
{
if (errno == EEXIST)
shmid = shmget(key, 128, 0600);
else
{
perror("shmget err");
return -1;
}
}
//使用共享内存地址空间首地址作为输入输出标志位
// char *p = shmat(shmid, NULL, 0);
// while (1)
// {
// if (*p == 0)
// {
// fgets(p + 1, 127, stdin);
// *p = 'A';
// }
// if (strcmp(p + 1, "quit\n") == 0)
// break;
// }
//使用附带标志位的结构体传送数据
data p = shmat(shmid, NULL, 0);
while (1)
{
fgets(p->arr, 64, stdin);
p->flag=1;
if (strcmp(p + 1, "quit\n") == 0)
break;
}
shmdt(p);
shmctl(shmid, IPC_RMID, NULL); //映射连接数为0时删除
return 0;
}
outputshm.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
typedef struct DATA
{
int flag;
char arr[64];
} * data;
int main(int argc, char const *argv[])
{
key_t key = ftok(".", 66);
if (key < 0)
{
perror("ftok err");
return -1;
}
int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0600);
if (shmid < 0)
{
if (errno == EEXIST)
shmid = shmget(key, 128, 0600);
else
{
perror("shmget err");
return -1;
}
}
//使用共享内存地址空间首地址作为输入输出的标志位
// char *p = shmat(shmid, NULL, 0);
// while (1)
// {
// if (*p == 0)
// continue;
// if (*p == 'A')
// {
// if (strcmp(p + 1, "quit\n") == 0)
// {
// // *p = *(p+1)=0;//不删除共享内存,前两位清空
// break;
// }
// fputs(p + 1, stdout);
// *p = 0;
// }
// }
//使用附带标志位的结构体传送数据
data p = shmat(shmid, NULL, 0);
while (1)
{
if (p->flag = 1)
{
if (strcmp(p + 1, "quit\n") == 0)
break;
fputs(p->arr,stdout);
p->flag=0;
}
}
shmdt(p);
return 0;
}
信号灯集
概念
信号灯(semaphore),也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制,并不存储进程间通信数据;System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。而Posix信号灯指的是单个计数信号灯。
函数接口
int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯
参数:key:ftok产生的key值
nsems:信号灯集中包含的信号灯数目
semflg:信号灯集的访问权限,通常为IPC_CREAT |0666
返回值:成功:信号灯集ID
失败:-1
int semctl ( int semid, int semnum, int cmd…/*union semun arg*/);
功能:信号灯集合的控制(初始化/删除)
参数:semid:信号灯集ID
semnum: 要操作的集合中的信号灯编号
cmd:
GETVAL:获取信号灯的值,返回值是获得值
SETVAL:设置信号灯的值,需要用到第四个参数:共用体
IPC_RMID:从系统中删除信号灯集合
返回值:成功 0
失败 -1
int semop ( int semid, struct sembuf *opsptr, size_t nops);
功能:对信号灯集合中的信号量进行PV操作
参数:semid:信号灯集ID
opsptr:操作方式
nops: 要操作的信号灯的个数 1个
返回值:成功 :0
失败:-1
struct sembuf {
short sem_num; // 要操作的信号灯的编号
short sem_op; // 0 : 等待,直到信号灯的值变成0
// 1 : 释放资源,V操作
// -1 : 申请资源,P操作
short sem_flg; // 0(阻塞),IPC_NOWAIT, SEM_UNDO
};
操作步骤
- 创建key值 ftok
- 创建或打开信号灯集 semget
- 初始化信号灯 semctl
- PV操作 semop
- 删除信号灯集 semctl
代码实现共享内存同步操作:
input.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/sem.h>
union semun {
int val;
};
key_t getkey()
{
key_t key = ftok(".", 'A');
if (key < 0)
{
perror("ftok err");
return -1;
}
return key;
}
void initsem(int semid, int semnum, int val)
{
union semun sem;
sem.val = val;
semctl(semid, semnum, SETVAL, sem);
}
int creatsem(key_t key, int n, int arr[])
{
int semid = semget(key, n, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
if (errno == EEXIST)
semid = semget(key, n, 0666);
else
{
perror("semget err");
return -1;
}
}
else
{
for (int i = 0; i < n; i++)
initsem(semid, i, arr[i]);
}
return semid;
}
void Popt(int semid, int semnum)
{
struct sembuf buf;
buf.sem_num = semnum;
buf.sem_op = -1;
buf.sem_flg = 0;
semop(semid, &buf, 1);
}
void Vopt(int semid, int semnum)
{
struct sembuf buf;
buf.sem_num = semnum;
buf.sem_op = 1;
buf.sem_flg = 0;
semop(semid, &buf, 1);
}
int main(int argc, char const *argv[])
{
key_t key = ftok(".", 66);
if (key < 0)
{
perror("ftok err");
return -1;
}
int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0600);
if (shmid < 0)
{
if (errno == EEXIST)
shmid = shmget(key, 128, 0600);
else
{
perror("shmget err");
return -1;
}
}
char *p = shmat(shmid, NULL, 0);
if(p==(void *)-1)
{
perror("shmat err");
return -1;
}
key = getkey();
int arr[2] = {1, 0};
int semid = creatsem(key, 2, arr);
while (1)
{
Popt(semid, 0);
fgets(p, 127, stdin);
Vopt(semid, 1);
if (strcmp(p, "quit\n") == 0)
break;
}
shmdt(p);
return 0;
}
output.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <sys/sem.h>
union semun {
int val;
};
key_t getkey()
{
key_t key = ftok(".", 'A');
if (key < 0)
{
perror("ftok err");
return -1;
}
return key;
}
void initsem(int semid, int semnum, int val)
{
union semun sem;
sem.val = val;
semctl(semid, semnum, SETVAL, sem);
}
int creatsem(key_t key, int n, int arr[])
{
int semid = semget(key, n, IPC_CREAT | IPC_EXCL | 0666);
if (semid <= 0)
{
if (errno == EEXIST)
semid = semget(key, n, 0666);
else
{
perror("semget err");
return -1;
}
}
else
{
for (int i = 0; i < n; i++)
initsem(semid, i, arr[i]);
}
return semid;
}
void Popt(int semid, int semnum)
{
struct sembuf buf;
buf.sem_num = semnum;
buf.sem_op = -1;
buf.sem_flg = 0;
semop(semid, &buf, 1);
}
void Vopt(int semid, int semnum)
{
struct sembuf buf;
buf.sem_num = semnum;
buf.sem_op = 1;
buf.sem_flg = 0;
semop(semid, &buf, 1);
}
int main(int argc, char const *argv[])
{
key_t key = ftok(".", 66);
if (key < 0)
{
perror("ftok err");
return -1;
}
int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0600);
if (shmid < 0)
{
if (errno == EEXIST)
shmid = shmget(key, 128, 0600);
else
{
perror("shmget err");
return -1;
}
}
char *p = shmat(shmid, NULL, 0);
if (p == (void *)-1)
{
perror("shmat err");
return -1;
}
key = getkey();
int arr[2] = {1, 0};
int semid = creatsem(key, 2, arr);
while (1)
{
Popt(semid, 1);
if (strcmp(p, "quit\n") == 0)
break;
fputs(p, stdout);
Vopt(semid, 0);
}
shmdt(p);
shmctl(shmid, IPC_RMID, NULL);
semctl(semid, 0, IPC_RMID);
return 0;
}
消息队列
概念及特点
- 消息队列是IPC对象的一种
- 由消息队列ID来唯一标识
- 就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等
- 可以按照类型来发送(添加)/接收(读取)消息
函数接口
int msgget(key_t key, int flag);
功能:创建或打开一个消息队列
参数: key值
flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666
返回值:成功:msgid
失败:-1
int msgsnd(int msqid, const void *msgp, size_t size, int flag);
功能:添加消息
参数:msqid:消息队列的ID
msgp:指向消息的指针。常用消息结构msgbuf如下:
struct msgbuf{
long mtype; //消息类型
char mtext[N]}; //消息正文
size:发送的消息正文的字节数
flag:IPC_NOWAIT消息没有发送完成函数也会立即返回
0:直到发送完成函数才返回
返回值:成功:0
失败:-1
int msgrcv(int msgid, void* msgp, size_t size, long msgtype, int flag);
功能:读取消息
参数:msgid:消息队列的ID
msgp:存放读取消息的空间
size:接受的消息正文的字节数
msgtype:0:接收消息队列中第一个消息。
大于0:接收消息队列中第一个类型为msgtyp的消息.
小于0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
flag:0:若无消息函数会一直阻塞
IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
返回值:成功:接收到的消息的长度
失败:-1
int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );
功能:对消息队列的操作,如删除消息队列
参数:msqid:消息队列的队列ID
cmd:
IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
IPC_SET:设置消息队列的属性。这个值取自buf参数。
IPC_RMID:从系统中删除消息队列。
buf:消息队列缓冲区
返回值:成功:0
失败:-1
用法:msgctl(msgid, IPC_RMID, NULL)
操作步骤
- 创建key值 ftok
- 创建或打开消息队列 msgget
- 添加消息:按照类型将消息添加到以打开的消息队列末尾 msgsnd
- 读取消息:可以按照类型将消息从消息队列中读走 msgrcv
- 删除消息队列 msgctl
测试代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
struct msgbuf
{
long mtype;
char text[32];
};
int main(int argc, char const *argv[])
{
key_t key = ftok(".", 66);
if (key < 0)
{
perror("ftok err");
return -1;
}
int msgid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);
if (msgid <= 0)
{
if (errno == EEXIST)
msgid = msgget(key, 0666);
else
{
perror("msgget err");
return -1;
}
}
printf("msgid:%d\n", msgid);
struct msgbuf buf;
buf.mtype=123;
fgets(buf.text,32,stdin);
msgsnd(msgid,&buf,sizeof(buf)-4,0);
struct msgbuf msg;
msgrcv(msgid,&msg,sizeof(msg)-4,123,0);
printf("%s",msg.text);
msgctl(msgid,IPC_RMID,NULL);
return 0;
}
IPC方式总结
- 无名管道:速度慢,容量有限,只有父子进程能通讯
- 有名管道:任何进程间都能通讯,速度慢
- 信号:实现异步的软中断
- 共享内存:能够很容易控制容量,速度快,但要注意同步问题
- 信号灯集:不能传递复杂消息,只能用来同步。用于实现进程间的互斥与同步,而不是用于存储进程间通信数据
- 消息队列:容量受到系统限制,要注意读的时候,要考虑上一次没有读完数据的问题