一.為什么進(jìn)程間需要通信?
1).數(shù)據(jù)傳輸
一個進(jìn)程需要將它的數(shù)據(jù)發(fā)送給另一個進(jìn)程;
2).資源共享
多個進(jìn)程之間共享同樣的資源;
3).通知事件
一個進(jìn)程需要向另一個或一組進(jìn)程發(fā)送消息,通知它們發(fā)生了某種事件;
4).進(jìn)程控制
有些進(jìn)程希望完全控制另一個進(jìn)程的執(zhí)行(如Debug進(jìn)程),該控制進(jìn)程希望能夠攔截另一個進(jìn)程的所有操作,并能夠及時知道它的狀態(tài)改變。
基于以上幾個原因,所以就有了進(jìn)程間通信的概念,那仫進(jìn)程間通信的原理是什仫呢?目前有哪幾種進(jìn)程間通信的機(jī)制?他們是如何實現(xiàn)進(jìn)程間通信的呢?在這篇文章中我會就這幾個問題進(jìn)行詳細(xì)的講解。
二.進(jìn)程間通信的原理
每個進(jìn)程各自有不同的用戶地址空間,任何一個進(jìn)程的全局變量在另一個進(jìn)程中都看不到,所以進(jìn)程之間要交換數(shù)據(jù)必須通過內(nèi)核,在內(nèi)核中開辟一塊緩沖區(qū),進(jìn)程1把數(shù)據(jù)從用戶空間拷到內(nèi)核緩沖區(qū),進(jìn)程2再從內(nèi)核緩沖區(qū)把數(shù)據(jù)讀走,內(nèi)核提供的這種機(jī)制稱為進(jìn)程間通信機(jī)制。
主要的過程如下圖所示:
三.進(jìn)程間通信的幾種方式
1.管道(pipe)
管道又名匿名管道,這是一種最基本的IPC機(jī)制,由pipe函數(shù)創(chuàng)建:
#include
int pipe(int pipefd[2]);
返回值:成功返回0,失敗返回-1;
調(diào)用pipe函數(shù)時在內(nèi)核中開辟一塊緩沖區(qū)用于通信,它有一個讀端,一個寫端:pipefd[0]指向管道的讀端,pipefd[1]指向管道的寫端。所以管道在用戶程序看起來就像一個打開的文件,通過read(pipefd[0])或者write(pipefd[1])向這個文件讀寫數(shù)據(jù),其實是在讀寫內(nèi)核緩沖區(qū)。
使用管道的通信過程:
1.父進(jìn)程調(diào)用pipe開辟管道,得到兩個文件描述符指向管道的兩端。
2.父進(jìn)程調(diào)用fork創(chuàng)建子進(jìn)程,那么子進(jìn)程也有兩個文件描述符指向同一管道。
3.父進(jìn)程關(guān)閉管道讀端,子進(jìn)程關(guān)閉管道寫端。父進(jìn)程可以往管道里寫,子進(jìn)程可以從管道里讀,管道是用環(huán)形隊列實現(xiàn)的,數(shù)據(jù)從寫端流入從讀端流出,這樣就實現(xiàn)了進(jìn)程間通信。
管道出現(xiàn)的四種特殊情況:
1.寫端關(guān)閉,讀端不關(guān)閉;
那么管道中剩余的數(shù)據(jù)都被讀取后,再次read會返回0,就像讀到文件末尾一樣。
2.寫端不關(guān)閉,但是也不寫數(shù)據(jù),讀端不關(guān)閉;
此時管道中剩余的數(shù)據(jù)都被讀取之后再次read會被阻塞,直到管道中有數(shù)據(jù)可讀了才重新讀取數(shù)據(jù)并返回;
3.讀端關(guān)閉,寫端不關(guān)閉;
此時該進(jìn)程會收到信號SIGPIPE,通常會導(dǎo)致進(jìn)程異常終止。
4.讀端不關(guān)閉,但是也不讀取數(shù)據(jù),寫端不關(guān)閉;
此時當(dāng)寫端被寫滿之后再次write會阻塞,直到管道中有空位置了才會寫入數(shù)據(jù)并重新返回。
使用管道的缺點:
1.兩個進(jìn)程通過一個管道只能實現(xiàn)單向通信,如果想雙向通信必須再重新創(chuàng)建一個管道或者使用sockpair才可以解決這類問題;
2.只能用于具有親緣關(guān)系的進(jìn)程間通信,例如父子,兄弟進(jìn)程。
一個簡單的關(guān)于管道的例子:
代碼實現(xiàn)如下:
#include
#include
#include
#include
int main()
{
int _pipe[2]={0,0};
int ret=pipe(_pipe); //創(chuàng)建管道
if(ret == -1)
{
perror("create pipe error");
return 1;
}
printf("_pipe[0] is %d,_pipe[1] is %dn",_pipe[0],_pipe[1]);
pid_t id=fork(); //父進(jìn)程fork子進(jìn)程
if(id < 0)
{
perror("fork error");
return 2;
}
else if(id == 0) //child,寫
{
printf("child writingn");
close(_pipe[0]);
int count=5;
const char *msg="i am from XATU";
while(count--)
{
write(_pipe[1],msg,strlen(msg));
sleep(1);
}
close(_pipe[1]);
exit(1);
}
else //father,讀
{
printf("father readingn");
close(_pipe[1]);
char msg[1024];
int count=5;
while(count--)
{
ssize_t s=read(_pipe[0],msg,sizeof(msg)-1);
if(s > 0){
msg[s]='?';
printf("client# %sn",msg);
}
else{
perror("read error");
exit(1);
}
}
if(waitpid(id,0,NULL) != -1){
printf("wait successn");
}
}
return 0;
}
2.命名管道(FIFO)
上一種進(jìn)程間通信的方式是匿名的,所以只能用于具有親緣關(guān)系的進(jìn)程間通信,命名管道的出現(xiàn)正好解決了這個問題。FIFO不同于管道之處在于它提供一個路徑名與之關(guān)聯(lián),以FIFO的文件形式存儲文件系統(tǒng)中。命名管道是一個設(shè)備文件,因此即使進(jìn)程與創(chuàng)建FIFO的進(jìn)程不存在親緣關(guān)系,只要可以訪問該路徑,就能夠通過FIFO相互通信。
命名管道的創(chuàng)建與讀寫:
1).是在程序中使用系統(tǒng)函數(shù)建立命名管道;
2).是在Shell下交互地建立一個命名管道,Shell方式下可使用mknod或mkfifo命令來創(chuàng)建管道,兩個函數(shù)均定義在頭文件sys/stat.h中;
#include
#include
#include
#include
int mknod(const char *pathname, mode_t mode, dev_t dev);
#include
#include
int mkfifo(const char *pathname, mode_t mode);
返回值:都是成功返回0,失敗返回-1;
path為創(chuàng)建的命名管道的全路徑名;
mod為創(chuàng)建的命名管道的模式,指明其存取權(quán)限;
dev為設(shè)備值,該值取決于文件創(chuàng)建的種類,它只在創(chuàng)建設(shè)備文件時才會用到;
mkfifo函數(shù)的作用:在文件系統(tǒng)中創(chuàng)建一個文件,該文件用于提供FIFO功能,即命名管道。
命名管道的特點:
1.命名管道是一個存在于硬盤上的文件,而管道是存在于內(nèi)存中的特殊文件。所以當(dāng)使用命名管道的時候必須先open將其打開。
2.命名管道可以用于任何兩個進(jìn)程之間的通信,不管這兩個進(jìn)程是不是父子進(jìn)程,也不管這兩個進(jìn)程之間有沒有關(guān)系。
一個簡單的關(guān)于命名管道的例子:
代碼實現(xiàn)如下:
server.c
#include
#include
#include
#include
#include
void testserver()
{
int namepipe=mkfifo("myfifo",S_IFIFO|0666); //創(chuàng)建一個存取權(quán)限為0666的命名管道
if(namepipe == -1){
perror("mkfifo error");
exit(1);
}
int fd=open("./myfifo",O_RDWR); //打開該命名管道
if(fd == -1){
perror("open error");
exit(2);
}
char buf[1024];
while(1)
{
printf("sendto# ");
fflush(stdout);
ssize_t s=read(0,buf,sizeof(buf)-1); //從標(biāo)準(zhǔn)輸入獲取消息
if(s > 0){
buf[s-1]='?'; //過濾掉從標(biāo)準(zhǔn)輸入中獲取的換行
if(write(fd,buf,s) == -1){ //把該消息寫入到命名管道中
perror("write error");
exit(3);
}
}
}
close(fd);
}
int main()
{
testserver();
return 0;
}
client.c
#include
#include
#include
#include
#include
void testclient()
{
int fd=open("./myfifo",O_RDWR);
if(fd == -1){
perror("open error");
exit(1);
}
char buf[1024];
while(1){
ssize_t s=read(fd,buf,sizeof(buf)-1);
if(s > 0){
printf("client# %sn",buf);
}
else{ //讀失敗或者是讀取到字符結(jié)尾
perror("read error");
exit(2);
}
}
close(fd);
}
int main()
{
testclient();
return 0;
}
3.消息隊列(msg)
由于內(nèi)容較多,以后再詳細(xì)分享
4.信號量(sem)
什仫是信號量?
信號量的本質(zhì)是一種數(shù)據(jù)操作鎖,用來負(fù)責(zé)數(shù)據(jù)操作過程中的互斥,同步等功能。
信號量用來管理臨界資源的。它本身只是一種外部資源的標(biāo)識,不具有數(shù)據(jù)交換功能,而是通過控制其他的通信資源實現(xiàn)進(jìn)程間通信??梢赃@樣理解,信號量就相當(dāng)于是一個計數(shù)器。當(dāng)有進(jìn)程對它所管理的資源進(jìn)行請求時,進(jìn)程先要讀取信號量的值:大于0,資源可以請求;等于0,資源不可以用,這時進(jìn)程會進(jìn)入睡眠狀態(tài)直至資源可用。
當(dāng)一個進(jìn)程不再使用資源時,信號量+1(對應(yīng)的操作稱為V操作),反之當(dāng)有進(jìn)程使用資源時,信號量-1(對應(yīng)的操作為P操作)。對信號量的值操作均為原子操作。
為什仫要使用信號量?
為了防止出現(xiàn)因多個程序同時訪問一個共享資源而引發(fā)的一系列問題,我們需要一種方法,它可以通過生成并使用令牌來授權(quán),在任一時刻只能有一個執(zhí)行線程訪問代碼的臨界區(qū)域。
什仫是臨界區(qū)?什仫是臨界資源?
臨界資源:一次只允許一個進(jìn)程使用的資源。
臨界區(qū):訪問臨界資源的程序代碼片段。
信號量的工作原理?
P(sv):如果sv的值大于零,就給它減1;如果它的值為零,就掛起該進(jìn)程的執(zhí)行等待操作;
V(sv):如果有其他進(jìn)程因等待sv而被掛起,就讓它恢復(fù)運行,如果沒有進(jìn)程因等待sv而掛起,就給它加1;
舉個例子,就是兩個進(jìn)程共享信號量sv,一旦其中一個進(jìn)程執(zhí)行了P(sv)操作,它將得到信號量,并可以進(jìn)入臨界區(qū),使sv減1。而第二個進(jìn)程將被阻止進(jìn)入臨界區(qū),因為當(dāng)它試圖執(zhí)行P(sv)時,sv為0,它會被掛起以等待第一個進(jìn)程離開臨界區(qū)域并執(zhí)行V(sv)釋放信號量,這時第二個進(jìn)程就可以恢復(fù)執(zhí)行了。
與信號量有關(guān)的函數(shù)操作?
1).創(chuàng)建/獲取一個信號量集合
#include
#include
#include
int semget(key_t key, int nsems, int semflg);
返回值:成功返回信號量集合的semid,失敗返回-1。
key:可以用函數(shù)key_t ftok(const char *pathname, int proj_id);來獲取。
nsems:這個參數(shù)表示你要創(chuàng)建的信號量集合中的信號量的個數(shù)。信號量只能以集合的形式創(chuàng)建。
semflg:同時使用IPC_CREAT和IPC_EXCL則會創(chuàng)建一個新的信號量集合。若已經(jīng)存在的話則返回-1。單獨使用IPC_CREAT的話會返回一個新的或者已經(jīng)存在的信號量集合。
2).信號量結(jié)合的操作
#include
#include
#include
int semop(int semid, struct sembuf *sops, unsigned nsops);
int semtimedop(int semid, struct sembuf *sops, unsigned nsops,struct timespec *timeout);
返回值:成功返回0,失敗返回-1;
semid:信號量集合的id;
struct sembuf *sops;
struct sembuf
{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
}
sem_num:為信號量是以集合的形式存在的,就相當(dāng)于所有信號在一個數(shù)組里面,sem_num表示信號量在集合中的編號;
sem_op:示該信號量的操作(P操作還是V操作)。如果其值為正數(shù),該值會加到現(xiàn)有的信號內(nèi)含值中。通常用于釋放所控資源的使用權(quán);如果sem_op的值為負(fù)數(shù),而其絕對值又大于信號的現(xiàn)值,操作將會阻塞,直到信號值大于或等于sem_op的絕對值。通常用于獲取資源的使用權(quán) 。
sem_flg:信號操作標(biāo)志,它的取值有兩種:IPC_NOWAIT和SEM_UNDO。
IPC_NOWAIT:對信號量的操作不能滿足時,semop()不會阻塞,而是立即返回,同時設(shè)定錯誤信息;
SEM_UNDO: 程序結(jié)束時(不管是正常還是不正常),保證信號值會被設(shè)定;
nsops:表示要操作信號量的個數(shù)。因為信號量是以集合的形式存在,所以第二個參數(shù)可以傳一個數(shù)組,同時對一個集合中的多個信號量進(jìn)行操作。
semop()調(diào)用之前的值。這樣做的目的在于避免程序在異常的情況下結(jié)束未將鎖定的資源解鎖(死鎖),造成資源永遠(yuǎn)鎖定。
3).int semctl(int semid,int semnum,int cmd,...);
semctl()在semid標(biāo)識的信號量集合上,或者該信號量集合上第semnum個信號量上執(zhí)行cmd指定的控制命令。根據(jù)cmd不同,這個函數(shù)有三個或四個參數(shù),當(dāng)有第四個參數(shù)時,第四個參數(shù)的類型是union。
union semun{
int val; //使用的值
struct semid_ds *buf; //IPC_STAT、IPC_SET使用緩存區(qū)
unsigned short *array; //GETALL、SETALL使用的緩存區(qū)
struct seminfo *__buf; //IPC_INFO(linux特有)使用緩存區(qū)
};
返回值:成功返回0,失敗返回-1;
semid:信號量集合的編號。
semnum:信號量在集合中的標(biāo)號。
4).信號量類似消息隊列也是隨內(nèi)核的,除非用命令才可以刪除該信號量
ipcs -s //查看創(chuàng)建的信號量集合的個數(shù)
ipcrm -s semid //刪除一個信號量集合
一個簡單的關(guān)于信號量的例子?
父進(jìn)程中打印BB,子進(jìn)程中打印AA。利用信號量機(jī)制使得AA和BB之間不出現(xiàn)亂序。此時的顯示器就是臨界資源,我們需要在父子進(jìn)程的臨界區(qū)進(jìn)行加鎖。
comm.h
#ifndef COMM_H
#define COMM_H
#include
#include
#include
#include
#include
#include
#include
#include
#define PATHNAME "."
#define PROJID 0x6666
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) */
};
int CreateSemSet(int num);//創(chuàng)建信號量
int GetSemSet(); //獲取信號量
int InitSem(int sem_id,int which);
int P(int sem_id,int which); //p操作
int V(int sem_id,int which); //v操作
int DestroySemSet(int sem_id);//銷毀信號量
#endif //COMM_H
comm.c
#include"comm.h"
static commSemSet(int num,int flag)
{
key_t key=ftok(PATHNAME,PROJID);
if(key == -1)
{
perror("ftok error");
exit(1);
}
int sem_id=semget(key,num,flag);
if(sem_id == -1)
{
perror("semget error");
exit(2);
}
return sem_id;
}
int CreateSemSet(int num)
{
return commSemSet(num,IPC_CREAT|IPC_EXCL|0666);
}
int InitSem(int sem_id,int which)
{
union semun un;
un.val=1;
int ret=semctl(sem_id,which,SETVAL,un);
if(ret < 0)
{
perror("semctl");
return -1;
}
return 0;
}
int GetSemSet()
{
return commSemSet(0,IPC_CREAT);
}
static int SemOp(int sem_id,int which,int op)
{
struct sembuf buf;
buf.sem_num=which;
buf.sem_op=op;
buf.sem_flg=0; //
int ret=semop(sem_id,&buf,1);
if(ret < 0)
{
perror("semop error");
return -1;
}
return 0;
}
int P(int sem_id,int which)
{
return SemOp(sem_id,which,-1);
}
int V(int sem_id,int which)
{
return SemOp(sem_id,which,1);
}
int DestroySemSet(int sem_id)
{
int ret=semctl(sem_id,0,IPC_RMID);
if(ret < 0)
{
perror("semctl error");
return -1;
}
return 0;
}
SemSet.c
#include"comm.h"
void testSemSet()
{
int sem_id=CreateSemSet(1); //創(chuàng)建信號量
InitSem(sem_id,0);
pid_t id=fork();
if(id < 0){
perror("fork error");
exit(1);
}
else if(id == 0){ //child,打印AA
printf("child is running,pid=%d,ppid=%dn",getpid(),getppid());
while(1)
{
P(sem_id,0); //p操作,信號量的值減1
printf("A");
usleep(10031);
fflush(stdout);
printf("A");
usleep(10021);
fflush(stdout);
V(sem_id,0); //v操作,信號量的值加1
}
}
else //father,打印BB
{
printf("father is running,pid=%d,ppid=%dn",getpid(),getppid());
while(1)
{
P(sem_id,0);
printf("B");
usleep(10051);
fflush(stdout);
printf("B");
usleep(10003);
fflush(stdout);
V(sem_id,0);
}
wait(NULL);
}
DestroySemSet(sem_id);
}
int main()
{
testSemSet();
return 0;
}
5.共享內(nèi)存(shm)
共享內(nèi)存的原理圖:
與共享內(nèi)存有關(guān)的函數(shù):
1). 創(chuàng)建共享內(nèi)存
#include
#include
int shmget(key_t key, size_t size, int shmflg);
返回值:成功返回共享內(nèi)存的id,失敗返回-1;
key:和上面介紹的信號量的semget函數(shù)的參數(shù)key一樣;
size:表示要申請的共享內(nèi)存的大小,一般是4k的整數(shù)倍;
flags:IPC_CREAT和IPC_EXCL一起使用,則創(chuàng)建一個新的共享內(nèi)存,否則返回-1。IPC_CREAT單獨使用時返回一個共享內(nèi)存,有就直接返回,沒有就創(chuàng)建。
2).掛接函數(shù)
void *shmat(int shmid);
返回值:返回這塊內(nèi)存的虛擬地址;
shmat的作用是將申請的共享內(nèi)存掛接在該進(jìn)程的頁表上,是將虛擬內(nèi)存和物理內(nèi)存相對應(yīng);
3).去掛接函數(shù)
int shmdt(const void *shmaddr);
返回值:失敗返回-1;
shmdt的作用是去掛接,將這塊共享內(nèi)存從頁表上剝離下來,去除兩者的映射關(guān)系;
shmaddr:表示這塊物理內(nèi)存的虛擬地址。
4).int shmctl(int shmid,int cmd,const void* addr);
shmctl用來設(shè)置共享內(nèi)存的屬性。當(dāng)cmd是IPC_RMID時可以用來刪除一塊共享內(nèi)存。
5).共享內(nèi)存類似消息隊列和信號量,它的生命周期也是隨內(nèi)核的,除非用命令才可以刪除該共享內(nèi)存;
ipcs -m //查看創(chuàng)建的共享內(nèi)存的個數(shù)
ipcrm -m shm_id //刪除共享內(nèi)存
一個簡單的關(guān)于共享內(nèi)存的例子:
利用共享內(nèi)存實現(xiàn)在serve這個進(jìn)程中向共享內(nèi)存中寫入數(shù)據(jù)A,從client讀出數(shù)據(jù)。
comm.h
#ifndef COMM
#define COMM
#include
#include
#include
#include
#include
#define PATHNAME "."
#define PROCID 0x6666
#define SIZE 4096*1
int CreatShm();
int GetShm();
//int AtShm();
//int DtShm();
int DestroyShm(int shm_id);
#endif
comm.c
#include"comm.h"
static int CommShm(int flag)
{
key_t key=ftok(PATHNAME,PROCID);
if(key < 0)
{
perror("ftok");
return -1;
}
int shm_id=shmget(key,SIZE,flag);
if(shm_id < 0)
{
perror("shmget");
return -2;
}
return shm_id;
}
int CreatShm()
{
return CommShm(IPC_CREAT|IPC_EXCL|0666);
}
int GetShm()
{
return CommShm(IPC_CREAT);
}
//int AtShm();
//int DtShm();
int DestroyShm(int shm_id)
{
int ret=shmctl(shm_id,IPC_RMID,NULL);
if(ret < 0)
{
perror("shmctl");
return -1;
}
return 0;
}
server.c
#include"comm.h"
void testserver()
{
int shm_id=CreatShm();
printf("shm_id=%dn",shm_id);
char *mem=(char *)shmat(shm_id,NULL,0);
while(1)
{
sleep(1);
printf("%sn",mem);
}
shmdt(mem);
DestroyShm(shm_id);
}
int main()
{
testserver();
return 0;
}
client.c
#include"comm.h"
void testclient()
{
int shm_id=GetShm();
char *mem=(char *)shmat(shm_id,NULL,0);
int index=0;
while(1)
{
sleep(1);
mem[index++]='A';
index %= (SIZE-1);
mem[index]='?';
}
shmdt(mem);
DestroyShm(shm_id);
}
int main()
{
testclient();
return 0;
}
共享內(nèi)存的特點:
共享內(nèi)存是這五種進(jìn)程間通信方式中效率最高的。但是因為共享內(nèi)存沒有提供相應(yīng)的互斥機(jī)制,所以一般共享內(nèi)存都和信號量配合起來使用。
為什仫共享內(nèi)存的方式比其他進(jìn)程間通信的方式效率高?
消息隊列,F(xiàn)IFO,管道的消息傳遞方式一般為 :
1).服務(wù)器獲取輸入的信息;
2).通過管道,消息隊列等寫入數(shù)據(jù)至內(nèi)存中,通常需要將該數(shù)據(jù)拷貝到內(nèi)核中;
3).客戶從內(nèi)核中將數(shù)據(jù)拷貝到自己的客戶端進(jìn)程中;
4).然后再從進(jìn)程中拷貝到輸出文件;
上述過程通常要經(jīng)過4次拷貝,才能完成文件的傳遞。
而共享內(nèi)存只需要:
1).輸入內(nèi)容到共享內(nèi)存區(qū)
2).從共享內(nèi)存輸出到文件
上述過程不涉及到內(nèi)核的拷貝,這些進(jìn)程間數(shù)據(jù)的傳遞就不再通過執(zhí)行任何進(jìn)入內(nèi)核的系統(tǒng)調(diào)用來傳遞彼此的數(shù)據(jù),節(jié)省了時間,所以共享內(nèi)存是這五種進(jìn)程間通信方式中效率最高的。
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7002瀏覽量
88938 -
通信
+關(guān)注
關(guān)注
18文章
6024瀏覽量
135949 -
緩沖
+關(guān)注
關(guān)注
0文章
52瀏覽量
17819 -
程序
+關(guān)注
關(guān)注
117文章
3785瀏覽量
81001
發(fā)布評論請先 登錄
相關(guān)推薦
評論