今天要分享的是Linux进程的同步机制,包括管道和IPC。之前学习的信号也有控制进程同步的作用,但是信号仅仅传输很少的信息,而且系统开销大,所以这里再介绍几种其他的进程同步机制。在之前的一篇文章中有提到相关内容,但是当时没有详细展开,可以回顾一下:Linux笔记(10)| 进程概述。
一、管道(无名管道)
一般说管道都是指无名管道(也称匿名管道),有名管道(也称命名管道)不会直接说管道,肯定会加上前缀。
管道是Linux下最常见的进程间的通信方式之一,它是在两个进程之间实现一个数据流通的通道。它有以下特点:
1、管道一般是半双工的,数据只能向一个方向流动。
2、通常只能在父子进程或者兄弟进程之间使用。
3、管道是一种特殊的文件,并且只存在于内存当中
……
接下来说一下管道的使用:
创建管道
int pipe(int pipefd[2]);
输入参数是一个数组,实际上这个输入参数是当做输出来用的,调用这个函数,成功的话数组里就会保存两个文件描述符,并且返回0,失败返回-1.
其中文件描述符fd[0]是读端,fd[1]是写端,这是固定不变的。刚刚我们也说了,创建管道肯定是为了父子进程或者兄弟进程之间通信的,单独在一个进程里面使用管道毫无意义。
所以父进程创建好管道之后,再调用fork函数创建子进程,子进程就会继承那个管道,于是父子间可以约定谁来读,谁来写。因为是半双工通信,所以只能一个读,一个写,如果想要两端都能读写,那就要创建两个管道。
比如父进程读,子进程写,那么父进程可以先close(fd[1]),然后read(fd[0]),子进程先close(fd[0]),然后write(fd[1]).
读写的时候只要把他当做普通的文件就行了,和普通的文件描述符的读写一样,但是有一点不一样的是普通文件读完了数据还在,而管道读完之后数据就没了。
二、有名管道
管道只能在有亲缘关系的进程之间实现通信,但是有名管道可以在任何两个进程之间实现通信。有名管道严格遵循先进先出的规则,不支持lseek函数等文件定位操作。
首先创建一个有名管道:
int mkfifo(const char*pathname,mode_t mode);
pathname参数是一个普通的路径名,也就是创建后有名管道文件的名字,mode参数是文件的操作权限,调用成功返回0,调用失败返回-1.
接下来就可以使用open或者fopen函数打开刚刚创建的有名管道文件,对其进行读写操作了。
三、System V IPC机制
IPC机制由消息队列、信号量以及共享内存三种具体实现方法组成。
首先要了解两个概念,标识符和关键字。每一个IPC结构(消息队列或者信号量或者共享内存)都有一个标识符,这是一个非负整数,每创建一个IPC结构,相应的标识符就会加1,这个标识符在相同的结构中是唯一的,也就是说,如果“666”是某个消息队列的标识符,那么肯定不会有第二个消息队列的标识符也是“666”。但是需要注意的是,可能某个共享内存的标识符也是“666”。于是,还得用一个东西来区分,那就是关键字了。所以,根据关键字和标识符可以唯一确定一个IPC结构。
IPC的关键字一般可以使用IPC_PRIVATE,也可以使用ftok函数获得,他们有一些区别,后面会提到。
ftok函数的使用:
key_t ftok(const char* pathname,int proj_id);
ftok函数是用于将一个路径和项目ID转换为关键字,第一个参数必须是一个存在的、可以访问的文件路径名,第二个参数项目ID只有低八位有效。
在创建一个IPC对象的时候,他们有一些共同的特点:
我们先来看一下IPC对象创建函数:
1、创建消息队列
int msgget(key_t key,int msgflg);
2、创建信号量
int semget(key_t key,int nsems,int semflg);
3、创建共享内存
int shmget(key_t ,size_t size,int shmflg);
可以发现,他们都有一个关键字key,都有一个flag参数。在使用上,也有一些共同的特点:
当key使用IPC_PRIVATE时,操作系统保证创建一个唯一的IPC对象,此时flag参数仅决定对象的存取权限。
当key使用ftok函数得到的关键字时,flag参数不仅决定对象的存取权限,还和创建方式有关,具体就是:
设置flag参数的IPC_CREAT位,但不设置IPC_EXCL位,如果不存在指定key的IPC对象,就创建,如果存在,就返回该对象。
同时设置IPC_CREAT位和IPC_EXCL位,如果对象不存在就创建,如果已经存在,则返回错误。
这和文件操作函数open是类似的。
接下来介绍一下各个IPC对象涉及到的API函数。由于具体的细节比较多,这里就不一一罗列了,可以使用man手册来查看具体细节。
1、消息队列
创建:用来创建或者打开一个消息队列
int msgget(key_t key,int msgflg);
控制:其中cmd命令参数可以获取或者设置buf里的内容,也可以删除这个消息队列(看具体的参数设置)
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
发送:向消息队列里发送消息
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);
2、信号量
创建:用来创建或者打开一个信号量
int semget(key_t key, int nsems, int semflg);
操作:可以申请或者释放信号量。
int semop(int semid, struct sembuf *sops, size_t nsops);
控制:可以设置或返回信号量的值,可以删除信号量
int semctl(int semid, int semnum, int cmd, ...);
3、共享内存
创建:用来创建或者打开一个共享内存。
int shmget(key_t key, size_t size, int shmflg);
连接:将共享内存附加到进程里的地址空间(有点像映射)
void *shmat(int shmid, const void *shmaddr, int shmflg);
脱离:将共享内存和进程地址空间脱离,但不删除共享内存本身
void *shmat(int shmid, const void *shmaddr, int shmflg);
设置属性:可以设置或者返回buf里的内容,也可以删除共享内存。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
接下来写一段代码,需求是:创建一个共享内存,子进程写入数据,父进程读出数据,同时使用信号量来对读写进行保护
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define SHMSIZE 128
int main(int argc,char* argv[])
{
if(argc !=2){
printf("please input param\n");
exit(0);
}
key_t semkey,shmkey;
int semid,shmid,pid;
char* shmp;
struct sembuf locksembuf={0,-1,SEM_UNDO};
struct sembuf unlocksembuf={0,1,SEM_UNDO|IPC_NOWAIT};
struct shmid_ds shmdsbuff;
semkey=ftok(argv[1],0);
if(semkey==-1){
printf("semkey ftok failed\n");
exit(0);
}
shmkey=ftok(argv[1],1);
if(semkey==-1){
printf("shmkey ftok failed\n");
exit(0);
}
semid=semget(semkey,1,IPC_CREAT | 0666); //创建信号量
if(semid==-1){
perror("semget");
exit(0);
}
shmid=shmget(shmkey,SHMSIZE,0666 | IPC_CREAT ); //创建共享内存
if(shmid==-1)
{
perror("shmget");
exit(0);
}
printf("creat share memory success\n");
shmp=shmat(shmid,NULL,0); //连接共享内存(映射地址)
if((long)shmp==-1)
{
perror("shmat failed\n");
exit(0);
}
pid=fork();
if(pid<0)
{
perror("fork failed\n");
exit(0);
}
else if(pid==0) //子进程
{
printf("creat child process success\n");
if(semctl(semid,0,SETVAL,1)<0) //初始化信号量为1
{
perror("semctl");
exit(0);
}
if(semop(semid,&locksembuf,1)==-1) //申请资源
{
perror("semop");
exit(0);
}
sleep(4);
strcpy(shmp,"hello world\n");
if(shmdt((void*)shmp)<0) //使共享内存脱离进程地址空间
{
perror("shmdt");
exit(0);
}
if(semop(semid,&unlocksembuf,1)<0) //解锁临界资源
{
perror("child process unlock semop");
exit(0);
}
}
else //父进程
{
printf("This is parent process\n");
sleep(1);
int ret=semop(semid,&locksembuf,1);
if(ret<0)
{
perror("parent process semop lock");
exit(0);
}
if(shmctl(shmid,IPC_STAT,&shmdsbuff)<0) //父进程获取共享内存的信息
{
perror("shmctl");
exit(0);
}
else{
printf("get shm success\n");
printf("Shared Memory Infomation:\n");
printf("\tCreator PID:%d\n",shmdsbuff.shm_cpid);
printf("\tSize(bytes):%ld\n",shmdsbuff.shm_segsz);
printf("\tLast Operator PID :%d\n",shmdsbuff.shm_lpid);
printf("Receive message:%s\n",(char*)shmp);
}
if(shmdt((void*)shmp)<0)
{
perror("parent process shmdt");
exit(0);
}
ret=semop(semid,&unlocksembuf,1);
if(ret<0)
{
perror("parent process semop");
exit(0);
}
if(shmctl(shmid,IPC_RMID,NULL)<0)
{
perror("shmctl RMID");
exit(0);
}
if(semctl(semid,0,IPC_RMID,NULL)<0)
{
perror("semctl RMID");
exit(0);
}
}
return 0;
}
接下来编译执行
gcc sem.c
./a.out a.txt //注意,当前目录下需要有一个a.txt,其他文件也行
输出:
以上就是今天的内容。