一、进程间通信方式
1. 传统通信方式:
1、无名管道 (在内核开辟的一个缓冲区(默认64KB))
2、有名管道 (在内存中开辟一个缓冲区,且在文件系统中会生成一个管道文件)
3、信号 (例如 kill)
2. IPC对象
共享内存
消息队列
信号灯集
3. 套接字:
socket
二、基本术语
1. 单工:
单向通信
2. 半双工:
可以相互通信,不能同时进行
3. 全双工:
可以同时相互通信
4. 原子操作:
不可被打断的操作
三、无名管道
单工通信,并且只适用具有亲缘关系的进程间通信;具有读端和写端。
流程:创建—>读/写----->关闭
pipe():创建无名管道
int pipe(int fd[2]);
返回值:0成功,-1失败
fd[0]:固定的读端
fd[1]:固定的写端
当读端存在时:
无名管道有空间:
1. 空间充足:直接写入数据,write函数返回写入数据的字节数
2. 空间不足:write会写入能够写入的数据,不保证原子操作,如果读进程不读走管道缓冲区中的数据,那么写操作将会一直阻塞。
读端不存在:
wirte出现管道破裂。
写端存在:
有数据: read直接返回实际读取的字节数。
无数据: read阻塞等待数据写入。
写端不存在:
有数据: read直接返回实际读取的字节数。
无数据:read直接返回0。
代码示例
/*===============================================
* 文件名称:Anonymous_pipe.c
* 创 建 者:
* 创建日期:2023年08月22日
* 描 述:
================================================*/
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#define N 64
int main(int argc, char *argv[])
{
pid_t pid0 , pid1 ; //定义两个进程号类型的变量
int fd[2];//定义一个为2大小的数组,创建管道后fd[0]为读端,fd[2]为写端
char buf[N];//缓冲区
//创建无名管道
if( pipe(fd) < 0 )
{
perror("pipe");
exit(-1);
}
pid0 = fork(); //创建子进程0
if( -1 == pid0 )
{
perror("fork0");
exit(-1);
}
else if( 0 == pid0 )
{
//子进程0 定为写端
close(fd[0]); // 关闭读端
strcpy(buf,"I'm son0 ");//写内容到buf
write(fd[1],buf,N);//将buf内容写到无名管道
close(fd[1]);//写完关闭无名管道
exit(0);//退出该子进程
}
else
{
//父进程
pid1 = fork();//创建子进程1
if( -1 == pid1 )
{
perror("fork1");
exit(-1);
}
else if( 0 == pid1 )
{
//关闭读端
close(fd[0]);
strcpy(buf,"I'm son 1 ");//写内容到buf
write(fd[1],buf,N);//将buf内容写到无名管道
close(fd[1]);//写完关闭无名管道
exit(0);//退出该子进程
}
else
{
//父进程
close(fd[1]);//关闭写端
read(fd[0],buf,N);//将管道内容读到buf
puts(buf);//输出buf中内容
read(fd[0],buf,N);//将管道内容读到buf
puts(buf);//输出buf中内容
wait(NULL);//一个等待进程结束
close(fd[0]);//关闭读端
}
}
return 0;
}
结果:
四、有名管道
无名管道只能用于具有亲缘关系的进程之间,这就限制了无名管道的使用范围。
有名管道可以使互不相关的两个进程互相通信。有名管道可以通过路径名来指出,并且在文件系统中可见。
进程通过文件IO来操作有名管道。
有名管道遵循先进先出规则。
不支持如lseek() 操作 。
mkfifo():创建有名管道
int mkfifo(const char *filename, mode_t mode);
返回值:成功0 失败-1
const char *filename:要创建有名管道的名字
mode_t mode:管道的权限 一般为0666
代码示例
两个进程使用有名管道实现通信
进程1
/*===============================================
* 文件名称:read.c
* 创 建 者:
* 创建日期:
* 描 述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
//打开有名管道
int rfd = open("/home/rfifo",O_RDONLY);
if( -1 == rfd )
{
perror("open rfd");
return -1;
}
int wfd = open("/home/Wfifo",O_WRONLY);
if( -1 == wfd )
{
perror("open wfd");
return -1;
}
puts("open fifo successed");
char buf[32] ; //定义缓冲区
while(1)
{
read(rfd,buf,32); //读管道中内容到buf
if( 0 == strcmp(buf,"quit\n"))//输入quit退出
{
break;
}
printf("recv: ");
puts(buf);//打印读到的内容
//写内容到管道
printf("Please send:");
//memset(buf,0,32);
fgets(buf,32,stdin);
write(wfd,buf,32);//写内容到管道
if( 0 == strcmp(buf,"quit\n"))//输入quit退出
{
break;
}
}
//关闭管道
close(rfd);
close(wfd);
return 0;
}
进程2
/*===============================================
* 文件名称:write.c
* 创 建 者:
* 创建日期:
* 描 述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
//创建管道
/*
if( mkfifo("/home/rfifo",0666))
{
perror("mkfifo r");
return -1;
}
if( mkfifo("/home/Wfifo",0666))
{
perror("mkfifo w");
return -1;
}
*/
//打开有名管道
int wfd = open("/home/rfifo",O_WRONLY);
if( -1 == wfd )
{
perror("W open rfd");
return -1;
}
int rfd = open("/home/Wfifo",O_RDONLY);
if( -1 == rfd )
{
perror("W open wfd");
return -1;
}
puts("open fifo successed");
char buf[32] ; //定义缓冲区
while(1)
{
//写内容到管道
printf("Please send:");
fgets(buf,32,stdin);
write(wfd,buf,32);//写内容到管道
if( 0 == strcmp(buf,"quit\n"))//输入quit退出
{
break;
}
read(rfd,buf,32); //读管道中内容到buf
//memset(buf,0,32);
if( 0 == strcmp(buf,"quit\n"))//输入quit退出
{
break;
}
printf("recv: ");
puts(buf);//打印读到的内容
}
//关闭管道
close(rfd);
close(wfd);
return 0;
}
五、信号通信
信号是在软件层次上对中断机制的一种模拟,是一种异步通信方式
信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。
如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程
用户进程对信号的响应方式:
忽略信号:对信号不做任何处理,但是有两个信号不能忽略:即SIGKILL及SIGSTOP。
捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。
执行缺省操作:Linux对每种信号都规定了默认操作
异步通信: 当我们关注某一个事件时,如果事件未发生,我们继续完成自己任务,
当事件发生后,它会给我们传递一个信号,此时我们再回头完成对应事件
信号处理方式:
捕捉:信号发生后,我们通过某些方法(函数)获取对应信号然后自定义该信号的处理方式
忽略:信号不在执行对应的默认操作(SIGKILL/SIGSTOP除外)
默认:
kill指令:
kill -n pid: 给pid进程发送 第n个信号
kill -n -pid: 给pid进程组发送第n个信号
kill -n -1: 给除了init进程和本进程外其它所有进程发送第n个信号
int kill(int pid, int sig);
返回值: 成功 0 失败 -1
int pid: 0 同组进程; -1 除init外的所有进程 ; >0 进程号==pid
int sig: 对应的信号
unsinged int alarm(unsinged int s);
unsinged int s: 定时秒数
unsinged int: 失败 -1; 如果之前调用alarm函数,则返回上一个闹钟剩余时间
六、IPC对象
1.每个IPC对象有唯一的ID(操作对应IPC对象的凭证)
2.IPC对象创建后一直存在,直到被显示地删除(函数/命令)
3.每一个IPC对象有一个关联的key(多个进程找到同一个IPC对象,事先约定好)
ipcs/ipcrm(查看操作指令)
key == 0 表示私有
key:
IPC_PRIVATE(0):表示私有
ftok():生产key值
key_t ftok(const char *path, int proj_id);
返回值: 成功 返回key 失败返回 EOF
const char *path: 存在切可访问的文件路径
int proj_id: 用于生成key的数字,不能为0 且低八位有效
七、共享内存
效率最高的进程间通信方式,进程可以直接读写内存,而不需要任何数据拷贝
共享内存在内核空间创建,只有被映射到用户空间才可以使用
当多个进程同时访问共享内存时,需要加入同步或者互斥机制配合使用
操作步骤:
1.创建/打开共享内存
int shmget(key_t key, int size, int shmflg);
返回值: 成功时返回共享内存ID, 失败返回EOF
key_t key: 和共享内存关联的key, IPC_PRIVATE/ftok生成
int size: 共享内存的大小,以字节为单位
int shmflg: 共享内存标志位是否新建(IPC_CREAT|0666)及操作权限
2.映射到用户空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
返回值: 成功 映射后的地址, 失败 返回(void *)-1
int shmid: 对应的共享内存ID
const void *shmaddr: 要映射后的地址, NULL表示由系统自动映射
int shmflg: 0表示可读可写;SHM_RDONLY表示只读
3.读/写共享内存(通过指针访问/指针类型根据共享内存中存放的数据类型决定)
fgets()
strcpy()
...
4.撤销共享内存映射
int shmdt(void *shmaddr);
返回值: 成功返回0, 失败返回EOF
void *shmaddr: 共享内存映射的地址
注意: 一般不使用共享内存时应及时撤销映射,进程结束时自动撤销所有的共享
内存映射,若未取消映射,则无法删除对应的共享内存
5.删除共享内存(只是标记,直到所有相关进程退出时才删除)
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
返回值:成功 0,失败 EOF
int shmid: 要删除的共享内存ID
int cmd: 要执行的操作 IPC_STAT IPC_SET IPC_RMID
struct shmid_ds *buf: 存放共享内存的属性(删除时为NULL)
注意:
每块共享内存有大小限制: ipcs -l (cat /proc/sys/kernel/shmmax )
多进程使用时,应由第一个进程创建,所有映射取消时才会真正的删除
八、消息队列:
1.使用最普遍
2.消息队列ID来唯一标识
3.是一个消息的列表,用户可以添加/读取消息
4.消息队列可以按照类型来发送/接收
使用步骤:
1.打开/创建消息队列 msgget
int masgget(key_t key, int msgflg);
返回值: 成功对应消息队列ID, 失败返回EOF
key_t key: 关联的KEY
int msgflg: 标志位 IPC_CREAT|0666
2.向消息队列发送消息 msgsnd
int msgsnd(int msgid, const void *msgp, size_t size, int msgflg);
返回值: 成功返回0, 失败返回 -1
int msgid: 对应消息队列ID
const void *msgp: 要发送消息的缓冲区地址
size_t size: 指定本次发送消息的长度(正文长度)
int msgflg: 标志位 0/IPC_NOWAIT (0阻塞 IPC_NOWAIT非阻塞(消息队列满时))
注:
消息格式:
1.根据需求定义结构体类型
2.首成员类型必须为long,代表消息的类型
3.其它成员都属于正文
typedef struct{
long mtype; //类型,正整数
... //成员
}MSG;
3.从消息队列接收消息 msgrcv
int msgrcv(int msgid, void *msgp, size_t size, long msgtype, int msgflg);
返回值: 成功返回消息的长度, 失败 -1
int msgid: 消息队列的id
void *msgp: 接收的消息存放的缓冲区
size_t: 指定接收消息的长度(小于实际长度,剩余消息丢失;超过无影响)
long msgtype: 指定接收消息的类型(0 按时间接收最早的消息; 负数按照优先级接收(几乎没见到))
int msgflg: 标志位 0/IPC_NOWAIT
4.控制消息队列 msgctl
int msgctl(int msgid, int cmd, struct msqid_ds *buf);
返回值: 成功0, 失败 -1
int msgid: 消息队列ID
int cmd: 要执行的操作 IPC_STAT/IPC_SET/IPC_RMID
struct msqid_ds *buf: 存放消息队列属性的地址
代码示例
使用消息队列实现两进程数据的交互
/*===============================================
* 文件名称:clientA.c
* 创 建 者:
* 创建日期:
* 描 述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define N 64
typedef struct
{
long mtype;
char data[N];
}MSG;
#define LEN (sizeof(MSG)-sizeof(long))
#define TypeA 100
#define TypeB 200
int main(int argc, char *argv[])
{
//创建关联的key
key_t key = ftok("./",'a');
if( -1 == key )
{
perror("ftok");
return -1;
}
//创建结构体
MSG m ;
//创建或者打开消息队列
int msg = msgget(key,IPC_CREAT|0666);
if( EOF == msg )
{
perror("msgget creat err");
return -1;
}
while(1)//不间断发送或接收消息
{
//发送消息
printf("A send:");
fgets(m.data,N,stdin);
m.mtype = TypeB;
if( -1 == msgsnd(msg,&m,LEN,0))
{
perror("mgsnd");
return -1;
}
if( 0 == strcmp(m.data,"quit\n"))
{
break;
}
//接收消息
if( -1 == msgrcv(msg,&m,LEN,TypeA,0))
{
perror("msgrcv");
return -1;
}
if( 0 == strcmp(m.data,"quit\n"))
{
msgctl(msg,IPC_RMID,NULL);
break;
}
printf("Rcve B: %s ",m.data);
}
return 0;
}
/*===============================================
* 文件名称:clientB.c
* 创 建 者:
* 创建日期:
* 描 述:
================================================*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#define N 64
typedef struct
{
long mtype;
char data[N];
}MSG;
#define LEN (sizeof(MSG)-sizeof(long))
#define TypeA 100
#define TypeB 200
int main(int argc, char *argv[])
{
//创建关联的key
key_t key = ftok("./",'a');
if( -1 == key )
{
perror("ftok");
return -1;
}
//创建结构体
MSG m ;
//打开消息队列
int msg = msgget(key,IPC_CREAT|0666);
if( EOF == msg )
{
perror("msgget creat err");
return -1;
}
while(1)//不间断发送或接收消息
{
//接收消息
if( -1 == msgrcv(msg,&m,LEN,TypeB,0))
{
perror("msgrcv");
return -1;
}
if( 0 == strcmp(m.data,"quit\n"))
{
msgctl(msg,IPC_RMID,NULL);
break;
}
printf("Rcve A: %s ",m.data);
//发送消息
printf("B send:");
fgets(m.data,N,stdin);
m.mtype = TypeA;
if( -1 == msgsnd(msg,&m,LEN,0))
{
perror("mgsnd");
return -1;
}
if( 0 == strcmp(m.data,"quit\n"))
{
break;
}
}
return 0;
}
九、信号灯集:
用于进程/线程实现同步或互斥机制
信号灯的类型:
Posix 无名信号灯(信号量)
Posix 有名信号灯(有对应文件)
System V 信号灯
含义:
计数信号灯(代表某类资源):无名/有名
System V 信号灯是一个或多个计数信号灯的集合
可同时操作集合中的多个信号灯
申请多个资源时避免死锁
使用步骤:
1.打开/创建 semget
int semget(key_t key, int nsems, int semflg);
返回值: 成功返回信号灯集的id;失败 -1
key_t key: 关联的key
int nsems: 集合中包含的计数信号灯个数
int semflg: 标志位 IPC_CREAT|0666 IPC_EXECL(如果已创建则失败)
2.信号灯初始化 semctl
int semctl(int semid, int semnum, int cmd, ...);
返回值: 成功返回0;失败 EOF
int semid: 要操作的信号灯集ID
int semnum: 要操作的集合中的信号灯编号(从0开始类似数组下标)
int cmd: 执行的操作 SETVAL IPC_RMID
union semun: 取决于cmd (需要自己定义这个共用体)
3.信号灯集的P/V操作 semop
int semop(int semid, struct sembuf *sops, unsigned nsops);
返回值: 成功返回0;失败-1
int semid: 要操作的信号灯集
struct sembuf *sops: 描述对信号灯操作的结构体(多个信号灯操作应该定义对应结构体数组)
unsigned nsops: 要操作的信号灯的个数
系统头文件定义好的结构体:
struct sembuf{
short semnum;
short sem_op;
short sem_flg;
};
semnum: 信号灯编号
sem_op: 负数(-1):P操作; 正数(1):V操作
sem_flg: 0/IPC_NOWAIT
sgctl
int msgctl(int msgid, int cmd, struct msqid_ds *buf);
返回值: 成功0, 失败 -1
int msgid: 消息队列ID
int cmd: 要执行的操作 IPC_STAT/IPC_SET/IPC_RMID
struct msqid_ds *buf: 存放消息队列属性的地址
练习:使用消息队列实现两进程数据的交互
3、信号灯集:
用于进程/线程实现同步或互斥机制
信号灯的类型:
Posix 无名信号灯(信号量)
Posix 有名信号灯(有对应文件)
System V 信号灯
含义:
计数信号灯(代表某类资源):无名/有名
System V 信号灯是一个或多个计数信号灯的集合
可同时操作集合中的多个信号灯
申请多个资源时避免死锁
使用步骤:
1.打开/创建 semget
int semget(key_t key, int nsems, int semflg);
返回值: 成功返回信号灯集的id;失败 -1
key_t key: 关联的key
int nsems: 集合中包含的计数信号灯个数
int semflg: 标志位 IPC_CREAT|0666 IPC_EXECL(如果已创建则失败)
2.信号灯初始化 semctl
int semctl(int semid, int semnum, int cmd, ...);
返回值: 成功返回0;失败 EOF
int semid: 要操作的信号灯集ID
int semnum: 要操作的集合中的信号灯编号(从0开始类似数组下标)
int cmd: 执行的操作 SETVAL IPC_RMID
union semun: 取决于cmd (需要自己定义这个共用体)
3.信号灯集的P/V操作 semop
int semop(int semid, struct sembuf *sops, unsigned nsops);
返回值: 成功返回0;失败-1
int semid: 要操作的信号灯集
struct sembuf *sops: 描述对信号灯操作的结构体(多个信号灯操作应该定义对应结构体数组)
unsigned nsops: 要操作的信号灯的个数
系统头文件定义好的结构体:
struct sembuf{
short semnum;
short sem_op;
short sem_flg;
};
semnum: 信号灯编号
sem_op: 负数(-1):P操作; 正数(1):V操作
sem_flg: 0/IPC_NOWAIT
共享内存和信号灯集代码示例
/*===============================================
* 文件名称:shm_sem.c
* 创 建 者:
* 创建日期:
* 描 述:
================================================*/
#include <stdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <unistd.h>
#include <signal.h>
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) */
};
#define N 64
#define READ 0
#define WRITE 1
//信号灯集初始化
int sem_init(int semid, int n, int val[])
{
union semun myun ; // 定义共用体
for(int i = 0; i < n; i++)
{
myun.val = val[i];//对共用体里的成员赋值
if(-1 == semctl(semid, i, SETVAL,myun))
{
return -1;
}
}
return 0;
}
//定义函数来进行便捷的pv操作
void pv(int semid, int n, int op)
{
struct sembuf buf;
buf.sem_num = n;
buf.sem_op = op;
buf.sem_flg = 0;
semop(semid, &buf, 1);
}
int main(int argc, char *argv[])
{
//生成key
key_t key = ftok(".", 'a');
if(-1 == key)
{
perror("ftok");
return -1;
}
//创建共享内存
int shmid = shmget(key, N, IPC_CREAT|0666);
if(-1 == shmid)
{
perror("shmget");
return -1;
}
//映射
char *shmadd = shmat(shmid, NULL, 0);
if((char *)-1 == shmadd)
{
perror("shmadd");
goto _error1;
}
//信号灯集创建
int semid = semget(key, 2, IPC_CREAT|0666|IPC_EXCL);
if(-1 == semid)
{
perror("semget");
goto _error1;
}
//初始化信号灯
int val[2] = {0,1}; //资源数
if(-1 == sem_init(semid, 2, val))
{
goto _error2;
}
//创建子进程
pid_t pid = fork();
if(-1 == pid)
{
perror("fork");
goto _error2;
}
else if(0 == pid)
{
char *p, *q;
while(1)
{
p = q = shmadd;
pv(semid, READ, -1);
while(*p)
{
if(*p != ' ')
*q++ = *p;
p++;
}
*q = '\0';
puts(shmadd);
pv(semid, WRITE, 1);
}
}
else
{
while(1)
{
pv(semid, WRITE, -1); //写资源P操作
printf("input> ");
fgets(shmadd, N, stdin);
if(0 == strcmp(shmadd, "quit\n"))
break;
pv(semid, READ, 1); //读资源V操作
}
kill(pid, SIGUSR1);
}
_error2:
semctl(semid, 2, IPC_RMID);
_error1:
shmctl(shmid, IPC_RMID, NULL);
return 0;
}