[Linux]進程間通信
進程間通信
什么是進程間通信
進程之間具有獨立性,如果需要進行通信,就必須打破進程間的獨立性。進程通信需要提供一塊公共的能夠進行信息存儲和取出的空間。文件系統(tǒng)提供的我們稱為管道,操作系統(tǒng)提供的System V。
進程間通信的目的
- 數(shù)據(jù)傳輸:一個進程將數(shù)據(jù)發(fā)送給另一個進程。
- 資源共享:多個進程共享相同的資源。
- 事件通知:一個進程需要向另一個或一組進程發(fā)送消息。通知它們發(fā)生了某種事件,例如子進程終止時需要通知父進程。
- 進程控制:有的進程希望完全控制另一個進程的執(zhí)行,例如Debug進程。
進程間通信的本質(zhì)
進程間通信的本質(zhì)就是讓不同的進程看到同一份資源。
實際上就是構(gòu)建一個公共區(qū)域,供不同進程進行寫入或讀取數(shù)據(jù)。
為什么存在進程間通信
實際情況中,有時候我們需要多進程協(xié)作完成某種業(yè)務(wù)。
進程間通信的分類
- 管道
- 匿名管道
- 命名管道
- System V
- System V消息隊列
- System V信號量
- System V共享內(nèi)存
- POSIX
- 共享內(nèi)存
- 信號量
- 消息隊列
- 互斥量
- 條件變量
- 讀寫鎖
管道
什么是管道
- 管道是Unix中最古老的進程間通信的形式。
- 我們把從一個進程連接到另一個進程的數(shù)據(jù)流稱為一個“管道”。
其實我們早在之前命令中的學(xué)習(xí)就已經(jīng)見過了管道|
,我們也經(jīng)常使用管道命令。
例如,查看服務(wù)器連接人數(shù):
[---@VM-8-4-centos day04]$ who | wc -l
匿名管道
本質(zhì)
匿名管道主要用于父子間的通信。它本質(zhì)上就是讓父子進程看到同一個被打開的文件,然后讓父子進程進行寫入或讀取數(shù)據(jù),從而實現(xiàn)父子間的通信。
這里的文件是由操作系統(tǒng)提供的,所以在父進程或子進程寫入數(shù)據(jù)時,并不會發(fā)生寫時拷貝。
pipe
int pipe(int pipefd[2]);
- 頭文件:
#include<unistd.h>
- pidfd是一個輸出型參數(shù),pipfd[0]:管道讀端的文件描述符;pipfd[1]:管道寫端的文件描述符。
- 返回值:調(diào)用成功,返回0;調(diào)用失敗,返回-1。
pipe的使用
實際上,我們需要對一對父子進程關(guān)閉相反的兩個端口來使用匿名管道進行通信。
- 需要父進程讀,則關(guān)閉父進程的寫端和關(guān)閉子進程的讀端。
- 需要父進程寫,則關(guān)閉父進程的讀端和關(guān)閉子進程的寫端。
例如,父進程關(guān)閉寫端,子進程關(guān)閉讀端。
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int fd[2] = {0};
int n = pipe(fd);
if (n != 0)
perror("pipe fail");
pid_t id = fork();
int cnt = 0;
const char *str = "I am a child process. MYPID->";
if (id > 0)
{
close(fd[1]); // 父進程關(guān)閉寫端
while (1)
{
char buffer[1024];
ssize_t r = read(fd[0], buffer, sizeof(buffer) - 1);
if (r > 0)
{
buffer[r] = '\0';
cout << "parent process get message-> " << buffer << endl;
}
else if (r == 0)
{
// 讀完了
cout << endl;
cout << "數(shù)據(jù)讀取完畢!!!" << endl;
break;
}
else
{
perror("read fail");
return -1;
}
}
int wp = waitpid(-1, nullptr, 0);
close(fd[0]);
return 0;
}
else if (id == 0)
{
close(fd[0]); // 子進程關(guān)閉讀端
while (1)
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "Message: %s%d, count: %d", str, getpid(), cnt++);
write(fd[1], buffer, strlen(buffer));
sleep(1); // 每隔1秒寫入一次
if (cnt == 6)
break;
}
close(fd[1]);
exit(0);
}
else
{
perror("fork fail");
return -1;
}
return 0;
}
匿名管道讀寫情況
- 如果管道中沒有數(shù)據(jù),讀端進行讀取,就會阻塞當(dāng)前讀取的進程
- 如果寫端寫滿了,寫端還進行寫入,就會阻塞當(dāng)前寫端的進程
- 如果寫端關(guān)閉了,讀端讀完數(shù)據(jù)后就會返回0,正常退出
- 如果讀端關(guān)閉了,操作系統(tǒng)會向?qū)懚税l(fā)送13號信號
SIGPIPE
,從而讓寫端關(guān)閉
匿名管道的特征
- 匿名管道是半雙工通信的。
- 單工通信:數(shù)據(jù)傳輸在通信雙方是單向的,一方為固定發(fā)送端,另一方為固定接收端。
- 半雙工通信:數(shù)據(jù)傳輸在通信雙方是雙向的,但不能同時發(fā)送數(shù)據(jù)。
- 全雙工通信:數(shù)據(jù)傳輸在通信雙方是雙向的,允許同時發(fā)送數(shù)據(jù)。
- 管道的生命周期隨進程,進程退出,則管道釋放。
- 管道的本質(zhì)是通過一個文件進行通信的,當(dāng)打開這個文件的進程退出后,這個文件也會被釋放掉。
- 管道提供的是流式服務(wù)。
- 對于寫端寫入的數(shù)據(jù),讀端讀取的數(shù)據(jù)是任意的,這就是流式服務(wù)。
- 對于寫端寫入的數(shù)據(jù),讀端讀取時根據(jù)數(shù)據(jù)的分割(按一定的報文段)讀取,這就是與流式服務(wù)相對的數(shù)據(jù)報服務(wù)。
- 內(nèi)核對管道操作會進行同步和互斥。
- 同步:在特定的時間點或條件下,不同進程之間的操作按照一定的順序和速度進行,以保證它們之間的狀態(tài)和行為達到預(yù)期的一致性。
- 互斥:一個公共資源同一時刻只能被一個進程使用,多個進程不能同時使用公共資源。
對于管道來說,同步就是指這兩個進程不能同時對管道進行操作,但這兩個進程必須要按照某種次序來對管道進行操作;互斥就是兩個進程不可以同時對管道進行操作,它們會相互排斥,必須等一個進程操作完畢,另一個才能操作。
ps:管道的最大容量一般是65536字。
我們也可以通過以下命令查看:
[---@VM-8-4-centos day04]$ ulimit -a
命名管道
匿名管道通常只能用于父子進程間的通信,為了讓不具有親緣關(guān)系的進程相互通信,由此有了命名管道。
本質(zhì)
通過創(chuàng)建一個特殊的文件,讓兩個進程看到同一份資源,從而實現(xiàn)通信。
匿名管道和命名管道都是內(nèi)存文件,但是命名管道在磁盤上有一個特殊的映像。(大小為0,因為命名管道和匿名管道都不會刷新到磁盤上)
命令行創(chuàng)建命名管道
[---@VM-8-4-centos day04]$ mkfifo named_pipe
我們可以從第一個p看到出,這個文件類型是管道文件,管道文件大小默認(rèn)是0。
此時我們已經(jīng)可以進行通信了,我們使用兩個不同的命令行進行通信測試:
如果我們不進行讀取,寫端就會阻塞等待讀端讀取。
創(chuàng)建和刪除命名管道
int mkfifo(const char *pathname, mode_t mode);
-
pathname:命名管道創(chuàng)建路徑,若給出文件名,則創(chuàng)建在當(dāng)前路徑下;若給出路徑,按路徑創(chuàng)建
-
mode:管道文件默認(rèn)權(quán)限
-
返回值:創(chuàng)建成功,返回0;創(chuàng)建失敗,返回-1。
int main()
{
umask(0);
int n = mkfifo("named_pipe", 0666);
if(n < 0) perror("mkfifo fail");
//創(chuàng)建成功
cout << "mkfifo success..." << endl;
return 0;
}
int unlink(const char *path)
- path:管道路徑
- 返回值,創(chuàng)建成功,返回0;創(chuàng)建失敗,返回-1
int main()
{
int n = unlink("./named_pipe");
if(n == -1)
{
perror("unlink fail");
return -1;
}
cout << "unlink success ......." << endl;
return 0;
}
實現(xiàn)服務(wù)端與客戶端通信
com.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
using namespace std;
bool create_named_pipe()
{
umask(0);
int n = mkfifo("./named_pipe", 0600); // 創(chuàng)建管道文件
if (n != 0)
{
perror("mkfifo fail");
return false;
}
cout << "mkfifo success ......" << endl;
return true;
}
void unlink_named_pipe()
{
int n = unlink("./named_pipe");
if (n != 0)
perror("unlink fail");
cout << "unlink success ......" << endl;
}
serve.cc
#include "com.hpp"
int main()
{
bool flag = create_named_pipe();
if (flag == false)
{
perror("create_named_pipe fail");
exit(-1);
}
int fd = open("./named_pipe", O_RDONLY);
if (fd < 0)
perror("open fail");
char buffer[1024];
while (1)
{
ssize_t r = read(fd, buffer, sizeof(buffer) - 1);
if (r > 0)
{
buffer[r] = '\0';
cout << "serve get message -> " << buffer << endl;
}
else if (r == 0)
{
cout << "read end ......" << endl;
break;
}
else
{
perror("read fail");
return -1;
}
}
close(fd);
unlink_named_pipe();
return 0;
}
client.cc
#include "com.hpp"
int main()
{
int fd = open("./named_pipe", O_WRONLY);
if (fd < 0)
perror("open fail");
char buffer[1024];
while(1)
{
cout << "client Enter # ";
fgets(buffer, sizeof(buffer), stdin);//fgets剩一個空間會被系統(tǒng)填充'\0',不用-1
ssize_t w = write(fd, buffer, strlen(buffer));
if(w != strlen(buffer))
{
perror("write fail");
exit(-1);
}
}
close(fd);
return 0;
}
另外一提,命令行中的管道|
是匿名管道。
System V
之前我們提到過System V通信方式有:System V共享內(nèi)存、System V消息隊列、System V信號量,下面我們就著重說說System V共享內(nèi)存。
system V共享內(nèi)存
共享內(nèi)存的原理
用戶使用操作系統(tǒng)提供的接口在物理內(nèi)存中申請一塊資源,通過頁表將這段物理空間映射至進程地址空間,進程將這段虛擬地址的起始地址返回給用戶。
操作系統(tǒng)中的進程都可以通過共享內(nèi)存進行通信,一個操作系統(tǒng)可以有多個共享內(nèi)存。
共享內(nèi)存不止一個,操作系統(tǒng)必然對這些共享內(nèi)存也要進行管理,系統(tǒng)也為他維護了一個數(shù)據(jù)結(jié)構(gòu):
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
第一個成員shm_perm
的類型ipc_perm結(jié)構(gòu)是這樣的。(key也存在shm_perm中)
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
IPC資源的查看
我們使用以下命名,可以查看系統(tǒng)中的IPC資源:
[---@VM-8-4-centos day05]$ ipcs
ipcs
這個命令還有3個選項,分別來查看共享內(nèi)存、消息隊列和信號量。
-
-q
:僅顯示消息隊列的信息 -
-m
:僅顯示共享內(nèi)存的信息 -
-a
:僅顯示信號量的信息
共享內(nèi)存的創(chuàng)建和釋放
key_t ftok(const char *pathname, int proj_id);
- 作用:將一個存在且可獲取的路徑名pathname和一個整數(shù)標(biāo)識符proj_id轉(zhuǎn)換成一個key值,稱為IPC鍵值,方便共享內(nèi)存創(chuàng)建唯一標(biāo)識。
- 返回值:創(chuàng)建成功,返回key值;創(chuàng)建失敗,返回-1。
int shmget(key_t key, size_t size, int shmflg);
- key:形成唯一標(biāo)識,保證進程看到的是同一塊共享內(nèi)存。(使用
ftok
獲取) - size:創(chuàng)建共享內(nèi)存的大小。
- shmflg:共享內(nèi)存的創(chuàng)建方式。IPC_CREAT:共享內(nèi)存不存在,則創(chuàng)建,如果存在則獲取;IPC_EXCL:無法單獨使用,IPC_CREAT|IPC_EXCL:如果不存在就創(chuàng)建,如果存在就出錯返回。(記得設(shè)置權(quán)限0666等,例如
IPC_CREAT | IPC_EXCL | 0666
) - 返回值:創(chuàng)建成功,返回共享內(nèi)存標(biāo)識符;創(chuàng)建失敗,返回-1。
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
using namespace std;
int main()
{
key_t key = ftok("./makefile", 0x6666);
if(key < 0)
{
perror("ftok fail");
return -1;
}
int shm = shmget(key, 4096, IPC_CREAT | IPC_EXCL);
if(shm < 0)
{
perror("shmget fail");
return -2;
}
cout << key << endl << shm << endl;
return 0;
}
我們使用ipcs
命令來看一下,我們是否創(chuàng)建成功:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- shmid:表示所控制共享內(nèi)存的用戶級標(biāo)識符。
- cmd:表示具體的控制動作。IPC_RMID最為常用,表示刪除共享內(nèi)存。
- buf:用于獲取或設(shè)置所控制共享內(nèi)存的數(shù)據(jù)結(jié)構(gòu)。一般設(shè)置為nullptr
- 返回值:調(diào)用成功,返回0;調(diào)用失敗。返回-1。
int main()
{
key_t key = ftok("./makefile", 0x6666);
if (key < 0)
{
perror("ftok fail");
return -1;
}
int shm = shmget(key, 4096, IPC_CREAT | IPC_EXCL);
if (shm < 0)
{
perror("shmget fail");
return -2;
}
cout << key << endl << shm << endl;
cout << "create success..." << endl;
sleep(3);
int ctl = shmctl(shm, IPC_RMID, nullptr);
if(ctl < 0)
{
perror("shmctl fail");
return -3;
}
cout << "delete success..." <<endl;
return 0;
}
我們創(chuàng)建一塊共享內(nèi)存,讓程序休眠3秒。休眠后,刪除這塊共享內(nèi)存。(我們可以看見其中有3行打印了這塊共享內(nèi)存,第4行就沒有了,這也證明了我們代碼的邏輯是正確的)
右邊命令行使用以下的監(jiān)控腳本:
[wsj@VM-8-4-centos day05]$ while :; do ipcs -m;echo "###################################";sleep 1;done
我們也可以使用命令行來刪除共享內(nèi)存:
[wsj@VM-8-4-centos day05]$ ipcrm -m 6(數(shù)字代表自己共享內(nèi)存的shm)
共享內(nèi)存的關(guān)聯(lián)和去關(guān)聯(lián)
void *shmat(int shmid, const void *shmaddr, int shmflg);
- shmid:待關(guān)聯(lián)的共享內(nèi)存標(biāo)識符
- shmaddr:指定共享內(nèi)存映射到進程地址空間中的某一地址,一般設(shè)置
nullptr
讓內(nèi)核自己選擇。 - shmflg:SHM_RDONLY,表示關(guān)聯(lián)共享內(nèi)存后,僅進行讀取操作;SHM_RND,表示如果shmaddr不為空,則自動向下調(diào)整為SHMLBA的整數(shù)倍;0,默認(rèn)為讀寫權(quán)限。
- 返回值:調(diào)用成功,返回映射到進程地址空間的共享內(nèi)存的地址;調(diào)用失敗,返回(void*)-1。
int shmdt(const void *shmaddr);
-
shmaddr:待去關(guān)聯(lián)的共享內(nèi)存,使用shmat得到的地址。
-
返回值,調(diào)用成功,返回0;調(diào)用失敗,返回-1。
接下來,我們使用一段代碼加深一下對這些接口調(diào)用的理解:
int main()
{
key_t key = ftok("./makefile", 0x6666);
if (key < 0)
{
perror("ftok fail");
return -1;
}
int shm = shmget(key, 4096, IPC_CREAT | IPC_EXCL | 0666);
if (shm < 0)
{
perror("shmget fail");
return -2;
}
cout << "create success..." << endl;
cout << "-----------------------" << endl;
void* mem = shmat(shm, nullptr, 0);
if(mem == (void*)-1)
{
perror("shmat fail");
return -3;
}
cout << "attach success..." << endl;
cout << "-----------------------" << endl;
int dt = shmdt(mem);
if(dt < 0)
{
perror("shmdt fail");
return -4;
}
cout << "detach success ..." << endl;
cout << "-----------------------" << endl;
int ctl = shmctl(shm, IPC_RMID, nullptr);
if(ctl < 0)
{
perror("shmctl fail");
return -5;
}
cout << "delete success..." << endl;
return 0;
}
右邊的命令行,任然使用上面的監(jiān)控腳本。
我們從共享內(nèi)存,從無到有;從關(guān)聯(lián)數(shù),從0到1,再到0。證明我們代碼的邏輯是正確的。(創(chuàng)建共享內(nèi)存->關(guān)聯(lián)該共享內(nèi)存->去關(guān)聯(lián)該共享內(nèi)存->刪除共享內(nèi)存)
ps:創(chuàng)建共享內(nèi)存時需要設(shè)置權(quán)限,不然就無法正常關(guān)聯(lián)。
實現(xiàn)服務(wù)端與客戶端通信
com.hpp
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
using namespace std;
#define PATH "./makefile"
#define PROJ_ID 0X888
#define MAX_SIZE 4096
key_t getKey() // 獲取key值
{
key_t key = ftok(PATH, PROJ_ID);
if (key == -1)
{
perror("ftok fail");
exit(-1);
}
return key;
}
int getShm(key_t key, int flag) // 創(chuàng)建共享內(nèi)存,為下面兩個函數(shù)服務(wù)
{
int shm = shmget(key, MAX_SIZE, flag);
if (shm < 0)
{
perror("shmget fail");
exit(-2);
}
return shm;
}
int shmHelper(key_t key) // 獲取共享內(nèi)存(已創(chuàng)建的前提)
{
return getShm(key, IPC_CREAT);
}
int createShm(key_t key) // 創(chuàng)建共享內(nèi)存
{
return getShm(key, IPC_CREAT | IPC_EXCL | 0666);
}
void *attachShm(int shm) // 關(guān)聯(lián)
{
void *mem = shmat(shm, nullptr, 0);
if (mem == (void *)-1)
{
perror("shmat fail");
exit(-3);
}
return mem;
}
void detachShm(void *mem) // 去關(guān)聯(lián)
{
if (shmdt(mem) < 0)
{
perror("shmdt fail");
exit(-4);
}
}
int deleteShm(int shm) // 刪除共享內(nèi)存
{
if (shmctl(shm, IPC_RMID, nullptr) < 0)
{
perror("shmctl fail");
exit(-5);
}
}
serve.cc
#include "com.hpp"
int main()
{
int key = getKey();
int shm = createShm(key);
cout << shm << endl;
void *mem = attachShm(shm);
cout << mem << endl;
int cnt = 0;
while (cnt++ < 10)
{
printf("client # %s\n", mem);
struct shmid_ds ds;
shmctl(shm,IPC_STAT,&ds);
cout << "PID->" << getpid() << ", creator->" << ds.shm_cpid << ", key->" << ds.shm_perm.__key << endl;
sleep(1);
}
detachShm(mem);
deleteShm(shm);
return 0;
}
client.cc
#include "com.hpp"
int main()
{
int key = getKey();
int shm = shmHelper(key);
cout << shm << endl;
void *mem = attachShm(shm);
cout << mem << endl;
int cnt = 0;
char *message = "Hello, I`m client";
while (1)
{
snprintf((char *)mem, MAX_SIZE, "PID->%d : %s, count : %d\n", getpid(), message, ++cnt);
sleep(1);
}
detachShm(mem);
return 0;
}
文章來源:http://www.zghlxwxcb.cn/news/detail-616712.html
共享內(nèi)存是所有通信中最快的通信方式,因為它沒有緩沖區(qū),能大大減少通信數(shù)據(jù)的拷貝次數(shù)。文章來源地址http://www.zghlxwxcb.cn/news/detail-616712.html
到了這里,關(guān)于[Linux]進程間通信的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!