在前面章節中講解了進程的派生和常見調用,可是進程之間通訊的惟一途徑就是經過打開的文件,或者是使用進程以前信號傳輸,因爲這些技術的侷限性,Unix系統提供了一種進程間通訊模型(IPC)。
IPC是進程通訊各類方式的統稱,目前只有一些經典的IPC方式能作到移植使用:管道、FIFO、消息隊列、信號量、共享存儲。還有基於套接字技術的網絡IPC。javascript
管道是很古老的進程間通訊機制了,基本全部的Unix系統或者非Unix系統都支持這種方式,管道有如下特性:java
雖然具備侷限性,可是因爲它的可移植性,因此目前仍然是首選的進程間通訊技術,管道在shell中很是常見,咱們經常使用如下命令shell
> command1 | command2 ... commandn複製代碼
shell使用管道將前一個進程的標準輸出與後一條命令的標準輸入相鏈接。
開發者調用pipe函數建立管道數組
int pipe(int fildes[2]);複製代碼
filedes參數包含了兩個文件描述符,Unix手冊上這麼描述The first descriptor connects to the read end of the pipe; the second connects to the write end.
,也就是說,filedes[0]是讀的一端,filedes[1]是寫入的一端,這就是一個半雙工的管道,而後能夠經過fork的方式將文件描述符分給父子進程,從而實現通訊。在fork之後,雙方都持有讀寫的端口,而父子進程能夠關閉其中兩個,每一個進程只持有一個,從而作到真正的管道。管道有如下特色:安全
咱們實際上能夠將管道理解爲一塊內核維護的緩衝區,因此常量PIPE_BUF規定了管道的大小。服務器
#include "include/apue.h"
int main(int argc, char *argv[]) {
int n;
int fd[2];
pid_t pid;
char line[MAXLINE];
if (pipe(fd) < 0)
err_sys("pipe error");
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0) {
close(fd[0]);
write(fd[1], "hello world\n", 12);
} else {
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
exit(0);
}複製代碼
上面就是很是簡單的父子進程使用同一個管道通訊的小實例。網絡
除了上面的pipe函數之外,更常見的作法是建立一個鏈接到另外一個進程的管道,而後讀其輸出或者向其輸出端發送數據,爲此標準IO庫提供了popen和pclose函數數據結構
FILE *popen(const char *command, const char *mode);
int pclose(FILE *stream);複製代碼
咱們能夠看到,這兩個函數是標準C庫提供的函數,而且返回的是FILE結構體的指針,而且這兩個函數的實現是:建立一個管道,fork一個子進程,關閉管道端,exec一個shell運行命令,而後等待命令終止。
popen函數的type參數指示返回的文件指針鏈接的類型,若是type是"r",則文件指針鏈接到cmdstring的標準輸出。若是type是"w",則文件指針鏈接到cmdstring的標準輸入。
pclose函數關閉標準IO流,而且等待命令終止,最後返回shell的終止狀態架構
在平常Unix使用中,一般咱們會使用管道鏈接多個命令,當一個程序標準輸入輸出都鏈接到管道的時候,這就是協同進程。
下面是一個父子進程實現的協同進程函數
#include "include/apue.h"
int main(int argc, char *argv[]) {
int n, int1, int2;
char line[MAXLINE];
while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0) {
line[n] = 0;
if (sscanf(line, "%d%d", &int1, &int2) == 2) {
sprintf(line, "%d\n", int1 + int2);
n = strlen(line);
if (write(STDOUT_FILENO, line, n) != n)
err_sys("write error");
} else {
if (write(STDOUT_FILENO, "invalid args\n", 13) != 13)
err_sys("write error");
}
}
exit(0);
}複製代碼
上面的代碼很是簡單,就是標準輸入讀取,計算後輸出到標準輸出,將其編譯爲add2程序。
#include "include/apue.h"
static void sig_pipe(int);
int main(int argc, char *argv[]) {
int n, fd1[2], fd2[2];
pid_t pid;
char line[MAXLINE];
if (signal(SIGPIPE, sig_pipe) == SIG_ERR)
err_sys("signal error");
if (pipe(fd1) < 0 || pipe(fd2) < 0)
err_sys("pipe error");
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid > 0) {
close(fd1[0]);
close(fd2[1]);
while (fgets(line, MAXLINE, stdin) != NULL) {
n = strlen(line);
if (write(fd1[1], line, n) != n)
err_sys("write error to pipe");
if ((n = read(fd2[0], line, MAXLINE)) < 0)
err_sys("read error from pipe");
if (n == 0) {
err_msg("child closed pipe");
break;
}
line[n] = 0;
if (fputs(line, stdout) == EOF)
err_sys("fputs error");
}
if (ferror(stdin))
err_sys("fgets error on stdin");
exit(0);
} else {
close(fd1[1]);
close(fd2[0]);
if (fd1[0] != STDIN_FILENO) {
if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
err_sys("dup2 error to stdin");
close(fd1[0]);
}
if (fd2[1] != STDOUT_FILENO) {
if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
err_sys("dup2 error to stdout");
close(fd2[1]);
}
if (execl("./add2", "add2", (char *)0) < 0)
err_sys("execl error");
}
exit(0);
}複製代碼
程序建立了兩個管道,父子進程各自關閉不須要使用的管道。而後使用dup2函數將其移到標準輸入輸出,最後調用execl。
FIFO就是First In First Out
先進先出隊列,也被稱爲命名管道。未命名的管道只能在兩個相關進程使用,而命名管道就能全局使用。
建立一個FIFO就等同於建立一個文件,在前面的時候就講過,FIFO就是一種文件,而且在stat文件結構中就有st_mode成員編碼能夠知道是不是FIFO。
int mkfifo(const char *path, mode_t mode);複製代碼
還有一個mkfifoat函數在某些系統中是不可用的,其實就是一個文件描述符版本的mkfifo函數,基本同樣,其中mode參數和open函數中mode參數相同。新的FIFO用戶和組的全部權規則和前面章節中講述的同樣。
當建立完FIFO文件後,須要使用open函數打開,由於這確實是一個文件,幾乎全部的正常文件IO函數都能操做FIFO。
非阻塞標誌(O_NONBLOCK
)對FIFO會有如下影響:
和管道相似,若是write一個沒有進程讀打開的FIFO則會產生SIGPIPE信號。若是讀完全部數據,read函數會返回文件結束。
因爲這是一個文件,因此多個進程讀寫是很是正常的事情,因此爲了保證讀寫安全,原子寫操做是必需要考慮的。FIFO有如下用途:
這裏就再也不講實例了。須要的朋友自行尋找代碼。
在IPC中有三種被稱爲XSI IPC,他們有不少共同點。這裏先講解共同點。
XSI IPC在內核中存在着IPC結構,它們都用一個非負整數做爲標識符,這點很像文件描述符,可是文件描述符永遠是當前最小的開始,好比,第一個文件描述符必然是從3開始,而後這個文件描述符刪除後,再次打開一個文件,文件描述符仍然是3,而IPC結構則不會減小,會變成4,而後不斷增長直到整數的最大值,而後又迴轉到0。
標識符是IPC結構的內部名稱,爲了能全局使用,須要有一個鍵做爲外部名稱,不管什麼時候建立IPC結構,都應當指定一個鍵名,這個鍵的數據結構是基本系統數據類型key_t
。而且有不少種方法使客戶進程和服務器進程在同一個IPC結構匯聚
key_t ftok(const char *path, int id);複製代碼
path參數必須是一個現有的文件,當產生鍵的時候,只會使用id參數低八位。
ftok建立鍵通常依據以下行爲:首先根據給定的path參數得到對應文件的stat結構中st_dev和st_ino字段,而後將他們和項目ID組合。
每一個IPC結構都關聯了一個ipc_perm結構,這個結構體關聯了權限和全部者。
struct ipc_perm
{
uid_t uid; /* [XSI] Owner's user ID */
gid_t gid; /* [XSI] Owner's group ID */
uid_t cuid; /* [XSI] Creator's user ID */
gid_t cgid; /* [XSI] Creator's group ID */
mode_t mode; /* [XSI] Read/write permission */
unsigned short _seq; /* Reserved for internal use */
key_t _key; /* Reserved for internal use */
};複製代碼
上面是蘋果平臺的結構體內容,通常來講,都會有uid、gid、cuid、cgi、mode這些基本的內容,而其餘則是各個實現自由發揮。在建立的時候,這些字段都會被賦值,然後,若是想要修改這些字段,則必須是保證具備root權限或者是建立者。
XSI IPC一個問題就是:IPC結構是在系統範圍內使用的,可是卻沒有引用計數。若是進程使用完可是沒有對其進行刪除就終止了,那就會致使IPC依然在系統中存在,而管道有引用計數,等最後一個引用管道的進程終止便會自動回收。FIFO就算沒有刪除,可是等最後一個引用FIFO的進程終止,裏面的數據已經被刪除了。
XSI IPC還有一個問題就是它不是文件,咱們不能使用ls和rm等文件操做函數或者命令處理它們,它們也沒有文件描述符,這就限制了它們的使用,而且若是須要使用還得攜帶一大堆額外的API。
消息隊列,正如其名稱同樣,是消息的鏈表形式,它由內核存儲維護。而且和XSI IPC結構同樣,由消息隊列標識符標識。
msgget函數建立一個隊列或者打開一個現有隊列,msgsnd將新數據添加到消息末尾,每一個消息包含一個正的長整形字段、一個非負的長度以及實際數據字節數。msgrcv從隊列中得到消息。
struct __msqid_ds {
struct __ipc_perm_new msg_perm; /* [XSI] msg queue permissions */
__int32_t msg_first; /* RESERVED: kernel use only */
__int32_t msg_last; /* RESERVED: kernel use only */
msglen_t msg_cbytes; /* # of bytes on the queue */
msgqnum_t msg_qnum; /* [XSI] number of msgs on the queue */
msglen_t msg_qbytes; /* [XSI] max bytes on the queue */
pid_t msg_lspid; /* [XSI] pid of last msgsnd() */
pid_t msg_lrpid; /* [XSI] pid of last msgrcv() */
time_t msg_stime; /* [XSI] time of last msgsnd() */
__int32_t msg_pad1; /* RESERVED: DO NOT USE */
time_t msg_rtime; /* [XSI] time of last msgrcv() */
__int32_t msg_pad2; /* RESERVED: DO NOT USE */
time_t msg_ctime; /* [XSI] time of last msgctl() */
__int32_t msg_pad3; /* RESERVED: DO NOT USE */
__int32_t msg_pad4[4]; /* RESERVED: DO NOT USE */
}複製代碼
每一個系統實現都會在SUS標準的基礎上增長本身私有的字段,因此可能和上面的有所區別。這個結構體定義了隊列的當前狀態。
int msgget(key_t key, int flag);複製代碼
msgget根據key得到已有隊列或者新隊列。
int msgctl(int msqid, int cmd, struct msqid_ds *buf);複製代碼
msgctl用於對隊列進行多種操做,其中,msqid則是消息隊列ID,cmd參數是命令參數,能夠取如下值:
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);複製代碼
這個函數很好理解,就是把ptr指針對應的消息放入消息隊列中,flag的值能夠指定爲IPC_NOWAIT,這點相似於文件IO非阻塞標誌。
ssize_t msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flag);複製代碼
和msgsnd同樣,這個ptr參數指向一個長整形數,隨後跟隨的是存儲實際區域的緩衝區。nbytes指定數據緩衝區的長度。參數type則能夠指定想要哪一種消息。
信號量是一個計數器,針對多個進程提供對共享數據對象的訪問。信號量的使用主要是用來保護共享資源的,使得資源在一個時刻只會被一個進程(線程)擁有。
信號量的使用以下:
內核爲每一個信號量集合維護着一個semid_ds結構體,根據每一個系統實現不一樣會有不一樣的字段,這裏就不列出了。當咱們想要使用信號量的時候,使用以下函數
int semget(key_t key, int nsems, int semflg);複製代碼
咱們知道,XSI IPC實際上具備其共性,因此如同消息隊列同樣,這裏將key變換爲標識符的規則也是同樣的。
int semctl(int semid, int semnum, int cmd, ...);複製代碼
就如同是前面的ioctl等函數同樣,這個函數也是用於控制信號量,其中第四個參數是可選的,取決於cmd參數。
IPC_STAT Fetch the semaphore set's struct semid_ds, storing it in the memory pointed to by arg.buf.
IPC_SET Changes the sem_perm.uid, sem_perm.gid, and sem_perm.mode members of the semaphore set's struct semid_ds to match those of the struct pointed to
by arg.buf. The calling process's effective uid must match either sem_perm.uid or sem_perm.cuid, or it must have superuser privileges.
IPC_RMID Immediately removes the semaphore set from the system. The calling process's effective uid must equal the semaphore set's sem_perm.uid or
sem_perm.cuid, or the process must have superuser privileges.
GETVAL Return the value of semaphore number semnum.
SETVAL Set the value of semaphore number semnum to arg.val. Outstanding adjust on exit values for this semaphore in any process are cleared.
GETPID Return the pid of the last process to perform an operation on semaphore number semnum.
GETNCNT Return the number of processes waiting for semaphore number semnum's value to become greater than its current value.
GETZCNT Return the number of processes waiting for semaphore number semnum's value to become 0.
GETALL Fetch the value of all of the semaphores in the set into the array pointed to by arg.array.
SETALL Set the values of all of the semaphores in the set to the values in the array pointed to by arg.array. Outstanding adjust on exit values for
all semaphores in this set, in any process are cleared.複製代碼
上面就是cmd可選值,除了GETALL之外的全部命令,semctl都返回相應值,除此之外,還有個semop函數自動執行信號量集合上的操做數組
int semop(int semid, struct sembuf *sops, size_t nsops);複製代碼
sembuf參數是一個指針,指向了sembuf結構體,實際上這是一個數組,
共享存儲是一項頗有用的技術,它容許兩個或者更多的進程共享同一個給定存儲區,因爲數據不須要複製,因此這是一種最快的IPC方式,共享存儲最重要的就是資源的競爭,因此信號量通常用於共享存儲訪問。
在前面的章節中,咱們看到了一種共享存儲的方式,就是內存映射技術,可是相比存儲映射,共享存儲不須要建立中間文件。
int shmget(key_t key, size_t size, int shmflg);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);複製代碼
就如同是其餘的XSI IPC同樣,這裏也是一個建立函數,經過這個函數得到存儲標識符。而後就是shmctl函數,具體的使用直接查手冊,基本上都是差很少的。
當建立完成共享存儲段後,使用shmat將其連接到本身的地址空間中。
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);複製代碼
實際上,因爲架構的不一樣和平臺不一樣,爲了保持可移植性,咱們不該當去指定共享存儲段的地址,而是由系統自行分配,最終返回的就是共享存儲段的地址,當操做完成後,咱們須要使用shmdt函數將其分離。
POSIX信號量是三種IPC機制之一,相比XSI標準規定的IPC方式,POSIX的方式更加簡潔好用。
POSIX信號量有兩種類型:命名的和未命名的,他們二者的區別就像是命名管道和未命名管道同樣,有了標識符的信號量就能全局使用,而沒有標識符的信號量只能在同一內存區域內使用。
sem_t *sem_open(const char *name, int oflag, ...);
The parameters "mode_t mode" and "unsigned int value" are optional.
The value of oflag is formed by or'ing the following values:
O_CREAT create the semaphore if it does not exist
O_EXCL error if create and semaphore exists複製代碼
實際上這個函數技能建立也能使用現有信號量,上面的是Unix手冊節選的內容,應該算是至關清楚了,當函數返回的時候,sem_open會返回一個指針,讓咱們傳遞到其餘的函數上,等一切結束,使用sem_close
關閉信號量指針。
int sem_close(sem_t *sem);複製代碼
固然,也能使用sem_unlink函數銷燬一個命名信號量。
int sem_unlink(const char *name);複製代碼
在這裏咱們能看出來,POSIX信號量的命名信號量和文件很像。不像XSI信號量,POSIX信號量的值只能經過一個函數調用來調節,也就是sem_wait函數
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);複製代碼
後一個是sem_wait函數的嘗試版本,還有一個是超時版本,可是蘋果下好像不存在,建議少使用。
還能夠調用sem_post函數使信號量值+1,這個解鎖一個二進制信號量或者釋放一個技術信號量資源的過程是很像的。
int sem_post(sem_t *sem);複製代碼
對於未命名信號量,只能使用在單進程或者單線程中,是很是容易的,咱們只須要使用sem_init函數建立,而後使用sem_destroy函數銷燬。這裏就不在贅述。