1.管道
對於具有公共祖先的進程,其管道是建立在3-4G的內核空間中的。每個進程各自有不同的用戶地址空間,任何一個進程的全局變量在另一個進程中都看不到,所以進程之間要交換數據必須通過內核,在內核中開辟一塊緩沖區,進程1把數據從用戶空間拷到內核緩沖區,進程2再從內核緩沖區把數據讀走,內核提供的這種機制稱為進程間通信(IPC,InterProcess Communication)。
調用pipe函數時在內核中開辟一塊緩沖區(稱為管道)用於通信,它有一個讀端一個寫端,然后通過filedes參數傳出給用戶程序兩個文件描述符,filedes[0]指向管道的讀端,filedes[1]指向管道的寫端(很好記,就像0是標准輸入1是標准輸出一樣)。所以管道在用戶程序看起來就像一個打開的文件,通過read(filedes[0]);或者write(filedes[1]);向這個文件讀寫數據其實是在讀寫內核緩沖區。pipe函數調用成功返回0,調用失敗返回-1。
#include <sys/wait.h> #include<stdio.h> #include<sys/types.h> #include<unistd.h> #include<stdlib.h> #include<string.h> int main(void) { pid_t pid; int fd[2]; if(pipe(fd)<0) { perror("pipe"); exit(1); } printf("fd[0]=%d, fd[1]= %d\n",fd[0],fd[1]); if((pid=fork())<0) { perror("fork"); exit(1); } else if(pid==0)//child { char c_str[1024]; int n; close(fd[1]);//關閉寫端口 n=read(fd[0],c_str,sizeof(c_str)/sizeof(c_str[0]));//由於不知道讀多少,所以讀取最大長度 close(fd[0]); write(STDOUT_FILENO,c_str,n); } else//parents { char str[]="hello pipe!\n"; sleep(2); close(fd[0]);//關閉讀端口 write(fd[1],str,strlen(str)); close(fd[1]); wait(NULL);//等待回收子進程資源 } return 0; }
在父進程沒有傳輸數據在管道中時,子進程中的read函數會阻塞等待。我們可以使用fcntl函數改變一個已經打開文件的屬性,如重新設置讀、寫、追加、非阻塞等標志。
#include<stdio.h> #include<sys/types.h> #include<sys/wait.h> #include<unistd.h> #include<stdlib.h> #include<string.h> #include<fcntl.h> #include<errno.h> int main(void) { pid_t pid; int fd[2]; if(pipe(fd)<0) { perror("pipe"); exit(1); } printf("fd[0]=%d, fd[1]= %d\n",fd[0],fd[1]); if((pid=fork())<0) { perror("fork"); exit(1); } else if(pid==0)//child { char c_str[1024]; int n,flags; flags=fcntl(fd[0],F_GETFL); flags |=O_NONBLOCK; if(fcntl(fd[0],F_SETFL,flags)==-1) { perror("fcntl"); exit(1); } close(fd[1]);//關閉寫端口 tryagain: n=read(fd[0],c_str,sizeof(c_str)/sizeof(c_str[0]));//由於不知道讀多少,所以讀取最大長度 if(n<0) { if(errno==EAGAIN) { write(STDOUT_FILENO,"try again...\n",13); sleep(1); goto tryagain; } perror("read"); exit(1); } close(fd[0]); write(STDOUT_FILENO,c_str,n); } else//parents { char str[]="hello pipe!\n"; sleep(2); close(fd[0]);//關閉讀端口 write(fd[1],str,strlen(str)); close(fd[1]); wait(NULL);//等待回收子進程資源 } return 0; }
此時,read已經不再是阻塞了。需要注意的是,使用管道技術,應該在fork之前創建管道。
2.FIFO
FIOF也被稱為命名管道。未命名的管道pipe只能在兩個有共同祖先的進程之間使用。但是通過FIFO,完全不相關的進程也能交換數據。
分別創建只讀和只寫文件fifo_r.c和fifo_r.c:
/*只讀:fifo_r.c*/ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <fcntl.h> #include <sys/stat.h> #include <string.h> void sys_err(const char *str, int exitno) { perror(str); exit(exitno); } int main(int argc, char *argv[]) { int fd, len; char buf[1024]; if (argc < 2) { printf("usage:%s fifoname\n",argv[0]); exit(1); } if(access(argv[1],F_OK)==-1) { if(mkfifo(argv[1],0775)==-1) { sys_err("mkfifo",1); } } printf("1\n"); fd = open(argv[1], O_RDONLY); if (fd < 0) sys_err("open", 1); printf("2\n"); len = read(fd, buf, sizeof(buf)); write(STDOUT_FILENO, buf, len); close(fd); return 0; }
/*只寫:fifo_w.c*/ #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <fcntl.h> #include <sys/stat.h> #include <string.h> void sys_err(const char *str, int exitno) { perror(str); exit(exitno); } int main(int argc, char *argv[]) { int fd; char buf[1024] = "hello nanmed pipe!\n"; if (argc < 2) { printf("usage:%s fifoname\n",argv[0]); exit(1); } if(access(argv[1],F_OK)==-1) { if(mkfifo(argv[1],0775)==-1) { sys_err("mkfifo",1); } } printf("1\n"); fd = open(argv[1], O_WRONLY); if (fd < 0) sys_err("open", 1); printf("2\n"); write(fd, buf, strlen(buf)); close(fd); return 0; }
先運行寫進程,此時程序阻塞在只寫打開的open函數,再ctrl+shift+n,打開新的終端,運行只讀進程:
此時讀進程正確讀取了寫進程的數據。反之,先執行讀取進程,讀進程也會阻塞在只讀open函數處,直到寫入數據。前提是沒有指定O_NONBLOCK標志。
FIFO和PIPE的最大數據量可以通過fpathconf函數得到:
在創建了FIFO或者PIPE之后使用:printf("FIFO_PIPE_BUF_SIZE = %ld\n",fpathconf(fd, _PC_PIPE_BUF));
可以發現,下ubuntu 16.04中,FIFO和PIPE的緩沖區大小為4096個字節。不同的系統版本,可能存在差異。
3.內存共享映射
sysconf(_SC_PAGESIZE)的返回值,在本文的ubuntu16.04中為4096字節。故off的值應該是4096的整數倍,通常該值設置為0。
現在,使用mmap實現一個復制指令:
1 #include <stdio.h> 2 #include <pthread.h> 3 #include <signal.h> 4 #include <stdlib.h> 5 #include <unistd.h> 6 #include <sys/time.h> 7 #include <sys/resource.h> 8 #include <sys/types.h> 9 #include <sys/stat.h> 10 #include <fcntl.h> 11 #include <syslog.h> 12 #include <string.h> 13 #include <sys/mman.h> 14 15 16 #define COPYINCR (1024*1024*1024) /* 1 GB */ 17 int main(int argc, char *argv[]) 18 { 19 int fdin, fdout; 20 void *src, *dst; 21 size_t copysz; 22 struct stat sbuf; 23 off_t fsz = 0; 24 if (argc != 3) 25 printf("usage: %s <fromfile> <tofile>", argv[0]); 26 if ((fdin = open(argv[1], O_RDONLY)) < 0) 27 printf("can’t open %s for reading", argv[1]); 28 if ((fdout = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0666)) < 0) 29 printf("can’t creat %s for writing", argv[2]); 30 if (fstat(fdin, &sbuf) < 0) /* need size of input file */ 31 printf("fstat error"); 32 if (ftruncate(fdout, sbuf.st_size) < 0) /* 文件字節數:sbuf.st_size ,set output file size */ 33 printf("ftruncate error"); 34 35 if ((sbuf.st_size - fsz) > COPYINCR) 36 copysz = COPYINCR; 37 else 38 copysz = sbuf.st_size - fsz; 39 if ((src = mmap(0, copysz, PROT_READ, MAP_SHARED, fdin, fsz)) == MAP_FAILED) 40 printf("mmap error for input"); 41 if ((dst = mmap(0, copysz, PROT_READ | PROT_WRITE,MAP_SHARED, fdout, fsz)) == MAP_FAILED) 42 printf("mmap error for output"); 43 44 memcpy(dst, src, copysz); /* does the file copy */ 45 46 munmap(src, copysz);//釋放內存 47 munmap(dst, copysz);//釋放內存 48 49 50 exit(0); 51 }
使用Vim打開對比,內容自然也是完全一致的:
這個例子相當於在磁盤的main.c映射一個地址空間到src(只讀),然后創建另一個文件,可讀可寫,通過前面映射的只讀地址空間,將其內容拷貝到此時創建的main.c.copy中。
這個思想可以應用在多進程的通信中。本文目前描述的情況,都是最簡單的場景,不存在進程間的競爭關系,如多個進程同時寫一個文件,此時則需要執行相應的處理方法,如信號量,互斥鎖等,這個在后面的隨筆中再介紹。
消息郵箱和socket的進程間通信方法,也將在后續隨筆中介紹。