linux下进程间通信
在linux的生产中我们不可能是单个进程完成某项任务,很多时候需要进程间实现交互。其中包括
- 数据传输:⼀一个进程需要将它的数据发送给另⼀一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:⼀一个进程需要向另⼀一个或⼀一组进程发送消息,通知它(它们)发⽣生了某种 事件(如进程 终⽌止时要通知⽗父进程)。
- 进程控制:有些进程希望完全控制另⼀一个进程的执⾏行(如Debug进程),此时控制进程希望能够拦 截另⼀一个进程的所有陷⼊入和异常,并能够及时知道它的状态改变。
我们一共有八种基本的进程间通信方式,各有优缺点,分别是命名管道,匿名管道,消息队列,共享内存,信号,套接字,内存映射,信号量,其中信号单独为一个章节见下一章博客,接下来逐一介绍七种进程间通信方式。
匿名管道
管道是最古老的进程间通信方式,它是一种半双工的通信方式,就是只能进行单向传输,管道的本质是内核的一块缓冲区。在linux中一切皆为文件,我们操作管道的方式就是通过文件进行操作,管道会有两个文件操作符fd[0],fd[1],fd[0]是代表读的一端,fd[1]是代表写的一端,我们在用的时候必须两个进程各关闭对应的文件,第一个进程关闭了读的端口第二个就必须关闭写的端口,由于管道是匿名管道所以不是随便两个进程都可以使用匿名管道只有具有亲缘关系的进程可以使用匿名管道进行通信。
int pipe(int pipefd[2]);
成功返回0,失败返回错误码。pipefd[0]是读端,pipefd[1]是写端。
管道的生命周期是跟随进程的,当进程结束时关闭文件即可,并且匿名管道自带同步与互斥
管道的读写规则
当没有数据可读时
- O_NONBLOCK disable:read调⽤用阻塞,即进程暂停执⾏行,一直等到有数据来到为止。
- O_NONBLOCK enable:read调⽤用返回-1,errno值为EAGAIN。
当管道数据满了的时候
- O_NONBLOCK disable: write调⽤用阻塞,直到有进程读⾛走数据
- O_NONBLOCK enable:调⽤用返回-1,errno值为EAGAIN
- 如果所有管道读端对应的⽂文件描述符被关闭,则write操作会产⽣生信号SIGPIPE,进⽽而可能导致write 进程退出
- 当要写⼊入的数据量不⼤大于PIPE_BUF时,linux将保证写⼊入的原⼦子性。
- 当要写⼊入的数据量⼤大于PIPE_BUF时,linux将不再保证写⼊入的原⼦子性
下面是一个利用匿名管道实现父子进程间通信的小程序
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int fd[2];
//创建管道
pid_t pid;
int n;
char buf[256]={0};
//管道创建失败
if(pipe(fd)!=0)
{
perror("fife create error!\n");
exit(0);
}
pid = fork();
//创建进程失败
if(pid < 0)
{
perror("fork error!\n");
exit(0);
}
//子进程
else if(pid == 0)
{
//子进程关闭读端
close(fd[0]);
write(fd[1],"hallo",5);
}
//父进程
else
{
//父进程关闭写端
close(fd[1]);
read(fd[0],buf,sizeof(buf));
printf("child say:%s\n",buf);
}
return 0;
}
命名管道
命名管道是文件系统可见,是一个特殊类型的文件
命名管道可以应用于同意主机上任意进程的进程间通信
创建:
1.命令创建:mkfifo pipe_fulename
2.代码创建:int mkfifo(const char *pathname, mode_t mode);(mode是权限)
打开特性:因为命名管道需要我们用户自己打开文件,匿名管道创建后直接打开返回描述符
1.如果只读打开,会阻塞等待这个命名管道被其他进程以写打开
2.若果只写打开,会阻塞等待这个命名管道被其他进程以读打开
3.如果以读写打开,则不会堵塞
读写特性
1.如果管道没数据,读取操作会阻塞,如果描述符被设置为非阻塞属性,那么这个操作不会被阻塞
2.如果管道数据满了,写入操作会阻塞
3.如果管道的写端全部关闭,read读取数据的时候会返回0
4.如果管道读端全部关闭,那么write写入数据的时候会触发异常,操作系统会发送信号到进程,进程退粗
5.当写入大小超过PIPE_BUF,则这个操作是非原子操作,可能被打断
命名管道与匿名管道的不同之处在于命名管道的本质是一个文件,而匿名管道本质是内核的一段缓冲区,命名管道可以用于任何两个进程间进行通信,匿名管道只能用于亲缘间进程通信
以下是命名管道的代码实现
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int mian()
{
pid_t pid;
int fd;
char buf[256]={0};
if(mkfifo("tp",0664)<0)
{
perror("mkfifo error!\n");
exit(0);
}
if(pid<0)
{
perror("create pid error!\n");
exit(0);
}
else if(pid == 0)
{
//子进程
fd=open("tp",O_WRONLY);
write(fd,"hello",5);
sleep(5);
close("tp");
}
else
{
//父进程
fd=open("tp",O_RDONLY);
read(fd,buf,sizeof(buf));
printf("child say:%s\n",buf);
close("tp");
}
return 0;
}
消息队列
消息队列是内核创建的一个队列,进程可以在这个队列中创建节点,通过这个队列的标识符key,每一个进程都可以找到这个节点,并且与管道不同的是,管道是随着进程的终止结束生命周期的,而消息队列是存在在内核中的所有进程可见的一个队列,所以消息队列的生命周期随内核,管道两个接口一个只能读一个只能写所以管道是一个半双工的通信方式,而消息队列是通过插入节点的方式,所有进程都可以插入节点也都可以读取节点,所以消息队列是一个全双工的通信方式。
不过事实上这是一种逐渐被淘汰的通信方式,由于我们可以用流管道与套接字更好的替代他所以很不推荐这一种进程间通信方式。
int msgget(key_t key, int msgflg);
创建消息队列
key:内核中消息队列的标识
msgflg:选项 IPC_CREAT不存在就创建,存在就打开 IPC_EXCL存在就返回
返回值:操作的句柄,失败就返回-1
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);
发送与接收数据
msqid:msgflg返回的句柄
msgp:用于接收/发送的数据
msgsz:用于接收/发送的数据大小
msgtype:用于接收的数据类型
msgflg:标志选项
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
控制/删除消息队列
msqid:msgflg返回的句柄
cmd:选项 我们这里用于退出用IPC_RMID
buf:不关心置NULL
下面是两个进程间利用消息队列通信的代码示例
#include<stdio.h>
#include<stdlib.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#define IPC_KEY 0x12345678
#define TYPE_S 1
#define TYPE_C 2
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1024]; /* message data */
};
int main()
{
int msgid = -1;
//创建一个消息队列
msgid = msgget(IPC_KEY,IPC_CREAT | 0664);
if(msgid<0)
{
//创建失败
perror("msgget error!\n");
exit(0);
}
while(1)
{
struct msgbuf buf;
//发送数据
memset(&buf,0x00,sizeof(struct msgbuf));
buf.mtype = TYPE_S;
scanf("%s",buf.mtext);
msgsend(msgid,&buf,1024,0);
//接收数据
memset(&buf,0x00,sizeof(struct msgbuf));
msgrcv(msgid,&buf,1024,TYPE_C,0);
printf("C say : %s\n",buf.mtext);
}
return 0;
}
共享内存
共享内存是通过一个进程创建一个内存共享区,其他进程可以对这块内存进行读写操作所实现的的进程间通信,共享内存是进程间最快的通信方式,因为不论是管道还是消息队列都经过了内核,多了用户态到内核与内核返回用户态两部操作,需要注意的是共享内存
我们一般用系统提供的接口shmxxx族函数来实现共享内存删除的时候,如果还有进程与共享内存保持映射关系,那么共享内存不会删除,而是等待这个进程解除映射
系统为我们提供了一套shmxxx的接口来供我们实现控制共享内存
int shmget(key_t key, size_t size, int shmflg);
key:共享内存在内存中的标识
size:要使用内存的大小
shmflg:权限选 IPC_CREAT IPC_EXCL
void *shmat(int shmid, const void *shmaddr, int shmflg);
映射物理内存
shmid:shmget返回的操作句柄
shmaddr:共享内存首地址,置空由系统分配
shmflg:权限选项 SHM_REONLY只读
int shmdt(const void *shmaddr);
解除映射,shamaddr是共享内存首地址
nt shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:操作句柄
cmd:权限 IPC_RMID 删除
buf:用来接收共享内存信息,不关心置空
下面是共享内存的实现用例
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<errno.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define IPC_KEY 0x12345678
int main()
{
int shmid = -1;
shmid = shmget(IPC_KEY,32,IPC_CREAT|0664);
if(shmid == -1)
{
perror("get shm error!\n");
exit(0);
}
void* start = shmat(shmid,NULL,0);
if((void*)-1 == start)
{
perror("shmat error!\n");
exit(0);
}
while(1)
{
printf("please input:");
memset(start,0x00,32);
scanf("%s",(char*)start);
sleep(1);
}
shmdt(start);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
信号量
信号量是用于实现临界资源同步与互斥的一种进程间通信方式,它的本质是一个内核中的计数器,信号量通过对临界资源进行PV操作来实现对临界资源的同步与互斥
同步:临界资源操作的时序性
互斥:临界资源同一时间的唯一访问性
简单来说一个进程或线程在对临界资源进行操作的时候先在信号量这里获取一把锁,且这个锁同一时间只能获取一次(原子操作),只能由锁的持有者释放,当操作完毕将锁释放,也被叫做PV操作,信号量作为进程间通信方式,意味着大家都能访问,信号量实际上也是一个临界资源,当然信号量的这个临界资源操作是不会出问题的,因为信号量的操作是一个原子操作
信号量分为两种:
- 内核信号量,由内核控制使用
- 用户态信号量,分为posix与System V两种
posix中的信号量一般是一个非负整数,常用于线程间同步,而System V中的信号量是一个或多个信号量的集合,常用于进程间同步,相对来说System V信号量更复杂一点。
##System V信号量
System V信号量强调的是一个或多个信号量的集合,对应一个信号量结构体,信号量只是它的一部分,经常用于进程间同步
int semget(key_t key, int nsems, int semflg);
创建一个信号量
key:信号量在内核中的标识
nsems:创建信号量的个数
semflg:选项 IPC_CREAT 没有就创建 0664 权限
int semctl(int semid, int semnum, int cmd, …);
控制信号量
semid:操作句柄
semnum:操作信号量的个数
cmd:具体的操作 SETALL操作多个信号量 SETVEL操作单个信号量(semnum将被忽略)
int semop(int semid, struct sembuf sops, unsigned nsops)
用来创建与访问一个信号量集
semid:操作句柄
sops:指向一个结构数值的指针
nsops:操作的信号量个数
###PV操作
由于PV操作至关重要而且我们这里主要是通过PV操作来控制,所以这里剖析下信号量的PV操作,当我们对临界资源进行操作的时候,内核中提供了信号量来实现进程间通信的同步与互斥,
每一个进程在对临界资源进行操作的时候先进行P操作,计数器加一,代表这个临界资源已经被获取,当操作完成后计数器减一代表计数器操作完成唤醒其他进程对临界资源进行操作。
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_INF (Linux-specific) */
};
这个是设置信号量的基本信息
struct sembuf { short sem_num; short sem_op; short sem_flg; };
这个是设置信号量进行的操作设置
num是操作信号量的个数,sem_op是要对信号量进行的操作,-1与+1
sem_flg的两个取值是IPC_NOWAIT或SEM_UNDO
在实现中我们先设置信号量的基本信息,在P与V的操作函数里利用第二个结构体设置信号量进行的操作,以下是代码实现
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define IPC_KEY 0x12345678
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) */
};
void sem_P(int id)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = -1;
buf.sem_flg = SEM_UNDO;
semop(id, &buf, 1);
}
void sem_V(int id)
{
struct sembuf buf;
buf.sem_num = 0;
buf.sem_op = 1;
buf.sem_flg = SEM_UNDO;
semop(id, &buf, 1);
}
int main()
{
int pid = -1;
int semid = semget(IPC_KEY, 1, IPC_CREAT | 0664);
if (semid < 0) {
perror("semget error");
return -1;
}
union semun val;
val.val = 1;
semctl(semid, 0, SETVAL, val);
pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
}else if (pid == 0) {
sleep(1);
while(1) {
sem_P(semid);
//对于一元信号量来说,当这个进程获取的信号量之后,那么
//另一个进程将获取不到信号量,会等待,也就是说,在释放
//信号量之前,我的临界操作不会被打断
printf("A");
fflush(stdout);
usleep(1000);
printf("A ");
fflush(stdout);
//释放信号量
sem_V(semid);
}
}else {
//打印B
while(1) {
sem_P(semid);
printf("B");
fflush(stdout);
usleep(1000);
printf("B ");
fflush(stdout);
sem_V(semid);
}
}
return 0;
}
posix信号量留作线程阶段在做讲解,
以上是常用的几种进程间通信方式。