進程間通信的方式?
- - 管道
- - 匿名管道
- - 有名管道
- - 內存映射
- - 本地套接字
- - 網絡套接字
- - 消息隊列
- - 共享內存
- 父子進程始終共享什麼東西?
- - 文件描述符
- - 內存映射區
目錄
一、管道
管道的本質:
- 是內核緩衝區
- 擁有文件的特質(讀操作、寫操作)
- 匿名管道 -> 沒有文件的實體
- 有名管道 -> 有文件實體, 不存儲數據
- 可以文件操作的方式對管道進行處理
在創建子進程之前, 父進程通過文件操作打開了兩個文件 a, b, 得到了兩個文件描述符: fd3 fd4
父進程通過fork 創建子進程
子進程對應一個虛擬地址空間
- 文件描述符表: fd3, fd4 -> 從父進程拷貝過來的
- 在子進程中通過得到的fd3 fd4 來操作 A, B文件
1.1匿名管道
創建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]); char*
參數:
pipefd: 傳出參數
- pipefd[0] -> 管道的讀端
- pipefd[1] -> 管道的寫端
返回值:
0: 調用成功
-1: 失敗
匿名管道的原理:
- 沒有名字, 在磁盤上沒有實體, 是內存中的一塊緩衝區
- 這個緩衝區, 由父進程在fork()子進程之前創建得到的
- 內核緩衝區有兩部分
- 讀端 -> 可以進行讀操作的文件描述符
- 寫端 -> 可以進行寫操作的文件描述符 - 父進程被銷燬, 管道自動釋放
- 默認阻塞
匿名管道數據結構是一個環形隊列,默認容量4k
實現過程:
- - 讀操作
- - 如果管道中有數據
- - read讀, read返回值是讀到的字節數
- - 管道中沒有數據
- - 寫端沒有關閉, 但是寫的慢,read會阻塞
- - 寫端關閉了,read解除阻塞, 返回值爲0
- - 如果管道中有數據
- - 寫數據
- - 管道有空間。接着寫, 寫滿之後阻塞
- - 沒有空間。直接阻塞
- - 寫數據的時候, 讀端關閉了
- - 管道破裂
- - 進程被 SIGPIPE 信號殺死
管道的讀寫兩端都默認阻塞,如何設置爲非阻塞呢?
// 使用 fcntl 函數
// 設置讀端爲非阻塞 -> fd[0]
int flag = fcntl(fd[0], F_GETFL)
flag |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flag);
匿名管道的侷限性:
- 管道中的數據只能被讀一次
- 半雙工,數據是單向流動的
- 只能進程有血緣關係的進程通信
- 在磁盤上沒有實體
- 被一個進程創建出來的
- 得到兩個fd(一讀一寫)
栗子:(使用匿名管道實現進程間通信)
流程:
父子進程間通信, 實現 ps aux
子進程 -> ps aux
子進程得到的結果, 給到父進程
涉及到有血緣關係的進程通信 -> pipe
創建進程 -> fork()
子進程做的事兒: execlp(), 執行命令數據會默認寫到終端
- 重定向: stdout_fileno -> 管道的寫端 dup2
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int fd[2];
// 創建匿名管道
int ret = pipe(fd);
if(ret == -1)
{
perror("pipe");
exit(0);
}
// 創建子進程
pid_t pid = fork();
if(pid > 0)
{
// 父進程, 讀管道
char buf[1024];
while(1)
{
read(fd[0], buf, sizeof(buf));
printf("%s", buf);
}
wait(NULL);
}
else if(pid == 0)
{
// 子進程, 寫管道
// 文件描述符重定向 stdout_fileno -> fd[1]
dup2(fd[1], STDOUT_FILENO);
// 執行shell命令 ps aux
execlp("ps", "ps", "aux", NULL);
perror("execlp");
exit(0);
}
else
{
perror("fork");
exit(0);
}
return 0;
2.2 有名管道
有名管道的原理:
- - 在磁盤上有一個文件 -> 僞文件
- - 通過這個僞文件給不同 的進程搭建一個橋樑 -> 找到同一塊內核緩衝區
- - 這個磁盤文件大小, 永遠爲0
- - 內核緩衝區 -> 環形隊列實現的
- - 數據只能被讀一次
- - 默認也是阻塞的
- - 實現沒有血緣關係的進程通信
創建方式:
# 通過命令創建
mkfifo 名字
# 通過函數創建
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
參數:
- pathname: 創建的管道文件對應路徑和名字
- mode: 用戶對管道文件的操作權限, 八進制的數, 最終權限: (mode & ~umask)
創建栗子:(2個無關的進程通信)
wirte文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 創建有名管道
int ret = mkfifo("test", 0664);
if(ret == -1)
{
perror("mkfifo");
exit(0);
}
// 打開管道文件
int fd = open("test", O_WRONLY);
// 寫數據
for(int i=0; i<100; ++i)
{
char buf[1024];
sprintf(buf, "hello, %d\n", i);
write(fd, buf, strlen(buf)+1);
sleep(2);
}
// 關閉文件
close(fd);
return 0;
}
read文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
int main()
{
// 打開管道文件
int fd = open("test", O_RDONLY);
// 寫數據
while(1)
{
char buf[1024];
read(fd, buf, sizeof(buf));
printf("recv buf: %s\n", buf);
}
// 關閉文件
close(fd);
return 0;
}
二、內存映射
將磁盤文件的數據映射到內存, 用戶通過修改內存就能修改磁盤文件
函數原型:
- mmap 得到映射內存在共享庫加載的區域(虛擬地址空間)
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
參數:
- addr: NULL, 由內核指定
- length: 要映射的數據的長度, 這個值不能爲0
- prot: 對申請的內存映射區的操作權限
- PROT_READ: 讀權限, 這個權限必須要有
- PROT_WRITE: 寫權限
- PROT_READ | PROT_WRITE: 讀寫權限
- flags:
- MAP_SHARED: 映射區數據會自動和磁盤文件進行同步, 進程間通信必須設置這個選項
- MAP_PRIVATE: 不同步
- fd: 文件描述符
- 通過open得到的, open的是一個磁盤文件
- 文件大小不能爲0 (新文件要用lseek擴展)
- open的時候需要指定flags
- 這個權限要和prot參數值對應
- prot:PROT_READ , flag=只讀/讀寫
- prot:PROT_READ | PROT_WRITE , flag=讀寫
- offset: 偏移量, 一定得的4k的整數倍, 0是可以的
返回值:
成功: 指向映射區起始位置的指針
失敗: MAP_FAILED (that is,(void *) -1)
- munmap 釋放申請的內存映射區
int munmap(void *addr, size_t length);
參數:
- addr: mmap的返回值
- length: 和mmap函數的第二個參數相同即可
父子進程使用內存映射通信的栗子:
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main()
{
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END);
// 創建內存映射區
void* ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
// 創建子進程
pid_t pid = fork();
if(pid > 0)
{
// 父進程
// 寫內存
strcpy((char*)ptr, "你是我兒子嗎?");
}
else if(pid == 0)
{
// 子進程
// 讀內存
char buf[64];
strcpy(buf, (char*)ptr);
printf("read data: %s\n", buf);
}
// 關閉內存映射區
munmap(ptr, size);
return 0;
}
無關係進程使用內存映射通信的栗子:
wirte文件
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main()
{
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END);
// 創建內存映射區
void* ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
strcpy((char*)ptr, "你是我兒子嗎===================xxxxxxxxxxxxxx?");
// 關閉內存映射區
munmap(ptr, size);
return 0;
}
read文件
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main()
{
int fd = open("test.txt", O_RDWR);
int size = lseek(fd, 0, SEEK_END);
// 創建內存映射區
void* ptr = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(-1);
}
// 讀內存
char buf[64];
strcpy(buf, (char*)ptr);
printf("read data: %s\n", buf);
// 關閉內存映射區
munmap(ptr, size);
return 0;
}
三、總結
3.1 進程間通信
- 有血緣關係的進程通信
- 還沒有子進程的時候。通過唯一的父進程, 先創建內存映射區,內存映射區有了之後, 創建子進程,父子進程共享創建的內存映射區
- 無血緣關係的進程通信
- 準備一個大小非0的磁盤文件
- 進程1 通過磁盤文件創建內存映射區得到一個操作這塊內存的指針(讀或者寫)
- 進程2 通過磁盤文件創建內存映射區得到一個操作這塊內存的指針(讀或者寫)
- 使用內存映射區通信, 不阻塞
3.2 面試問題
1. 如果對mmap的返回值(ptr)做++操作(ptr++), munmap是否能夠成功?
void* ptr = mmap();
ptr++;
munmap(ptr, len); // 錯誤
映射區的起始地址必須要保留下來, 用於映射區的釋放
2. 如果open時O_RDONLY, mmap時prot參數指定PROT_READ | PROT_WRITE會怎樣?
mmap返回 MAP_FAILED
prot參數指定PROT_READ | PROT_WRITE
int fd = open("xxx", O_RDWR);
3. 如果文件偏移量爲1000會怎樣?
mmap調用失敗, 返回: MAP_FAILED
4. mmap什麼情況下會調用失敗?
- 第二個參數: length == 0
- 第三個參數: prot
- 指定了寫權限
- prot:PROT_READ | PROT_WRITE,
第5個參數fd通open打開文件的時候指定了 O_RDONY/O_WRONLY
5. 可以open的時候O_CREAT一個新文件來創建映射區嗎?
- 創建的新文件大小肯定爲0, 這是不行的
- 可以對新得到的文件進行拓展
- lseek(fd, 拓展的長度, SEEK_END);然後對文件進行一次寫操作: write(fd, " ", 1);
- int truncate(const char *path, off_t length);
- int ftruncate(int fd, off_t length);
6. mmap後關閉文件描述符,對mmap映射有沒有影響?
int fd = open("xxx");
mmap(,,,,fd,0);
close(fd);
映射區還存在, 創建映射區使用的fd被關閉了
7. 對ptr越界操作會怎樣?
void* ptr = mmap(NULL, 100,,,,);
映射區的最新單位是4k
越界 == 操作非法內存 -> 段錯誤