一、进程和线程通讯方式区别
(1)线程通信:线程共用一个地址空间,所以线程之间共享数据很容易。
(2)进程间通信: 进程之间不是同一个给地址空间,怎么通讯
传统的通信方式:
无名管道 pipe
有名管道 fifo
信号 signal
system V IPC
共享内存
消息队列
信号灯集
其他
套接字
二、 通信方式分类
1进程间数据共享
管道、共享内存、消息队列、套接字
2异步通信
信号
3同步互斥(资源保护的)
信号灯集
三、方式详解
(1)无名管道
① 什么是管道: 一个管道实际上就是存在于内存中的文件。对于这个文件操作通过两个打开文件进行,分别代表管道的两端。管道是一种特殊的文件,不属于某个文件系统
② 数据的读写:一个进程向管道种写数据被管道另一端读取,写入内容每次都添加在管道缓冲区的尾部,读取时候是从缓冲区的头部读取数据。
③无名管道的特性:a.在文件系统种没有无名管道对应的文件名,存放在了内存中;b.只能用于亲缘关系的进程;c.半双工(一端读,另一端写)不同时进行;d.可以使用read、write函数,但是不能使用lseek;e.既然可以使用文件io接口,那么肯定产生文件描述符
④详细的创建流程:a.父进程创建一个无名管道pipe,fd[0]用于读管道,fd[1]用于写管道;b.父进程创建fork子进程,子进程继承了父进程的管道;c.再次之后取决于我们向要的数据流动的方向来关闭相应的读写端。
⑤管道特性:
a.管道的大小:PIPE_BUF
$ vi -t PIPE_BUF
b.从管道中读取数据(听)
(1)写端存在时候,当管道中无数据,读操作会阻塞
(2)写端存在时候,当读端请求读取的数据大于管道中的数据,此时读取管道中实际大小的数据。当读端请求的数据小于管道中的数据时候,此时读取请求数据大小。
(3)当读一个写端被关闭的管道时候,在所有的数据被读取之后,read返回0
c.从管道中写数据(说)
(1)读端存在时候,向管道中写入数据,管道缓冲区一有空闲区域,写进程就会试图先管道中写数据,如果多个进程同时写一个管道, 数据可能会于其他进程写操作发生数据交错,需要借助同步互斥机制
(2)如果管道缓冲区数据达到PIPE_BUF,进程如果不读走管道中数据, 那写操作将会一直阻塞。
(3)如果管道的读端已经被关闭,我去写数据, 则产生一个信号SIGPIPE(终止、退出),如果忽略这个信号,write出错返回,设置errno为EPIPE
⑥创建无名管道
#include <unistd.h>
int pipe(int pipefd[2]);
功能:如果成功产生两个文件描述符,分别存放在pipe[0]和pipe[1]
参数:pipefd[]是一个数组,pipefd[0]读端,pipefd[1]写端
返回值:成功0,失败-1
(2)有名管道
①解释: 一台机器上的任意两个进程之间进行通讯, 会出现文件名来代表有名管道(bcd-lsp中的p)
a.主要用于非亲缘关系进程之间的
b.半双工(一端读,另一端写不同时进行)
c.在文件系统可见的,open、read、write,但是不能使用lseek
d.当读端写端都关闭时候,内容也就释放了
②创建有名管道
命令:mkfifo 管道名
函数:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数:pathname 所创建的管道名和路径;mode:指定有名管道权限
例如mkfifo("a",0666);
返回值:成功0,失败-1,设置erron
注意:a.有名管道必须读端和写端都存在时候才会执行。
b.如果mkfifo的第一个参数时一个已经存在了的,就会返回EEXIST
c.所以调用mkfifo时候需要检查errno是不是等于EEXIST
③读端写端,有名管道open打开规则:O_RDONLY(只读)、 O_WRONLY(只写)、O_NONBLOCK(非阻塞)
flags=O_RDONLY:open将会调用阻塞,除非有另一个进程以写的方式打开了同一个FIFO,否则一直等
flags=O_WRONLY:open将会调用阻塞,除非有另一个进程以读的方式打开了同一个FIFO,否则一直等
flags=O_RDONLY|O_NONBLOCK:如果此时没有其他进程以写的方式打开FIFO,那open也会成功返回,此时FIFO被读打开,而不会返回错误
flags=O_WRONLY|O_NONBLOCK:立即返回,如果此时没有其他进程以读的方式打开,open会失败,此时FIFO没有被打开,返回-1
④有名管道读写规则:a.有名管道的读写规则和无名管道基本一致;b.主要参考无名管道读写规则即可;c.只有读端或只有写端的时候,open会阻塞;d.只有读写都存在open才返回
⑤删除文件
int unlink(char*pathname);
返回值成功0,失败-1
如果文件打开时候,使用unlink并不会立刻删除,并且对文件依然可以使用读写操作,在进程结束之后文件就会被删除。
(3)信号
①信号命令:信号是在软件层次上对中断机制的一种模拟,它是进程间唯一一个异步通讯方式。
信号由内核产生,递交给用户空间来处理,信号可以直接进行用户空间进程和内核进程之间交互,内核进程也可以利用他来通知用户空间进程发生了那些事件, 如果该进程当前并未处于执行态,则信号就由内核保存起来, 知道进程恢复执行在传递给他;ctrl+z将进程挂起,是不接受信号的。
kill -l 显示所有的信号
②信号分类
不可靠信号 信号值1-31
可靠信号 信号值34-64
ps: 所谓可靠和不靠全是按照信号值区分的,可靠信号不会丢失,不可靠信号可能丢失
③用户进程对信号对信号处理相应方式
1 忽略信号SIG_IGN: 对信号不做任何处理(但是有2个不能忽略的信号SIGKILL、SIGSTOP)
2 执行默认操作SIG_DFL:执行默认操作
3 捕捉信号:当信号发时,执行我们自己规定的事情(9号SIGKILL 不能被阻塞处理忽略,19号SIGSTOP不能被阻塞处理忽略)
kill -信号 pid
kill -9 1234 给进程1234发送9信号
kill 默认发送15号
/*
SIGKILL 杀死进程,不能忽略,不能捕获。
SIGSTOP Ctrl+Z让进程挂起,不能忽略,不能捕获。
SIGTERM kill命令默认信号,默认杀死进程
SIGINT Ctrl+C 默认杀死进程
SIGQUIT CTRL+\默认产生core调试文件,默认杀死进程
SIGPIPE 管道破裂,默认杀死进程
SIGALRM 定时器信号,默认杀死进程
SIGCHLD 默认忽略
*/
④信号相关函数(声明了,内核就一直盯着)
a.设置信号
#include <signal.h>
typedef void (*sighandler_t)(int) sighandler_t signal(int signum, sighandler_t handler);
参数:signum要监控的信号,handler函数指针变量:可以写SIG_IGN忽略信号,可以写SIG_DFL默认操作,可以写一个自定义的函数
返回值:成功返回函数指针,失败返回SIG_ERR
b.发送信号
#include<sys/types.h>
#include <signal.h>
int kill(pid_t pid,int sig)
功能:发送给进程信号
参数 pid 进程的id,sig发送的信号
返回值:成功返回0,失败-1
int raise(int sig)
功能自己给自己法信号
参数 sig发送的信号
返回值:成功返回0,失败-1
c.定时器函数
//alarm()给进程设定时间,一个进程只能设定一个时间
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
参数:seconds 设定的秒数,设定成0就是取消闹钟
返回值:当前剩余秒数,没有就返回0,如果已经设置了倒计时,刷新设置闹钟,如果设置为0秒,取消闹钟,返回上一个闹钟剩余的时间
(4)System V IPC
IPC inter-process communication 进程间通讯 ,每一个IPC对象有唯一的ID(系统随机分配的数字),只有创建IPC的进程才可以直接获取ID,在内核空间创建,创建之后一直存在,直到被函数或命令删除、关闭系统,才会被释放,每个IPC对象都关联一个KEY,可以看成一个属性,其他进程通过KEY值找到同一个IPC对象
a.命令:
ipcs 查看IPC对象的属性
key值:相当于IPC对象通讯的外部名字,key是一个整数
b.ftok函数
#include<sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char*pathname,int proj_id)
功能:获取key值,用于systemV通讯,ftok函数实际上是调用stat函数,pathname文件系统的信息(stat结构的st_dev成员)和该文件在本文件系统内索引节点(stat结构的st_ino成员),在和自己定义的proj_id的低八位运算,得出key值
参数:pathname 文件名,(要求文件必须存在,不存在返回-1),proj_id 整型值,(低八位不能为0,所以一般用一个字符)
返回值:成功返回key值,失败-1
每个IPC对象都维护着一个信息结构体
vi -t ipc_perm
struct ipc_ perm
{
//key值
kernel_ key_ t key ;
//属主ID,可以改变的
kerneL uid t uid ;//用户ID
kernel gid_ t gid; //组ID
//创建者ID
kernel uid t cuid;//用户ID
kernel_ gid_ t cgid;//组ID
//权限
kernel mode_ t mode ;
//序列号
unsigned short seq;
};
(5)共享内存
①特点: 效率高,共享内存是一种最为高效进程间通讯方式,进程可以直接读写内存,而不要任何的数据拷贝。共享内存在内核空间创建, 可以被进程映射到用户空间,使用灵活。多个进程可以同时访问共享内存,因此需要互斥配合
②共享内存使用步骤:a.创建/打开共享内存;b.映射共享内存;c.读写共享内存 ;d.撤销共享内存
;e.删除共享内存
a.创建/打开一个共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
//得到共享内存标识符或创建一个共享内存对象并返回共享内存标识符
参数:
key:由ftok创建的IPC值,0(IPC_PRIVATE)代表私有
size:大于0的整数:新建的共享内存大小,单位字节,0获取共享内存标识符
shmflg:IPC_CREAT|0666 如果内核中不存在键值与key相等的共享内存,那就新建一个,如果存在,就返回共享内存标识符。IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的共享内存,那就新建一个,
如果就报错
返回值:成功返回共享内存标识符失败-1,设置errno
b.ipcs
ipcs命令显示ipc对象
ipcs -m显示共享内存
ipcrm -m 1496532 删除id为1495632的共享内存
c.共享内存的映射shmat()
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
//连接共享内存标识符为shmid的共享内存,连接成功后把共享内存对象映射到调用它的进程的地址空间,随后就可像访问本地空间一样访问
参数:
shmid 要映射的共享内存的id
shmaddr 指定共享内存出现在进程内存地址的什么位置,一般指定为NULL,让内核自己决定一个合适地址
shmflg 0可读写,SHM_RDONLY只读
返回值:成功返回映射的共享内存地址,失败 (void*)-1
d.删除共享内存映射
int shmdt(const void *shmaddr);
//与shmat相反,用来断开与共享内存的映射,shmaddr映射的共享内存地址
返回值:成功0,失败-1,设置erron
e.共享内存控制
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid共享内存标识符
cmd:IPC_STAT:得到共享内存状态,放到struct shmid_ds *buf里;IPC_SET:改变共享内存的状态,把buf中shmid_ds结构中uid、gid、mode复制到共享内存的shmid_ds结构中;
IPC_RMID 删除这片共享内存
buf 共享内存管理结构体
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */
shmatt_t shm_nattch; /* No. of current attaches */
...
};
struct ipc_perm {
key_t __key; /* Key supplied to shmget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and SHM_LOCKED flags */
unsigned short __seq; /* Sequence number */
};
返回值:成功0,失败 -1,设置errno
//例如删除共享内存shmctl(shmid,IPC_RMID,NULL)
(6)消息队列
消息队列:是systemV IPC对象的一种,消息队列由消息队列ID来唯一标识,消息队列就是一个消息的列表, 用户可以在消息队列中添加消息读取消息,消息队列可以按照类型来发送接收
a.msgget创建或打开消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
得到消息队列标识符或创建一个消息队列并返回消息队列标识符
参数:
key 由ftok创建的IPC值,0(IPC_PRIVATE)代表私有
msgflg
IPC_CREAT|0666
如果内核中不存在键值与key相等的消息队列
那就新建一个,
如果存在,就返回消息队列标识符。
IPC_CREAT|IPC_EXCL
如果内核中不存在键值与key相等的消息队列
那就新建一个,
如果存在就报错
返回值:
成功返回消息队列标识符
失败-1,设置erron
b.msgsnd发送消息
将数据放到消息队列中,消息总是放到队列尾端,通讯双方首先需要先定义统一的消息格式,用户根据应用需求定义结构体,
首成员代表消息的类型,他是long类型,其他成员属于消息正文
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//将msgp消息写到标识符msqid的消息队列里。
参数:
msqid:消息队列的IPC标识符。通过msgget获取的
msgp:可以是任何类型的结构体,但是第一个字段必须是long类型,表示发送消息的类型,msgrcv 根据它去接收消息指向存放消息的缓冲区,msgp结构体
struct msgbuf {
long mtype; /* message type, must be > 0 */
必须大于0消息类型
char mtext[1]; /* message data */
消息正文,
};
msgsz 发送消息的大小,不含消息类型long占用字节,也就是mtext长度
msgflg 0阻塞方式发送,当队列满了,会阻塞等,直到能写进去数据,IPC_NOWAIT非阻塞方式发送消息,当消息队列满了,不等,立刻返回设置errno
返回值 成功0,失败-1,设置erron
c.msgrcv接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数:
msqid 消息队列的IPC标识符。通过msgget获取的
msgp 表示接收消息的类型
指向存放消息的缓冲区
msgp结构体
struct msgbuf {
long mtype; /* message type, must be > 0 */
必须大于0消息类型
char mtext[1]; /* message data */
消息正文,
};
msgsz 接收消息的大小,不含消息类型long占用字节,也就是mtext长度
msgtyp:0 接收第一个消息,>0接收值为msgtyp的第一个消息,<0接收类型等于或小于msgtyp绝对值的第一个消息
msgflg:0阻塞方式接收,当队列没有msgtyp就会阻塞等,直到等到有msgtyp数据在返回,IPC_NOWAIT非阻塞方式接收消息,失败 不等,立刻返回设置errno
返回值:成功返回读到消息数据长度,失败-1,设置erron
d.msgctl控制消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
获取和设置消息队列
参数:
msqid 消息队列的IPC标识符。通过msgget获取的
cmd
IPC_RMID删除一个消息队列
IPC_SET设置消息队列属性
uid gid mode qbtype
IPC_STAT获取消息队列属性
buf:
struct msqid_ds {
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in
queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages
in queue */
队列中当前消息个数
msglen_t msg_qbytes; /* Maximum number of bytes
allowed in queue */
队列允许最大字节数
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
struct ipc_perm {
key_t __key; /* Key supplied to msgget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
返回值成功0,失败-1设置errno
删除消息队列:msgctl(msgid,IPC_RMID,NULL);
(7)信号灯集
①信号灯集特点:
信号灯(semaphore),也叫信号量。它是不同进程间或
一个给定进程内部不同线程间同步的机制
System V的信号灯是一个或者多个信号灯的一个集合
(允许对集合中的多个计数信号灯进行同时操作)。
其中的每一个都是单独的计数信号灯。
而Posix信号灯指的是单个计数信号灯
②信号灯的种类:
posix有名信号灯
posix基于内存的信号灯(无名信号灯)(线程)
System V信号灯(IPC对象)(信号灯的值就是代表资源的数量)
③信号灯的创建步骤:
1. 产生key值
2. 创建/打开信号灯集
3. 初始化信号灯(对信号灯集中的每个信号灯进行初始化)
4. 信号灯的PV操作
5. 删除信号灯集
信号灯的相关函数:
创建key值函数: 《ftok》
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
功能:生成消息队列的key值
参数:@pathname 路径名
@proj_id 根据man手册得到,传入一个char类型即可
返回值: 成功: key_t值
失败: -1 更新 errno
int semget(key_t key, int nsems, int semflg);
功能:创建/打开信号灯集
参数:@key:ftok产生的key值(和信号灯关联的key值)
@nsems:信号灯集中包含的信号灯数目(0,1,2...)
@semflg:信号灯集的访问权限,
通常为IPC_CREAT|IPC_EXCL|0777
返回值:成功:信号灯集ID
失败:-1 更新 errno
int semop( int semid, struct sembuf *opsptr, size_t nops);
功能:对信号灯集合中的信号量进行PV操作
参数:@semid:信号灯集ID
struct sembuf {
short sem_num; // 要操作的信号灯的编号(0,1,2...)
short sem_op;
// 0 : 等待,直到信号灯的值变成0
// -1 : 分配资源,P操作
// +1 : 释放资源,V操作
short sem_flg;
// 0(阻塞),IPC_NOWAIT
};//对某一个信号灯的操作,如果同时对多个操作,则需要定义这种结构体数组
nops: 要操作的信号灯的个数 1个
返回值:成功:0
失败:-1 更新 errno
用法:
申请资源 P操作:
mysembuf.sem_num = 0;
mysembuf.sem_op = -1;
mysembuf.sem_flg = 0;
释放资源 V操作:
mysembuf.sem_num = 0;
mysembuf.sem_op = 1;
mysembuf.sem_flg = 0;
semop(semid, &mysembuf, 1);
int semctl(int semid, int semnum, int cmd,, union semun arg);
功能:信号灯集合的控制(初始化/删除)
参数:@semid: 信号灯集ID
@semnum: 要操作的集合中的信号灯编号
@cmd:
GETVAL:获取信号灯的值,返回值是获得值
SETVAL:设置信号灯的值,需要用到第四个参数:共用体
IPC_RMID:从系统中删除信号灯集合
@arg
union semun {
short val;
/*SETVAL用的值*/
struct semid_ds* buf;
/*IPC_STAT、IPC_SET用的 semid_ds结构*/
unsigned short* array;
/*SETALL、GETALL用的数组值*/
struct seminfo *buf;
/*为控制IPC_INFO提供的缓存*/
} arg;
返回值:成功: 0,失败: -1,更新 errno
用法:
初始化:semctl(semid, 0, SETVAL, mysemun);
需要在程序中定义共用体
获取信号灯值:semctl(semid, 0, GETVAL);
删除信号灯集:semctl(semid, 0, IPC_RMID);
(四)总结
pipe: 具有亲缘关系的进程间,单工,数据在内存中
fifo: 可用于任意进程间,双工,有文件名,数据在内存
signal: 唯一的异步通信方式
msg:按消息类型访问 ,可有优先级,如果msgtype的值为0,队列中第一个可用的消息就会被接收;如果其值大于0,具有相同消息类型的第一个消息就会被接收;如果其值小于0,第一个具有相同类型或是小于msgtype绝对值的消息就会被接收。
shm:效率最高(直接访问内存)
sem:配合共享内存使用,用以实现同步和互斥