常见的通信方式
无名管道通信pipe
无名管道是一种半双工的通信方式,数据只能单方向流动,通信的时候只能在具有亲缘关系的进程间通信,也就是上下级进程
命名管道通信fifo
命名管道是一种半双工的通信方式,但是数据读写具有原子性,优势在于可以用于任意进程之间通信
消息队列MessageQueue
消息队列就是由消息组成的链式队列,存放在内核中并由消息队列标识符进行标识,消息队列克服了消息量少,管道只能承受无格式字节流数据传输以及缓冲区大小受限的缺点
共享内存SharedMemory
共享内存就是映射一块其他的进程都可以访问的内存空间,该段内存由多个进程同时读取数据。共享内存属于最快IPC通信方式,由于通信机制效率最高,那么多个进程同时访问的时候,需要通过信号量互斥进行同步管理
信号量Semaphore
信号量是一个计数器,可以用来控制多个进程对于共享资源的访问,它作为一个锁机制存在,防止某个进程在访问共享资源的时候,影响到其他进程访问该资源,作为进程之间访问共享资源协调手段
信号signal
信号是一种复杂的通信机制,主要利用操作系统的消息机制,通过不同的信号值表示不同的信号内容,从而引起进程执行不同的内容
套接字socket
套接字也是进程当中的一种通信方式,不同的是该种机制用于不同计算机之间的通信,通过套接字的形式完成数据收发
管道通信方式
管道概念:是指无名管道和有名管道,实际上套接字和管道的方式是类似,套接字用于不同计算机之间的通信,管道只能用于同一个计算机之间的进程通信。链接两个进程对象
管道的局限性:
- 数据不能进程自己读自己写
- 管道中数据不能反复读取,一旦读取完成,则管道数据消失
- 管道中采用半双工通信,数据只能单方向流动,不具备原子性写入
无名管道
NAME 创建无名管道
pipe - create pipe
SYNOPSIS
//头文件
#include <unistd.h>
函数原型:
int pipe(int pipefd[2]);
函数参数:
int pipefd[2] 该数组表示无名管道拥有两个文件描述符
pipefd[0] refers to the read end of the pipe. 读端
pipefd[1] refers to the write end of the pipe. 写端
返回值:成功 为0 失败 -1
int main()
{
//定义读端和写端文件描述符数组
int fd[2];
//创建无名管道
int ret=pipe(fd);
if(ret == -1)
{
perror("PIPE falied\n");
return -1;
}
//定义存储缓冲区
char buf[50];
//建立亲缘进程
pid_t x=fork();//创建父子进程
if(x >0)//父进程 读取数据 读端 fd[0]
{
printf("我是父进程:PID=%d\n",getpid());
//无名管道 读端
while(1)
{
//清空缓冲区
memset(buf,0,sizeof(buf));
//读取数据
read(fd[0],buf,sizeof(buf));
printf("接受子进程的内容为:%s\n",buf);
//退出
if(!strcmp(buf,"bye"))
{
break;//退出循环
}
}
wait(NULL);
}
if(x == 0)//子进程 写入数据 写端 fd[1]
{
printf("我是子进程:PID=%d\n",getpid());
//无名管道 写端
while(1)
{
//清空缓冲区
memset(buf,0,sizeof(buf));
//写入数据
printf("请输入你想对父进程说的话\n");
scanf("%s",buf);
write(fd[1],buf,strlen(buf));
//退出
if(!strcmp(buf,"bye"))
{
break;//退出循环
}
}
exit(0);//退出
}
return 0;
}
无名管道没有文件名,只有读端和写端文件描述符,不能使用lseek定位
无名管道采用半双工通信,只能用于亲缘间通信
无名管道若是写入数据,则读端会一直阻塞,直到有数据写入为止
无名管道写入数据时进程死亡,则数据一直保存在管道中
无名管道写入数据没有原子性保存,注意缓冲区大小稳定操作
无名管道操作较为简单,适用场景较为单一,性能较弱限制条件多
查看无名管道的个数“ulimit -a”
有名管道
创建
NAME 生成一个有名管道文件【mkfifo既是函数名又是shell命令】
mkfifo - make a FIFO special file (a named pipe)
SYNOPSIS
#include <sys/types.h>
#include <sys/stat.h>
函数原型:
int mkfifo(const char *pathname, mode_t mode);
函数参数:
const char *pathname, 有名管道的存储路径
mode_t mode 有名管道的权限 八进制 0777
返回值:成功 返回为0 失败 返回-1
删除
NAME 删除有名称的文件对象【unlink既是函数名又是shell命令】
unlink- delete a name and possibly the file it refers to
SYNOPSIS
#include <unistd.h>
函数原型:
int unlink(const char *pathname);
函数参数:
const char *pathname, 提供需要删除文件的路径
返回值:成功 返回为0 失败 返回-1
案例
#include "myhead.h"
#define FIFO "/home/softeem/fifo" //宏定义管道名
//建立有名管道文件
int create_FIFO(const char *pathname)
{
//判断管道文件是否存在
int ret=access(pathname,F_OK);
//管道不存在就创建管道
if(ret == -1)
{
//创建管道文件
ret=mkfifo(pathname,0666);//可读可写
if(ret == -1)
{
perror("fifo failed\n");
return -1;
}
else
return 0;
}
return 0;
}
int main()
{
//建立有名管道文件
int ret=create_FIFO(FIFO);
if(ret == -1)//操作失败 结束运行
return -1;
//打开文件
int fd=open(FIFO,O_RDWR);
if(fd == -1)//打开文件失败
{
perror("open failed\n");
return -1;
}
//准备缓冲区
char buf[50];
//进程send 负责发送
while(1)
{
//清空缓冲区
memset(buf,0,sizeof(buf));
//写入数据
printf("请输入你想对进程说的话\n");
scanf("%s",buf);
write(fd,buf,strlen(buf));
//退出
if(!strcmp(buf,"bye"))
break;//退出循环
}
//关闭文件
close(fd);
return 0;
}
接收端
#include "myhead.h"
#define FIFO "/home/softeem/fifo"
//建立有名管道文件
int create_FIFO(const char *pathname)
{
//判断管道文件是否存在
int ret=access(pathname,F_OK);
if(ret == -1)//文件不存在
{
//创建管道文件
ret=mkfifo(pathname,0666);//可读可写
if(ret == -1)
{
perror("fifo failed\n");
return -1;
}
else
return 0;
}
return 0;
}
int main()
{
//建立有名管道文件
int ret=create_FIFO(FIFO);
if(ret == -1)//操作失败 结束运行
return -1;
//打开文件
int fd=open(FIFO,O_RDWR);
if(fd == -1)//打开文件失败
{
perror("open failed\n");
return -1;
}
//准备缓冲区
char buf[50];
//进程recv 负责接受
while(1)
{
//清空缓冲区
memset(buf,0,sizeof(buf));
//读取数据
read(fd,buf,sizeof(buf));
printf("接受的内容为:%s\n",buf);
//退出
if(!strcmp(buf,"bye"))
break;//退出循环
}
//关闭文件
close(fd);
//删除文件
unlink(FIFO);
return 0;
}
有名管道特点
- 有名管道可以用于任意两个进程间通信
- 有名管道属于一种文件类型,可以通过系统IO获取文件描述符
- 有名管道拥有文件权限,同时可以使用read/write函数读写数据
- 有名管道虽然是文件类型数据,但是不能使用lseek定位数据
- 有名管道具有写入原子性,支持多个进程同时写入数据,且数据不会错乱
- 有名管道属于先进先出的状态形式,最先写入的数据最先读取
管道检测原子性
管道检测原子性:
指一个操作要么完全执行,要么完全不执行,不会处于中间状态
。在多线程或多进程环境中,原子操作可以防止多个进程同时对共享资源进行读写,从而避免数据竞争和不一致性。
NAME 管理文件描述符对象
fcntl - manipulate file descriptor
SYNOPSIS
#include <unistd.h>
#include <fcntl.h>
函数原型:
int fcntl(int fd, int cmd, ... /* arg */ );
函数参数:
int fd, 需要管理的文件描述符对象
int cmd, 对文件描述符属性进行管理宏定义
F_GETFD (void) 读取文件描述符的属性
F_SETFD (int) 设置文件描述符的属性
F_GETFL (void) 获取存在的文件属性
F_SETFL (int) 设置文件的操作属性
... /* arg */命令字所需要的参数值【读取无需参数,设置需要参数】
文件属性:O_RDONLY, O_WRONLY, O_RDWR
O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC
O_APPEND 追加属性, O_ASYNC 信号异步,
O_DIRECT 目录属性, O_NOATIME 当前时间,
O_NONBLOCK 非阻塞属性
无名管道原子性
#include "myhead.h"
int main()
{
//创建无名管道
int fd[2];//fd[0]读端 fd[1]写端
int ret=pipe(fd);//创建无名管道
if(ret == -1)
{
perror("pipe failed\n");
return -1;
}
//无名管道用于亲缘间通信
pid_t x=fork();
if(x > 0)//父进程
{
printf("父进程 pid=%d\n",getpid());
close(fd[0]);//关闭读端
//保证写入的持续性,将写端描述符设置为非阻塞
int status=fcntl(fd[1],F_GETFL);//获取属性
status |= O_NONBLOCK;//非阻塞属性
fcntl(fd[1],F_SETFL,status);//设置属性
//持续写入
int num=0;
while(1)
{
int n=write(fd[1],"A",1);//写入数据
if(n == -1)
break;
else//统计成功写入的次数
printf("num=%d\n",num++);
}
wait(NULL);
}
if(x == 0)//子进程
{
printf("子进程 pid=%d\n",getpid());
close(fd[1]);//关闭写端
exit(0);
}
return 0;
}
有名管道原子性
#include "myhead.h"
#define FIFO "/home/softeem/fifo"
//建立有名管道文件
int create_FIFO(const char *pathname)
{
//判断管道文件是否存在
int ret=access(pathname,F_OK);
if(ret == -1)//文件不存在
{
//创建管道文件
ret=mkfifo(pathname,0666);//可读可写
if(ret == -1)
{
perror("fifo failed\n");
return -1;
}
else
return 0;
}
return 0;
}
int main()
{
//建立有名管道文件
int ret=create_FIFO(FIFO);
if(ret == -1)//操作失败 结束运行
return -1;
//打开文件
int fd=open(FIFO,O_RDWR);
if(fd == -1)//打开文件失败
{
perror("open failed\n");
return -1;
}
//设置文件描述符为非阻塞
int status=fcntl(fd,F_GETFL);//获取属性
status |= O_NONBLOCK;//非阻塞属性
fcntl(fd,F_SETFL,status);//设置属性
//持续写入
int num=0;
while(1)
{
int n=write(fd,"A",1);//写入数据
if(n == -1)
break;
else//统计成功写入的次数
printf("num=%d\n",num++);
}
//关闭文件
close(fd);
return 0;
}
IPC通信方式
IPC通信概念
共享内存、消息队列以及信号量统称为IPC通信方式,又称为systemV(V罗马数字5),在系统中他们使用键值作为唯一标识符,而且这些资源都是持续性资源,即创建之后不会因为进程运行结束而消失,除非调用特殊的函数或者命令执行删除,比如kill -9
systemV成为IPC通信在linux环境中存在三个形式,分别是共享内存、消息队列和信号量,用于同一个主机间的两个进程的消息传递或者消息同步,可以通过ipcs命令查看系统所有的ipc对象,可以通过命令获取ipc对象的键值和对象id
key键值获取
第一种:可以任意提供整数值作为键值,缺点在于IPC一旦创建则键值生效
第二种:通过系统函数ftok获取系统键值,由系统生成唯一键值对象
NAME 通过项目路径和项目编号生成IPC对象的系统键值key
ftok - convert a pathname and a project identifier to a System V IPC key
头文件
#include <sys/types.h>
#include <sys/ipc.h>
函数原型:
key_t ftok(const char *pathname, int proj_id);
函数参数:
const char *pathname, 提供存在的项目路径
int proj_id 提供任意的正整数
返回值:
成功 返回一个系统未使用的键值key
失败 返回-1
案例
#include "myhead.h"
int main()
{
//生成系统键值
key_t key=ftok("./",10);
printf("key:%d\n",key);
return 0;
}
注意点:
1.两个进程间的ftok函数的参数必须相同,那么才能产生同一个键值
2.ftok函数的第一参数一般使用项目所在路径,可以选择当前路径作为参数
3.同一个目录中多个进程使用不同的ipc对象,若需要区分key键值,则通过第二参数正整数改变key的内容
消息队列
消息队列是提供一种携带数据标识的特殊管道
消息队列查询方式:ipcs -q
消息队列删除方式:ipcrm -q
消息队列ID: ipcrm -Q key键值
消息队列特点
- 消息从消息队列读取之后,队列中的数据消息消失,将从队列中删除处理
- 消息队列当中,若存在未被读取的数据,则会残留消息队列中积累
- 在读取消息的时候,需要通过表示选择消息数据包接受
- 在消息队列中,多个消息表示相同的情况下,则遵循先进先出的原则
struct msgbuf
{
long mtype; 消息类型 /* message type, must be > 0 */
char mtext[1]; 消息内容 /* message data */
}; 注意该结构体需要自行定义,消息内容大小由自身需求决定
消息队列API
消息队列是一种以msg开头的系列函数
NAME 获取systemV中消息队列的id
msgget - get a System V message queue identifier
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:
int msgget(key_t key, int msgflg);
函数参数:
key_t key, 需要提供系统键值,使用ftok函数获取键值
int msgflg 消息队列的权限
IPC_CREAT 创建权限,若IPC不存在则创建对象
IPC_EXCL 判断IPC对象若存在,则退出创建操作
此时IPC对象的错误码为errno==EEXIST
mode 提供消息队列的操作权限,八进制权限(例如0666)
返回值: 成功 返回消息队列ID 失败 -1
消息队列发送与接受
NAME 消息队列的发送和接受
msgrcv, msgsnd - System V message queue operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); 消息发送
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);消息接受
函数参数:
int msqid, 消息队列ID msgget函数返回值
void *msgp, 存放发送或者接受消息的缓冲区(结构体)
struct msgbuf
{
long mtype; 消息类型
char mtext[1]; 消息内容
}; 注意该结构体需要自行定义,消息内容大小由自身需求决定
size_t msgsz, 发送或者接受的大小
【发送计算文本大小,接受获取整个大小】
long msgtyp,接受函数中选择接受标识
int msgflg 默认设置为0
返回值:
成功 发送函数为0 接受函数获取接受字节数
失败 返回为-1
消息队列属性
NAME 消息队列属性管理
msgctl - System V message control operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
函数原型:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
函数参数:
int msqid, 消息队列的id
int cmd, 消息队列的控制命令字
IPC_STAT 获取消息的属性,内存保存在 struct msqid_ds
IPC_SET 设置消息的属性,通过属性结构体struct ipc_perm
IPC_RMID 删除消息队列,第三参数设置为NULL
struct msqid_ds *buf
struct msqid_ds {
struct ipc_perm msg_perm; 属性结构体
time_t msg_stime;上次发送消息的时间
time_t msg_rtime;上次接受消息的时间
time_t msg_ctime; 上次修改数据时间
unsigned long __msg_cbytes;选中消息长度
msgqnum_t msg_qnum; 消息的个数
msglen_t msg_qbytes; 获取消息最大尺寸
pid_t msg_lspid; 发送消息进程号
pid_t msg_lrpid; 接受消息的进程号
};
struct ipc_perm {
key_t __key;消息队列键值
uid_t uid;用户对象id
gid_t gid;同组用户id
uid_t cuid;创建者id
gid_t cgid;创建组id
unsigned short mode;消息队列权限
unsigned short __seq; 序列号
};
消息队列通信步骤
操作步骤:
- 申请消息队列所需系统键值key ftok()
- 通过系统键值key获取消息队列ID msgget()
- 通过结构体配置消息标识和内容,选择发送或者接收==msgsend()/msgrcv()
- 获取消息队列的属性或者删除消息队列 msgctl() IPC_STAT IPC_RMID
#include "myhead.h"
//定义携带标识的结构体
struct msgbuf
{
long mtype;//消息标识
char mtext[100];//消息内容
};
int main()
{
//1.获取系统键值
key_t key=ftok(".",10);
if(key == -1)
{
perror("key failed\n");
return -1;
}
//2.获取消息队列ID
int msqid=msgget(key,IPC_CREAT|IPC_EXCL|0666);
//若获取消息队列ID时,消息队列key已存在则直接获取
if(msqid == -1&&errno == EEXIST)
{
msqid=msgget(key,0666);//直接获取存在消息队列ID
}
else if(msqid == -1)//创建失败
{
perror("msgget failed\n");
return -1;
}
//3.发送消息
struct msgbuf buf;//实例化结构体
buf.mtype=666;//消息标识
printf("请输入消息内容\n");
scanf("%s",buf.mtext);//消息内容
msgsnd(msqid,&buf,strlen(buf.mtext),0);
//4.获取消息队列属性
struct msqid_ds ds;//属性结构体
msgctl(msqid,IPC_STAT,&ds);//获取消息队列属性
printf("消息的尺寸:%ld\n",ds.__msg_cbytes);
printf("消息发送进程号:%d\n",ds.msg_lspid);
printf("消息队列键值:%d\n",ds.msg_perm.__key);
return 0;
}
发送
#include "myhead.h"
//定义携带标识的结构体
struct msgbuf
{
long mtype;//消息标识
char mtext[100];//消息内容
};
int main()
{
//1.获取系统键值
key_t key=ftok(".",10);
if(key == -1)
{
perror("key failed\n");
return -1;
}
//2.获取消息队列ID
int msqid=msgget(key,IPC_CREAT|IPC_EXCL|0666);
//若获取消息队列ID时,消息队列key已存在则直接获取
if(msqid == -1&&errno == EEXIST)
{
msqid=msgget(key,0666);//直接获取存在消息队列ID
}
else if(msqid == -1)//创建失败
{
perror("msgget failed\n");
return -1;
}
//3.接受消息
struct msgbuf buf;//实例化结构体
buf.mtype=666;//消息标识
ssize_t n=msgrcv(msqid,&buf,sizeof(buf),buf.mtype,0);//接受
printf("接受%ld字节 内容为%s\n",n,buf.mtext);
//4.获取消息队列属性
struct msqid_ds ds;//属性结构体
msgctl(msqid,IPC_STAT,&ds);//获取消息队列属性
printf("消息的尺寸:%ld\n",ds.__msg_cbytes);
printf("消息发送进程号:%d\n",ds.msg_lspid);
printf("消息队列键值:%d\n",ds.msg_perm.__key);
return 0;
}
IPC之共享内存
通信过程中只需要获取共享内存操作首地址,然后通过首地址进行数据的写入和读取即可。
共享内存查询
查看IPC共享内存方式:ipcs -m
删除IPC共享内存方式:ipcrm -m
共享内存ID ipcrm -M 键值key
- 共享内存只有等待的进程链接数为0的时候,才能真正删除共享内存,RMID只是为了删除提前做准备
- 共享内存只能通过可读可写的方式进行映射,那么才能实时读写数据
- 共享内存映射过程中,若不设置共享内存操作地址,由系统进行闲时分配共享内存操作地址,此时代码更具备可移植性
- 共享内存中的数据读取之后不会刷新,只有重写覆盖才能更新数据内容,也就是读取过程中若不限制,则会导致反复读取
共享内存API
共享内存是一种以shm开头的系列函数
NAME 共享内存的id获取
shmget - allocates a System V shared memory segment
SYNOPSIS
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型
int shmget(key_t key, size_t size, int shmflg);
函数参数:
key_t key, 共享内存的需求键值 通过ftok函数获取
size_t size, 需要申请的共享内存的大小(单位字节)
int shmflg 共享内存的标识权限
IPC_CREAT 创建权限,若IPC不存在则创建对象
IPC_EXCL 判断IPC对象若存在,则退出创建操作
此时IPC对象的错误码为errno==EEXIST
mode 提供消息队列的操作权限,八进制权限(例如0666)
返回值 成功 返回共享内存的id号 失败 -1
共享内存的映射和取消
NAME 共享内存映射和取消
shmat, shmdt - System V shared memory operations
SYNOPSIS
#include <sys/types.h>
#include <sys/shm.h>
函数原型
void *shmat(int shmid, const void *shmaddr, int shmflg);
函数参数:
int shmid, 共享内存的id号
const void *shmaddr, 共享内存的映射首地址,可设置为NULL
int shmflg 区域的权限设置一般写为0,作为可读可写
SHM_RDONLY 只读属性
int shmdt(const void *shmaddr);取消映射
函数参数:
const void *shmaddr 需要取消的首地址 shmat函数返回值
共享内存属性
NAME 共享内存的属性
shmctl - System V shared memory control
SYNOPSIS
#include <sys/ipc.h>
#include <sys/shm.h>
函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
函数参数:
int shmid, 共享内存的ID号
int cmd, 共享内存的命令字
IPC_STAT 获取内存的属性,内存保存在 struct shmid_ds
IPC_SET 设置内存的属性,通过属性结构体struct ipc_perm
IPC_RMID 删除共享内存,第三参数设置为NULL
struct shmid_ds *buf 属性结构体
struct shmid_ds {
struct ipc_perm shm_perm; 共享内存的属性
size_t shm_segsz; 共享内存大小
time_t shm_atime; 共享内存映射时间
time_t shm_dtime; 共享内存释放时间
time_t shm_ctime; 共享内存修改时间
pid_t shm_cpid; 创建共享内存的进程号
pid_t shm_lpid; 最后映射共享内存进程号
shmatt_t shm_nattch; 共享内存的链接数
...
};
返回值: 成功 0 失败 -1
共享内存通信步骤
操作方式
- 申请共享内存所需的键值号key ftok()
- 根据key键申请共享内存的id号 shmget()
- 通过共享内存ID号进行共享内存的映射 shmat()
- 通过共享内存的首地址shmaddr 进行数据的读写操作 scanf()/printf()
- 共享内存不使用时,接触共享内存的映射 shmdt()
- 共享内存进行删除或者获取属性操作 shmctl
#include "myhead.h"
int main()
{
//1. 申请共享内存所需的键值号key ftok()
key_t key=ftok(".",20);
if(key == -1)
{
perror("ftok failed\n");
return -1;
}
//2. 根据key键值申请共享内存的id号 shmget()
int shmid=shmget(key,1024,IPC_CREAT|IPC_EXCL|0600);
//共享内存key资源已存在,则直接获取共享内存id
if(shmid == -1&&errno == EEXIST)
{
shmid=shmget(key,1024,0600);//获取共享内存id
}
else if(shmid == -1)//申请失败
{
perror("shmid failed\n");
return -1;
}
//3. 通过共享内存ID号进行共享内存的映射 shmat()
char *p=(char *)shmat(shmid,NULL,0);
if(p == NULL)
{
printf("shmat failed\n");
return -1;
}
//4. 通过共享内存的首地址shmaddr进行数据的读写操作 scanf()/printf()
printf("请输入数据\n");
scanf("%s",p);
//5. 共享内存不使用时,解除共享内存的映射 shmdt()
shmdt(p);
//6. 共享内存进行删除或者获取属性操作 shmctl()
struct shmid_ds ds;
shmctl(shmid,IPC_STAT,&ds);//获取属性
printf("共享内存大小%ld\n",ds.shm_segsz);
printf("共享内存创建进程号%d\n",ds.shm_cpid);
printf("共享内存连接数%ld\n",ds.shm_nattch);
return 0;
}
#include "myhead.h"
int main()
{
//1. 申请共享内存所需的键值号key ftok()
key_t key=ftok(".",20);
if(key == -1)
{
perror("ftok failed\n");
return -1;
}
//2. 根据key键值申请共享内存的id号 shmget()
int shmid=shmget(key,1024,IPC_CREAT|IPC_EXCL|0600);
//共享内存key资源已存在,则直接获取共享内存id
if(shmid == -1&&errno == EEXIST)
{
shmid=shmget(key,1024,0600);//获取共享内存id
}
else if(shmid == -1)//申请失败
{
perror("shmid failed\n");
return -1;
}
//3. 通过共享内存ID号进行共享内存的映射 shmat()
char *p=(char *)shmat(shmid,NULL,0);
if(p == NULL)
{
printf("shmat failed\n");
return -1;
}
//4. 通过共享内存的首地址shmaddr进行数据的读写操作 scanf()/printf()
printf("共享内存数据:%s\n",p);
//5. 共享内存不使用时,解除共享内存的映射 shmdt()
shmdt(p);
//6. 共享内存进行删除或者获取属性操作 shmctl()
struct shmid_ds ds;
shmctl(shmid,IPC_STAT,&ds);//获取属性
printf("共享内存大小%ld\n",ds.shm_segsz);
printf("共享内存创建进程号%d\n",ds.shm_cpid);
printf("共享内存连接数%ld\n",ds.shm_nattch);
return 0;
}
IPC通信之信号量
信号量概念
不是用于传输数据,属于多进程之间协调数据的一种措施,用于协调不同进程之间的共享数据资源,用于处理多进程之间作为互斥机制存在,一般用于共享资源的互斥保护,保证资源的申请与释放完成一读一写操作,通过整数计数值进行调整
当多个进程同时访问同一份资源的时候,该资源称为共享资源,也称临界资源,访问这类资源的代码称为临界代码,这些代码的操作区域称为临界区,程序进入临界区的时候,必须要对资源进行申请 ,这个动作称为
p操作
,程序离开临界区的时候,必须对资源进行释放,这个动作称为v操作
信号量查询
- 信号量查询命令方式: ipcs -s
- 信号量删除命令方式: ipcrm -s 信号量id / ipcrm -S 键值key
信号量是具有原子性的操作变量,是由电气特性提供的计数值选择
信号量是一个非负数值,信号量表示一个资源的数量
若某个信号量的值变为负数值,则表示为阻塞等待,等待资源增加
p操作 此时对信号量使用减法操作,当信号量的值<0的时候进程阻塞
v操作此时对信号量使用加法操作,信号量的值>=0的时候进程畅通
信号量API
信号量ID号
NAME 信号量获取ID号
semget - get a System V semaphore set identifier
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semget(key_t key, int nsems, int semflg);
函数参数:
key_t key, 信号量的需求键值key ftok函数获取
int nsems, 信号量的元素个数
int semflg 信号量的标识权限
IPC_CREAT 创建权限,若IPC不存在则创建对象
IPC_EXCL 判断IPC对象若存在,则退出创建操作
此时IPC对象的错误码为errno==EEXIST
mode 提供信号量的操作权限,八进制权限(例如0666)
返回值 成功 返回信号量的id号 失败 -1
信号量属性设置
NAME 信号量的属性设置
semctl - System V semaphore control operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semctl(int semid, int semnum, int cmd, ...);
函数参数:
int semid, 信号量的id
int semnum, 信号量的对象,第几个信号量对象
int cmd, 信号量相关的命令字
IPC_STAT 获取信号量的属性 struct semid_ds;
IPC_SET 设置信号量的权限 struct ipc_perm;
IPC_RMID 删除信号量,命令参数设为NULL
GETALL 获取所有的信号量
GETVAL 获取信号量对应的元素值
SETALL 设置所有的信号量
SETVAL 设置信号量对应的元素值
... 根据命令字,选择配置结构体或者共用体
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*/
};
例如:
semctl(semid,0,SETVAL,semun.val);//设置信号量元素初始值
semctl(semid,0,GETVAL);//获取信号量元素初值
semctl(semid,0,IPC_STAT,semun.buf);//获取信号量的属性
信号量pv操作
NAME 信号量的设置
semop, semtimedop - System V semaphore operations
SYNOPSIS
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型:
int semop(int semid, struct sembuf *sops, size_t nsops);
函数参数:
int semid, 信号量的ID号
struct sembuf *sops, 信号量动作结构体
struct sembuf{
unsigned short sem_num; 信号量元素 从0开始
short sem_op; 信号量增减 p操作-1 v操作+1
short sem_flg; 操作选项 SEM_UNDO还原信号量
};
size_t nsops 信号量的设置个数
信号量步骤
操作步骤
- 获取信号量所需要的系统键值key ftok
- 根据系统键值key获取信号量id semget
- 设置信号量的元素初始值 semctl(SETVAL)
- 信号量资源进行数据的pv操作 semop
- 信号量资源的删除操作 semctl(IPC_RMID)
#include "myhead.h"
int main()
{
//1. 获取信号量所需要的系统键值key ftok()
key_t key=ftok(".",10);
if(key == -1)
{
perror("ktok failed\n");
return -1;
}
//2. 根据系统键值key获取信号量ID semget()
int semid=semget(key,1,IPC_CREAT|IPC_EXCL|0666);
//若信号量对应key存在,则直接打开信号量对象
if(semid == -1 && errno == EEXIST)
{
semid=semget(key,1,0666);//直接打开信号量获取id
}
else if(semid == -1)
{
perror("semget failed\n");
return -1;
}
//3. 设置信号量的元素初始值 semctl(SETVAL)
printf("信号量元素0的值%d\n",semctl(semid,0,GETVAL));
int ret=semctl(semid,0,SETVAL,5);//设置元素0的初值为5
if(ret == -1)
{
perror("SETVAL failed\n");
return -1;
}
printf("信号量初始值%d\n",semctl(semid,0,GETVAL));
//4. 信号量资源进行数据的pv操作 semop()
struct sembuf sop;//结构体初始化
sop.sem_num=0; //信号量元素 从0开始
sop.sem_op=-1; //信号量增减 p操作-1 v操作+1
sop.sem_flg=SEM_UNDO; //操作选项 SEM_UNDO还原信号量
for(int i=0;i<6;i++)//执行p操作
{
semop(semid,&sop,1);//p操作-1
printf("信号量元素0的值%d\n",semctl(semid,0,GETVAL));
}
//5. 信号量资源的删除操作 semctl(IPC_RMID)
return 0;
}
通信方式 | 优点 | 缺点 |
---|---|---|
共享内存 | 通信效率高,直接读取共享内存不需要额外的拷贝,像管道跟消息队列都需要内核跟用户交互4次,共享内存只需要2次,共享内存的数据只有发生覆盖重写内容才回发生变化 | 没有同步机制,多进程间通信的时候需要借助信号量,进行多进程之间的临界资源协调,当共享内存进行实时读取和写入的时候,通过信号量的阻塞和释放协调多进程之间的访问,防止强度数据 |
消息队列 | 适用于选择性通信,根据标识不同选择不同的消息接收,适用于任务分配,消息队列属于异步通信机制,只要消息发出,无须例会是否存在进程进行消息接收,只要有进程读取数据,则该队列该消息会被删除清空 | 可用性比较低,读取后就会删除处理,保证不了消息的重复性,不适用于大大量数据交互,容易丢失数据 |
信号量 signal
所有信号的操作都有共性
- 简单
- 不能携带大量的信息
- 满足某个特定条件才能发送在linux中信号是一种特殊的管道通信,但是在linux进程通信中属于唯一的异步(只管发不管收)通信方式,除了几个特殊的信号以外,其他信号都是无法预料的,当进程接收到信号之后,信号处理方式与cpu收到中断请求处理方式一致,进程负责
捕获信号、发送信号以及信号处理函数
信号的处理过程
递达:递送信号到进程对象中,能够捕获信号并执行处理函数
未决:产生和递达之间的状态,主要因为屏蔽或者阻塞导致接收信号暂停
linux内核的进程控制块PCB是一个机构体,在该结构体中包含信号相关的处理方式,主要是阻塞信号集合未决信号集
阻塞信号集(信号屏蔽集),将某些信号加入到集合当中,然后对其进行屏蔽操作,在屏蔽之后进程接受信号的时候,该信号会被推后处理(解除屏蔽);
未决信号集,信号产生之后,未决信号集中描述该信号的位置被置为1,表示该信号处于未决状态,当信号处理完毕之后,该信号位立即反转为0;信号产生之后由于某些原因导致无法递达,这类信号属于未决信号集。
发生信号的来源主要分为硬件来源和软件来源,通过linux内核发送不同的信号,然后判断该信号是否发生阻塞,然后判断信号的响应模式,最后完成响应的处理动作。
信号的种类
可靠实时信号 与不可靠非实时信号
区别:
早期的系统信号都是比较简单和原始,信号值从1~31进行描述,这部分信号都是非实时不可靠信号,主要问题就是不支持排队等待和信号存在丢失。
非实时信号(不可靠信号):
- 非实时信号不支持排队,信号的响应会相互嵌套
- 若目标进程中没有及时响应信号,那么随后到达的信号会导致该信号丢失
- 每个非实时信号都对应一个系统事件,事件发生就会触发该信号
- 进程中存在实时和非实时信号的时候,那么进程优先执行实时信号
实时信号(可靠信号):
- 实时信号的响应次序都是按照排队顺序出现,不会相互嵌套
- 目标进程中实时信号会被同时多次发生,且不会丢失并挨个响应
- 实时信号都是没有对应系统事件
- 实时信号的响应都是从大到小依次响应
信号的生命周期:信号产生、信号注册、信号响应和处理、信号销毁
查看信号值
命令:kill -l 或者trap -l
发送信号的格式:
进程号响应模式: kill -信号值 进程号
进程名响应模式:killall -信号值 进程名
信号的响应动作
信号的响应三种方式:忽略动作、默认动作和捕获动作
- 忽略动作:忽略信号里面大部分都可以执行,但是暂停信号SIGSTOP和杀戮信号SIGKILL不能忽略,不可捕获,不会阻塞。
- 默认动作:执行默认系统动作(linux系统下都有规定1-31信号的默认动作),根据系统的规定动作执行任务。
- 捕获动作:当捕获到信号之后,需要自定义完成信号处理函数执行任务
信号执行顺序:
- 若信号被阻塞,那么该信号将会挂起不做任何处理,直到解除阻塞之后,信号才会响应进程任务
- 若信号被捕获,那么进一步判断信号的响应方式
若为忽略动作,那么直接丢弃该信号,不做任何响应
若为默认动作,那么执行系统的默认操作事件
若为捕获动作,那么根据自定义动作执行响应操作
信号捕获与发送
NAME 信号响应处理函数
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>
函数指针 将函数指针取别名 sighandler_t==>void (*)(int)
typedef void (*sighandler_t)(int);
函数原型:
sighandler_t signal(int signum, sighandler_t handler);
函数参数:
int signum, 捕获的信号值
sighandler_t handler 信号的响应处理动作
SIG_IGN 忽略信号动作
SIG_DFL 默认信号动作
void 函数名(int) 自定义信号动作,函数中int表示为信号值
返回值:
成功 返回函数指针所指向函数对象地址
失败 返回SIG_ERR
NAME 发送一个信号给进程
kill - send signal to a process
SYNOPSIS
#include <sys/types.h>
#include <signal.h>
函数原型:
int kill(pid_t pid, int sig);
函数参数:
pid_t pid 进程号
int sig 信号值
返回值: 成功 0 失败 -1
NAME 等待一个信号值
pause - wait for signal
SYNOPSIS
#include <unistd.h>
函数原型:
int pause(void);
返回值:
成功 收到非致命信号或者信号已经被捕获
失败 返回 -1
收到致命信号导致进程退出 不返回
#include "myhead.h"
//自定义信号响应函数
void func(int sig)
{
printf("当前捕获的信号值%d\n",sig);
}
int main()
{
//获取进程的进程号
printf("当前进程号为%d\n",getpid());
//捕获信号值 2号信号
//signal(2,SIG_IGN);//忽略信号动作
//signal(2,SIG_DFL);//默认信号动作
signal(2,func);//捕获信号动作
//保证信号捕获 挂起
pause();
return 0;
}
信号的响应顺序:从实时信号响应到非实时信号,从大到小响应
#include "myhead.h"
//自定义信号响应函数
void func(int sig)
{
printf("当前捕获的信号值%d\n",sig);
}
int main()
{
//获取进程的进程号
printf("当前进程号为%d\n",getpid());
//循环捕获所有信号 除了SIGKILL和SIGSTOP以外
for(int i=1;i<=64;i++)
{
//跳过特殊信号
if(i==9 || i==19 || i==32 || i==33)
continue;
//能够捕获读取信号值
signal(i,func);
}
while(1)
{
//保证信号捕获 挂起
pause();
}
return 0;
}
#include "myhead.h"
int main(int argc,char *argv[])
{
//将所有的信号发送给某个进程,除了SIGKILL和SIGSTOP以外
for(int i=1;i<=64;i++)
{
//跳过特殊信号
if(i==9 || i==19 || i==32 || i==33)
continue;
//发送信号给某个进程
kill(atoi(argv[1]),i);
}
return 0;
}
特殊信号的发送方式
时钟信号的发送,定时一段时间,然后发送该信号
NAME 设置一个时钟信号进行发送该信号
alarm - set an alarm clock for delivery of a signal
SYNOPSIS
#include <unistd.h>
函数原型:
unsigned int alarm(unsigned int seconds);
函数参数:
unsigned int seconds 秒数 定时的时长
#include "myhead.h"
//自定义响应动作
void func(int sig)
{
printf("到点了,该上厕所了,信号值%d\n",sig);
}
int main()
{
//捕获定时器信号
signal(14,func);
//间隔5秒发送时钟信号
alarm(5);
//挂起进程
pause();
return 0;
}
给自身发送信号,自身的signal函数进行捕获响应
NAME 发送一个信号给自己
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>
函数原型
int raise(int sig);
函数参数: int sig 信号值
返回值: 成功 0 失败 非0
raise(9) 相当于 kill(getpid(),9);
#include "myhead.h"
//信号响应函数
void func(int sig)
{
printf("自身接受的信号为%d\n",sig);
}
int main()
{
//捕获 10号信号
signal(10,func);
//延时5秒发送信号
sleep(5);
raise(10);//发送10号信号
//延时5秒之后9号
printf("5秒钟后自杀\n");
sleep(5);
raise(9);//杀戮
//挂起进程
pause();
return 0;
}
携带数据传递信号
信号的发送
NAME 发送一个信号和数据给进程
sigqueue - queue a signal and data to a process
SYNOPSIS
#include <signal.h>
函数原型
int sigqueue(pid_t pid, int sig, const union sigval value);
函数参数:
pid_t pid, 发送对象的进程号
int sig, 需要发送信号值
const union sigval value 额外携带数据参数共用体
union sigval {联合体可以发送整型和指针
int sival_int;//携带一个整型数据
void *sival_ptr;//携带地址类型数据
};
返回值: 成功 0 失败 -1
信号的接收
NAME 捕获信号对象
sigaction - examine and change a signal action
SYNOPSIS
#include <signal.h>
函数原型:
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
函数参数:
int signum, 需要捕获的信号值数据 信号值
const struct sigaction *act,信号动作结构体设置
struct sigaction *oldact 原来的信号动作结构体
若不存在原来的信号动作设为NULL
struct sigaction {信号动作结构体
void (*sa_handler)(int);
和signal函数作用一致,作为普通的信号响应函数处理
void (*sa_sigaction)(int, siginfo_t *, void *);
当需要捕获信号以及携带参数时,通过siginfo_t类型进行获取
该类对象用于保存对方传递的数据值 void *补充数据
siginfo_t {
int si_int; /*POSIX保存信号携带整数 */
void *si_ptr; /* POSIX保存信号携带地址 */
};
sigset_t sa_mask; 设置阻塞的信号集对象
int sa_flags;选择标识,决定信号接收方式
标识设置为0,则表示选择sa_handler普通响应模式 信号捕获
标识设置为SA_SIGINFO,则表示选择sa_sigaction参数携带
void (*sa_restorer)(void);废弃的数据域
};
#include "myhead.h"
//自定义普通函数响应
void func(int sig)
{
printf("当前为普通函数响应,信号值%d\n",sig);
}
//携带参数的信号响应
void funs(int sig, siginfo_t *info, void *arg)
{
printf("当前捕获的信号值为%d\n",sig);
printf("当前除了捕获信号以外还获取到整数%d\n",info->si_int);
}
int main()
{
//获取进程的进程号
printf("当前进程号为%d\n",getpid());
//捕获10号信号 并且按照普通函数响应
struct sigaction act;//实例化信号动作结构体
//配置自定义结构体成员,选择普通响应函数和选择标识
act.sa_handler=func;//普通自定义信号处理函数
act.sa_flags=0;//标识设置为0 普通响应
//捕获10号信号值
sigaction(10,&act,NULL);
//捕获12号信号 并且按照携带参数信号响应
struct sigaction action;//实例化信号动作结构体
//配置自定义结构体成员,选择额外携带响应函数和选择标识
action.sa_sigaction=funs;//选择携带参数信号响应
action.sa_flags=SA_SIGINFO;//标识设置SA_SIGINFO携带获取
//捕获12号信号值
sigaction(12,&action,NULL);
//持续运行
while(1);
return 0;
}
#include "myhead.h"
int main(int argc,char *argv[])
{
//准备携带参数的数据值
union sigval value;
//设置共用的整数值100
value.sival_int=100;
//发送信号数据
sigqueue(atoi(argv[1]),atoi(argv[2]),value);
return 0;
}
信号集(阻塞/未决)
在信号中,阻塞就是将信号直接挂起,进程不受指定信号的干扰,信号不影响进程的执行方式,直到解除阻塞,在通过信号影响进程的运行,相当于接受到信号之后,由于阻塞关系暂时不执行,解除阻塞之后在执行
信号阻塞掩码集
信号集的阻塞掩码指的是linux定义"sigset_t"类型的变量,该类型的数据在linux当中存放所有的阻塞信号的集合,将多个信号集中到一个处理当中形成信号集,sigset_t类型变量其实为一个结构体描述,在这个结构体当中存放一个整型变量,这个整型变量存储linux当中的信号值
NAME 信号集处理函数
sigemptyset, sigfillset, sigaddset, sigdelset, sigismember - POSIX signal set operations
SYNOPSIS
#include <signal.h>
函数原型:信号集管理
//清空信号集对象set
int sigemptyset(sigset_t *set);
//将所有的信号加入到集合set
int sigfillset(sigset_t *set);
//将指定信号signum加入到集合set
int sigaddset(sigset_t *set, int signum);
//将指定信号signum从集合set中删除
int sigdelset(sigset_t *set, int signum);
函数参数:
sigset_t *set, 管理信号集合
int signum 指定信号管理
返回值:成功 返回0 失败 -1
//判断某个信号signum是否存在于集合set当中
int sigismember(const sigset_t *set, int signum);
函数参数:
const sigset_t *set, 管理信号集合
int signum 指定信号管理
返回值:
返回为1 表示信号存在于集合当中
返回为0 表示信号不存在于集合当中
返回为-1 表示判断失败
#include "myhead.h"
int main()
{
//定义信号集
sigset_t set;
//清空信号集
sigemptyset(&set);
//将所有的信号加入到集合
sigfillset(&set);
//判断指定信号是否存在于集合当中
for(int signum=1;signum<=64;signum++)
{
//判断信号是否存在
int ret=sigismember(&set,signum);
if(ret == 1)
printf("signum:%d存在于集合set\n",signum);
else if(ret == 0)
printf("signum:%d不存在于集合set\n",signum);
else
printf("判断失败\n");
}
//删除集合中的某些信号
for(int signum=1;signum<=64;signum++)
{
if(signum%3 == 0)
sigdelset(&set,signum);
}
//判断指定信号是否存在于集合当中
for(int signum=1;signum<=64;signum++)
{
//判断信号是否存在
int ret=sigismember(&set,signum);
if(ret == 1)
printf("signum:%d存在于集合set\n",signum);
else if(ret == 0)
printf("signum:%d不存在于集合set\n",signum);
else
printf("判断失败\n");
}
return 0;
}
阻塞信号集
设置信号集的阻塞和接触,完成一段时间内不受信号的控制影响,阻塞不同于忽略,阻塞是信号挂起,暂时不响应,解除阻塞之后,信号再次响应操作,忽略则是直接将信号丢弃
NAME 信号集的阻塞操作
sigprocmask - examine and change blocked signals
SYNOPSIS
#include <signal.h>
函数原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数参数:
int how, 设置信号集的方式
SIG_BLOCK 设置信号集的阻塞,在原有信号集之上
SIG_UNBLOCK 设置信号集解除阻塞
SIG_SETMASK 设置信号集的阻塞 使用old替换set
const sigset_t *set,需要设置阻塞的信号集
sigset_t *oldset 原来的信号集,若无需关注可写为NULL
返回值: 成功 0 失败 -1
#include "myhead.h"
void func(int sig)
{
printf("捕获的信号值为%d\n",sig);
}
int main()
{
printf("当前的进程号为%d\n",getpid());
//定义信号集
sigset_t set;
//操作信号集
sigemptyset(&set);//清空信号
sigfillset(&set);//添加所有信号
//捕获3号信号
signal(3,func);//捕获
signal(64,func);//捕获
//添加信号集阻塞
printf("添加阻塞\n");
sigprocmask(SIG_BLOCK,&set,NULL);
//延时20秒
sleep(20);
//添加信号集解除
printf("解除阻塞\n");
sigprocmask(SIG_UNBLOCK,&set,NULL);
//等待信号捕获
pause();
return 0;
}
信号集未决
NAME 读取当前进程的未决信号集
sigpending - examine pending signals
SYNOPSIS
#include <signal.h>
函数原型:
int sigpending(sigset_t *set);
函数参数:sigset_t *set 获取需要读取的对象
返回值:成功 返回0 失败 返回-1
#include "myhead.h"
//检测未决信号
void print(sigset_t *set)
{
//循环检测每个信号
for(int i=1;i<=64;i++)
{
//判断信号是否存在集合中
int ret=sigismember(set,i);
if(ret == 1)
{
putchar('1');
}
else
{
putchar('0');
}
}
printf("\n");
}
int main()
{
//定义信号集
sigset_t set,pedset;
//操作信号集
sigemptyset(&set);//清空信号
//添加指定信号
sigaddset(&set,2);
sigaddset(&set,3);
//添加信号集阻塞
printf("添加阻塞\n");
sigprocmask(SIG_BLOCK,&set,NULL);
//检测未决
while(1)
{
//未决检测
int ret=sigpending(&pedset);
if(ret == -1)
{
printf("检测失败\n");
break;
}
//获取未决数据
print(&pedset);
sleep(1);
}
return 0;
}