进程间通信:进程和进程间交换数据
管道(数据传输)
共享内存(数据共享)
消息队列(数据传输)
信号量(进程控制)
管道:内核当中的一块内存,内核为进程间通信创建的缓冲区
匿名管道
- 创建匿名管道
#include<unistd.h>
int pipe(int fd[2]);
fd[2]:具有两个元素的整型数组,数组当中的每一个元素都是一个文件描述符
fd[2]是一个出参,内核返回给用户两个文件描述符
fd[0]:表示读端,操作fd[0]可以对匿名管道进行读;
fd[1]:表示写端,操作fd[1]可以对匿名管道进行写;
返回值: 成功返回0,
失败返回-1;
2.匿名管道的特性
①内核开辟的缓冲区并没有标识,所以只能用于具有亲缘关系的进程
②管道的数据流只能从写端------>读端
③管道提供字节流
假如读端没有及时读走数据,而写端写了多次,每次写的数据之间是没有明确的数据边界的;
读端在进行读操作时,可以一次性将数据都读走,也可以按照自己的方式读取任意大小的数据
④管道的大小:pipe_size==64K
当读端不读,写端一直写,当管道写满时,写端会阻塞
当写端不写,读端一直读,当管道为空时,读端会阻塞
⑤创建匿名管道返回的文件描述符默认是阻塞状态,当然可以设置文件描述符为非阻塞状态,只需要给原来的属性 按位或上O_NONBLOCK
头文件
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
函数类型
定义函数 int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
fcntl()针对(文件)描述符提供控制.参数fd 是被参数cmd操作(如下面的描述)的描述符.
针对cmd的值,fcntl能够接受第三个参数int arg
参数介绍编辑
参数fd
参数fd代表欲设置的文件描述符。
参数cmd
参数cmd代表打算操作的指令。
有以下几种情况:
F_DUPFD用来查找大于或等于参数arg的最小且仍未使用的文件描述符,并且复制参数fd的文件描述符。执行成功则返回新复制的文件描述符。新描述符与fd共享同一文件表项,但是新描述符有它自己的一套文件描述符标志,其中FD_CLOEXEC文件描述符标志被清除。请参考dup2()。
F_GETFD取得close-on-exec旗标。若此旗标的FD_CLOEXEC位为0,代表在调用exec()相关函数时文件将不会关闭。
F_SETFD 设置close-on-exec 旗标。该旗标以参数arg 的FD_CLOEXEC位决定。
F_GETFL 取得文件描述符状态旗标,此旗标为open()的参数flags。
F_SETFL 设置文件描述符状态旗标,参数arg为新旗标,但只允许O_APPEND、O_NONBLOCK和O_ASYNC位的改变,其他位的改变将不受影响。
F_GETLK 取得文件锁定的状态。
F_SETLK 设置文件锁定的状态。此时flcok 结构的l_type 值必须是F_RDLCK、F_WRLCK或F_UNLCK。如果无法建立锁定,则返回-1,错误代码为EACCES 或EAGAIN。
F_SETLKW F_SETLK 作用相同,但是无法建立锁定时,此调用会一直等到锁定动作成功为止。若在等待锁定的过程中被信号中断时,会立即返回-1,错误代码为EINTR。
参数lock指针
参数lock指针为flock 结构指针,定义如下
struct flock
{
short int l_type;
short int l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
};
l_type 有三种状态:
F_RDLCK 建立一个供读取用的锁定
F_WRLCK 建立一个供写入用的锁定
F_UNLCK 删除之前建立的锁定
l_whence 也有三种方式:
SEEK_SET 以文件开头为锁定的起始位置。
SEEK_CUR 以目前文件读写位置为锁定的起始位置
SEEK_END 以文件结尾为锁定的起始位置。
l_start 表示相对l_whence位置的偏移量,两者一起确定锁定区域的开始位置。
l_len表示锁定区域的长度,如果为0表示从起点(由l_whence和 l_start决定的开始位置)开始直到最大可能偏移量为止。即不管在后面增加多少数据都在锁的范围内。
返回值 成功返回依赖于cmd的值,若有错误则返回-1,错误原因存于errno.
功能介绍编辑
fcntl()用来操作文件描述符的一些特性。fcntl 不仅可以施加建议性锁,还可以施加强制锁。同时,fcntl还能对文件的某一记录进行上锁,也就是记录锁。
函数返回值编辑
fcntl的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列四个命令有特定返回值:F_DUPFD、F_GETFD、F_GETFL、F_GETOWN.第一个返回新的文件描述符,接下来的两个返回相应标志,最后一个返回一个正的进程ID或负的进程组ID。
⑥创建一个匿名管道,更改对应读写端文件描述符为非阻塞属性
A.不进行读,但一直写;只把写端文件描述符设为非阻塞属性
A.a读端不关闭,写端一直写,write会返回-1,报错当前资源不可用
A.b读端关闭,写端一直写,当前进程收到SIGPIPE信号,写端程序被杀死,管道破裂
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<fcntl.h>
4
5 int main()
6 {
7 int fd[2];
8 int ret = pipe(fd);
9 if(ret<0)
10 {
11 perror("pipe error!!!");
12 return 0;
13 }
14
15 int f_write=fcntl(fd[1],F_GETFL);
16 f_write|=O_NONBLOCK;
17 fcntl(fd[1],F_SETFL,f_write);
18 //close(fd[0]);//关闭读端
19 int count=0;
20 while(1)
21 { printf("aaaaa!");//只是证明执行过这一循环
22 int write_size=write(fd[1],"a",1);
23 if(write_size<0)
24 {
perror("write");
25 printf("write_size:%d\n",write_size);
26 break;
27 }
28 ++count;
29 }
30 printf("count:%d\n",count);
31
32 return 0;
33 }
但如何可以证明当前进程收到SIGPIPE信号,从而写端程序被杀死,进而导致管道破裂
创建一个子进程,将读端全部关闭,在子进程中,将写端文件描述符设置为非阻塞状态,在父进程中,调用wait函数,这样就可以知道子进程退出的原因
1 #include<stdio.h>
2 #include<unistd.h>
3 #include <fcntl.h>
4
5 int main()
6 {
7 int fd[2];
8 int ret=pipe(fd);
9 if(ret<0)
10 {
11 perror("pipe");
12 return 0;
13 }
14 int pid =fork();
15 if(pid<0)
16 {
17 perror("fork");
18 return 0;
19 }
20 else if(pid==0)
21 {
22 //子进程
23 close(fd[0]);
24 int flag=fcntl(fd[1],F_GETFL);
25 flag |=O_NONBLOCK;
26 fcntl(fd[1],F_SETFL,flag);
27 printf("子进程\n");
28 write(fd[1],"123",3);
29 }
30 else
31 {
32 //父进程
33 close(fd[0]);
34 //进程等待
35 int status;
36 wait(status);
37 printf("signal = %d\n",status & 0x7f);
38
39 }
40 return 0;
41 }
B.不写,一直进行读,将读端设置为非阻塞
B.a写端不关闭,读端进行读,read返回-1,返回资源不可用
B.b写端关闭,读端进行读,read正常调用,read返回读到的字节数
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<fcntl.h>
4
5 int main()
6 {
7 int fd[2];
8 int ret = pipe(fd);
9 if(ret<0)
10 {
11 perror("pipe error!!!");
12 return 0;
13 }
14
15 int f_read=fcntl(fd[0],F_GETFL);
16 f_read|=O_NONBLOCK;
17 fcntl(fd[0],F_SETFL,f_read);
18 close(fd[0]);
19
20 char buf[1024]={0};
21 int read_size=read(fd[0],buf,sizeof(buf)-1);
22 printf("read_size=%d\n",read_size);
23 perror("read");
24 return 0;
25 }
⑦PIPE_BUF:4K,当读写的数据小于PIPE_BUF时,保证读写的原子性
命名管道
1.具有标识符,不同的进程可以通过标识符访问到命名管道 2.如何创建命名管道 ①.使用命令创建:mkfifo 命名管道文件名 ②.使用函数创建mkfifo()是一个建立实名管道的函数。
头文件
#include<sys/types.h>
#include<sys/stat.h>
定义函数
int mkfifo(const char * pathname,mode_t mode);
mkfifo()会依参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限(mode%~umask),因此 umask值也会影响到FIFO文件的权限。Mkfifo()建立的FIFO文件其他进程都可以用读写一般文件的方式存取。当使用open()来打开 FIFO文件时,O_NONBLOCK非阻塞标志的使用与否会有影响
1、当使用O_NONBLOCK 旗标时,打开FIFO 文件来读取的操作会立刻返回,但是若还没有其他进程打开FIFO 文件来读取,则写入的操作会出错返回errno,出错号为ENXIO 。
2、没有使用O_NONBLOCK 旗标时,打开FIFO 来读取的操作会等到其他进程打开FIFO文件来写入才正常返回。同样地,打开FIFO文件来写入的操作会等到其他进程打开FIFO 文件来读取后才正常返回。
返回值
若成功则返回0,否则返回-1,错误原因存于errno中。
错误代码
EACCESS 参数pathname所指定的目录路径无可执行的权限
EEXIST 参数pathname所指定的文件已存在。
ENAMETOOLONG 参数pathname的路径名称太长。
ENOENT 参数pathname包含的目录不存在
ENOSPC 文件系统的剩余空间不足
ENOTDIR 参数pathname路径中的目录存在但却非真正的目录。
EROFS 参数pathname指定的文件存在于只读文件系统内。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/stat.h>
5 int main()
6 {
7 mkfifo("./guandao",0664);
8
9 return 0;
10 }
3.用户可以通过命名管道文件对内核当中命名管道的内存区域进行读写操作
4.命名管道的特性,具有标识符,可以满足不同进程之间的进程间通信;其他特性和匿名管道相同,管道(匿名和命名)的生命周期都是跟随着进程的
共享内存
共享内存原理:
①首先在物理内存中开辟一段空间
②然后各个进程通过页表结果将物理内存映射到自己的虚拟地址空间当中的共享区
③最后各个进程间通信就是修改各自虚拟地址空间中的共享区的地址完成
特性:不同的进程对共享内存区域进行读的时候,并不会抹除物理内存当中的数值
共享内存的接口:
- 创建共享内存
shmget(得到一个共享内存标识符或创建一个共享内存对象)
所需头文件
#include <sys/ipc.h>
#include <sys/shm.h>
函数说明
得到一个共享内存标识符或创建一个共享内存对象并返回共享内存标识符
函数原型
int shmget(key_t key, size_t size, int shmflg)
函数传入值
key
0(IPC_PRIVATE):会建立新共享内存对象
大于0的32位整数:视参数shmflg来确定操作。通常要求此值来源于ftok返回的IPC键值
size
大于0的整数:新建的共享内存大小,以字节为单位
0:只获取共享内存时指定为0
shmflg
0:取共享内存标识符,若不存在则函数会报错
IPC_CREAT:当shmflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存,返回此共享内存的标识符
IPC_CREAT|IPC_EXCL:如果内核中不存在键值 与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存则报错
函数返回值
成功:返回共享内存的标识符
出错:-1,错误原因存于errno中
附加说明
上述shmflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600)进行|运算来确定信号量集的存取权限
错误代码
EINVAL:参数size小于SHMMIN或大于SHMMAX
EEXIST:预建立key所指的共享内存,但已经存在
EIDRM:参数key所指的共享内存已经删除
ENOSPC:超过了系统允许建立的共享内存的最大值(SHMALL)
ENOENT:参数key所指的共享内存不存在,而参数shmflg未设IPC_CREAT位
EACCES:没有权限
ENOMEM:核心内存不足
- 将进程附加到共享内存上
shmat(把共享内存区对象映射到调用进程的地址空间)
所需头文件
#include <sys/types.h>
#include <sys/shm.h>
函数说明
连接共享内存标识符为shmid的共享内存,连接成功后把共享内存区对象映射到调用进程的地址空间,随后可像本地空间一样访问
函数原型
void *shmat(int shmid, const void *shmaddr, int shmflg)
函数传入值
shmid
共享内存标识符
shmaddr
指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
shmflg
SHM_RDONLY:为只读模式,其他为读写模式
函数返回值
成功:附加好的共享内存地址
出错:-1,错误原因存于errno中
附加说明
fork后子进程继承已连接的共享内存地址。exec后该子进程与已连接的共享内存地址自动脱离(detach)。进程结束后,已连接的共享内存地址会自动脱离(detach)
错误代码
EACCES:无权限以指定方式连接共享内存
EINVAL:无效的参数shmid或shmaddr
ENOMEM:核心内存不足
- 从共享内存当中分离进程
shmdt(断开共享内存连接)
所需头文件
#include <sys/types.h>
#include <sys/shm.h>
函数说明
与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存
函数原型
int shmdt(const void *shmaddr)
函数传入值
shmaddr:连接的共享内存的起始地址
函数返回值
成功:0
出错:-1,错误原因存于errno中
附加说明
本函数调用并不删除所指定的共享内存区,而只是将先前用shmat函数连接(attach)好的共享内存脱离(detach)目前的进程
错误代码
EINVAL:无效的参数shmaddr
- 共享内存的销毁
shmctl(共享内存管理)
所需头文件
#include <sys/types.h>
#include <sys/shm.h>
函数说明
完成对共享内存的控制
函数原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
函数传入值
shmid
共享内存标识符
cmd
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
IPC_RMID:删除这片共享内存
buf
共享内存管理结构体。具体说明参见共享内存内核结构定义部分
函数返回值
成功:0
出错:-1,错误原因存于errno中
错误代码
EACCES:参数cmd为IPC_STAT,却无权限读取该共享内存
EFAULT:参数buf指向无效的内存地址
EIDRM:标识符为shmid的共享内存已被删除
EINVAL:无效的参数cmd或shmid
EPERM:参数cmd为IPC_SET或IPC_RMID,却无足够的权限执行
演示代码
read_shm.c
1#include<stdio.h>
2 #include<unistd.h>
3 #include<sys/shm.h>
4 #define shm_key 0x88888888
5 int main()
6 {
7
8 int shmid=shmget(shm_key,1024,IPC_CREAT|0664);
9 if(shmid<0)
10 {
11 perror("shmget");
12 return 0;
13 }
14 void* addr=shmat(shmid,NULL,0);
15 if(!addr)
16 {
17 perror("shmat");
18 return 0;
19 }
20 while(1)
21 {
22 printf("readshm read:\%s\n",(char*)addr);
23 sleep(1);
24 }
25 shmdt(addr);
26 shmctl(shmid,IPC_RMID,NULL);
27 return 0;
28 }
write_shm.c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/shm.h>
4 #define shm_key 0x88888888
5 int main()
6 {
7
8 int shmid=shmget(shm_key,1024,IPC_CREAT|0664);
9 if(shmid<0)
10 {
11 perror("shmget");
12 return 0;
13 }
14 void* addr=shmat(shmid,NULL,0);
15 if(!addr)
16 {
17 perror("shmat");
18 return 0;
19 }
20 int count=0;
21 while(1)
22 {
23 snprintf((char*)addr,1024,"%s-%d","he-he",count);
24 ++count;
25 sleep(1);
26 }
27 shmdt(addr);
28 shmctl(shmid,IPC_RMID,NULL);
29 return 0;
30 }
如何删除一个有进程附加的共享内存
操作系统内核做法:
- 将该共享内存状态标记未dest(destroy),将共享内存的标识设置为0x00000000,标志着当前共享内存不能再被其他进程所附加,同时释放共享内存
- 假如还有进程附加在被删除的共享内存上,有可能访问到非法内存,从而导致程序越界
- 当附加程序退出掉,操作系统内核也会随之将描述共享内存的结构体释放掉
消息队列
消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。我们可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
Linux用宏MSGMAX和MSGMNB来限制一条消息的最大长度和一个队列的最大长度。
消息队列的使用
- 消息队列的创建或访问
msgget()函数
该函数用来创建和访问一个消息队列。它的原型为:
int msgget(key_t key, int msgflg);
参数:
key:消息队列关联的键。
msgflg:消息队列的建立标志和存取权限。
IPC_CREAT如果内核中没有此队列,则创建它。
IPC_EXCL当和IPC_CREAT一起使用时,如果队列已经存在,则失败。
如果单独使用IPC_CREAT,则msgget()要么返回一个新创建的消息队列的标识符,要么返回具有相同关键字值的队列的标识符。如果IPC_EXCL和IPC_CREAT一起使用,则msgget()要么创建一个新的消息队列,要么如果队列已经存在则返回一个失败值-1。IPC_EXCL单独使用是没有用处的。
返回说明:
成功执行时,返回消息队列标识值。失败返回-1,errno被设为非零值 ,所以当返回0,也是可以正常使用的
- 给消息队列中增加消息
msgsnd()函数
该函数用来把消息添加到消息队列中。它的原型为:
int msgsend(int msgid, const void *msg_ptr, size_t msg_sz, int msgflg);
msqid:消息队列的识别码。
msgp:指向消息缓冲区的指针,此位置用来暂时存储发送和接收的消息,是一个用户可定义的通用结构,形态如下
struct msgbuf {
long mtype; /* 消息类型,必须 > 0 */
char mtext[1]; /* 消息文本 */
};
msgsz:消息的大小。
msgtyp:消息类型
msgtyp等于0 则返回队列的最早的一个消息。
msgtyp大于0,则返回其类型为msgtyp的第一个消息。
msgtyp小于0,则返回其类型小于或等于mtype参数的绝对值的最小的一个消息。
msgflg:这个参数依然是控制函数行为的标志,取值可以是:0,表示忽略;IPC_NOWAIT,如果消息队列为空,则返回一个ENOMSG,并将控制权交回调用函数的进程。如果不指定这个参数,那么进程将被阻塞直到函数可以从队列中得到符合条件的消息为止。如果一个client 正在等待消息的时候队列被删除,EIDRM 就会被返回。如果进程在阻塞等待过程中收到了系统的中断信号,EINTR 就会被返回。MSG_NOERROR,如果函数取得的消息长度大于msgsz,将只返回msgsz 长度的信息,剩下的部分被丢弃了。如果不指定这个参数,E2BIG 将被返回,而消息则留在队列中不被取出。当消息从队列内取出后,相应的消息就从队列中删除了。
返回说明
成功执行时,msgsnd()返回0,msgrcv()返回拷贝到mtext数组的实际字节数。失败两者都返回-1,errno被设为以下的某个值
- 获取消息队列信息
msgrcv()函数
该函数用来从一个消息队列获取消息,它的原型为
int msgrcv(int msgid, void *msg_ptr, size_t msg_st, long int msgtype, int msgflg);
msgid, msg_ptr, msg_st 的作用也函数msgsnd()函数的一样。
msgtype 可以实现一种简单的接收优先级。如果msgtype为0,就获取队列中的第一个消息。如果它的值大于零,将获取具有相同消息类型的第一个信息。如果它小于零,就获取类型等于或小于msgtype的绝对值的第一个消息。
msgflg 用于控制当队列中没有相应类型的消息可以接收时将发生的事情。
调用成功时,该函数返回放到接收缓存区中的字节数,消息被复制到由msg_ptr指向的用户分配的缓存区中,然后删除消息队列中的对应消息。失败时返回-1。
- 控制消息队列
msgctl()函数
该函数用来控制消息队列,它与共享内存的shmctl函数相似,它的原型为:
int msgctl(int msgid, int command, struct msgid_ds *buf);
command是将要采取的动作,它可以取3个值,
IPC_STAT:把msgid_ds结构中的数据设置为消息队列的当前关联值,即用消息队列的当前关联值覆盖msgid_ds的值。
IPC_SET:如果进程有足够的权限,就把消息列队的当前关联值设置为msgid_ds结构中给出的值
IPC_RMID:删除消息队列
buf是指向msgid_ds结构的指针,它指向消息队列模式和访问权限的结构。msgid_ds结构至少包括以下成员:
struct msgid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};
成功时返回0,失败时返回-1.
消息队列与命名管道的比较
消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write(),接收数据用read(),则在消息队列中,发送数据用msgsnd(),接收数据用msgrcv()。而且它们对每个数据都有一个最大长度的限制。
与命名管道相比,消息队列的优势在于:
1、消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。
2、同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。
3、接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。