很久沒更新了,今天主要說一下Linux的進程通訊,後續Linux方面的更新應該會變緩,由於最近在看Java和安卓方面的知識,後續會根據學習成果不斷分享更新Java和安卓的方面的知識~html
Linux進程通訊的知識,建議參照《UNIX環境高級編程》這本書,這裏也只是作一個總結:linux
一.線程:進程中的子線程之間的通訊,線程之間的內存(變量)是共享的,經過共享內存也就是全局變量便可,注意互斥便可程序員
二.進程:進程之間的通訊必需要藉助內核實現:shell
一、pipe:編程
(無名)管道,只能用於父子進程間通訊:單向的(一端寫入一端讀出),fork出來的就是子進程,對應的操做fock爲父進程windows
建立管道的數組是兩個大小,一個用於寫同時關閉讀的,一個用於讀同時關閉寫,兩個整數一個給寫的用,一個給讀的用數組
示例代碼:服務器
無名管道由pipe()函數建立:網絡
#include <unistd.h>異步
int pipe(int filedis[2]);
參數filedis返回兩個文件描述符:filedes[0]爲讀而打開,filedes[1]爲寫而打開。filedes[1]的輸出是filedes[0]的輸入。下面的例子示範瞭如何在父進程和子進程間實現通訊。
#define INPUT 0
#define OUTPUT 1
void main() {
int file_descriptors[2];
/*定義子進程號 */
pid_t pid;
char buf[256];
int returned_count;
/*建立無名管道*/
pipe(file_descriptors);
/*建立子進程*/
if((pid = fork()) == -1) {
printf("Error in fork/n");
exit(1);
}
/*執行子進程*/
if(pid == 0) {
printf("in the spawned (child) process.../n");
/*子進程向父進程寫數據,關閉管道的讀端*/
close(file_descriptors[INPUT]);
write(file_descriptors[OUTPUT], "test data", strlen("test data"));
exit(0);
} else {
/*執行父進程*/
printf("in the spawning (parent) process.../n");
/*父進程從管道讀取子進程寫的數據,關閉管道的寫端*/
close(file_descriptors[OUTPUT]);
returned_count = read(file_descriptors[INPUT], buf, sizeof(buf));
printf("%d bytes of data received from spawned process: %s/n",
returned_count, buf);
}
}
二、named pipe:
(有名)管道:適用於無親緣關係的進程,須要建立FIFO文件(有名字的文件,用於通訊的文件)
在Linux系統下,有名管道可由兩種方式建立:命令行方式mknod系統調用和函數mkfifo。下面的兩種途徑都在當前目錄下生成了一個名爲myfifo的有名管道:
方式一:mkfifo("myfifo","rw");
方式二:mknod myfifo p
生成了有名管道後,就可使用通常的文件I/O函數如open、close、read、write等來對它進行操做。下面便是一個簡單的例子,假設咱們已經建立了一個名爲myfifo的有名管道。
/* 進程一:讀有名管道*/
#include <stdio.h>
#include <unistd.h>
void main() {
FILE * in_file;
int count = 1;
char buf[80];
in_file = fopen("mypipe", "r");
if (in_file == NULL) {
printf("Error in fdopen./n");
exit(1);
}
while ((count = fread(buf, 1, 80, in_file)) > 0)
printf("received from pipe: %s/n", buf);
fclose(in_file);
}
/* 進程二:寫有名管道*/
#include <stdio.h>
#include <unistd.h>
void main() {
FILE * out_file;
int count = 1;
char buf[80];
out_file = fopen("mypipe", "w");
if (out_file == NULL) {
printf("Error opening pipe.");
exit(1);
}
sprintf(buf,"this is test data for the named pipe example/n");
fwrite(buf, 1, 80, out_file);
fclose(out_file);
}
三、消息隊列:
消息隊列用於運行於同一臺機器上的進程間通訊,它和管道很類似,是一個在系統內核中用來保存消息的隊列,它在系統內核中是以消息鏈表的形式出現。消息鏈表中節點的結構用msg聲明。
事實上,它是一種正逐漸被淘汰的通訊方式,能夠用流管道或者套接口的方式來取代它,因此,對此方式也再也不解釋,也建議讀者忽略這種方式。
四、信號量:
信號量又稱爲信號燈,它是用來協調不一樣進程間的數據對象的,而最主要的應用是前一節的共享內存方式的進程間通訊。本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取情況。通常說來,爲了得到共享資源,進程須要執行下列操做:
(1) 測試控制該資源的信號量。
(2) 若此信號量的值爲正,則容許進行使用該資源。進程將信號量減1。
(3) 若此信號量爲0,則該資源目前不可用,進程進入睡眠狀態,直至信號量值大於0,進程被喚醒,轉入步驟(1)。
(4) 當進程再也不使用一個信號量控制的資源時,信號量值加1。若是此時有進程正在睡眠等待此信號量,則喚醒此進程。
維護信號量狀態的是Linux內核操做系統而不是用戶進程。咱們能夠從頭文件/usr/src/linux/include /linux /sem.h 中看到內核用來維護信號量狀態的各個結構的定義。信號量是一個數據集合,用戶能夠單獨使用這一集合的每一個元素。要調用的第一個函數是semget,用以獲 得一個信號量ID。
struct sem {
short sempid;/* pid of last operaton */
ushort semval;/* current value */
ushort semncnt;/* num procs awaiting increase in semval */
ushort semzcnt;/* num procs awaiting semval = 0 */
}
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
key是前面講過的IPC結構的關鍵字,flag未來決定是建立新的信號量集合,仍是引用一個現有的信號量集合。nsems是該集合中的信號量數。若是是建立新 集合(通常在服務器中),則必須指定nsems;若是是引用一個現有的信號量集合(通常在客戶機中)則將nsems指定爲0。
semctl函數用來對信號量進行操做。
int semctl(int semid, int semnum, int cmd, union semun arg);
不一樣的操做是經過cmd參數來實現的,在頭文件sem.h中定義了7種不一樣的操做,實際編程時能夠參照使用。
semop函數自動執行信號量集合上的操做數組。
int semop(int semid, struct sembuf semoparray[], size_t nops);
semoparray是一個指針,它指向一個信號量操做數組。nops規定該數組中操做的數量。
下面,咱們看一個具體的例子,它建立一個特定的IPC結構的關鍵字和一個信號量,創建此信號量的索引,修改索引指向的信號量的值,最後咱們清除信號量。在下面的代碼中,函數ftok生成咱們上文所說的惟一的IPC關鍵字。
#include <stdio.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>
void main() {
key_t unique_key; /* 定義一個IPC關鍵字*/
int id;
struct sembuf lock_it;
union semun options;
int i;
unique_key = ftok(".", 'a'); /* 生成關鍵字,字符'a'是一個隨機種子*/
/* 建立一個新的信號量集合*/
id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666);
printf("semaphore id=%d/n", id);
options.val = 1; /*設置變量值*/
semctl(id, 0, SETVAL, options); /*設置索引0的信號量*/
/*打印出信號量的值*/
i = semctl(id, 0, GETVAL, 0);
printf("value of semaphore at index 0 is %d/n", i);
/*下面從新設置信號量*/
lock_it.sem_num = 0; /*設置哪一個信號量*/
lock_it.sem_op = -1; /*定義操做*/
lock_it.sem_flg = IPC_NOWAIT; /*操做方式*/
if (semop(id, &lock_it, 1) == -1) {
printf("can not lock semaphore./n");
exit(1);
}
i = semctl(id, 0, GETVAL, 0);
printf("value of semaphore at index 0 is %d/n", i);
/*清除信號量*/
semctl(id, 0, IPC_RMID, 0);
}
五、共享內存:效率高,注意互斥的問題(可使用信號量來實現互斥,或者使用鎖)
主要使用共享內存,效率高,A把數據放入內核某一個內存(共享內存),B直接去讀就能夠了
在Linux系統下,經常使用的方式是經過shmXXX函數族來實現利 用共享內存進行存儲的。
首先要用的函數是shmget,它得到一個共享存儲標識符。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, int size, int flag);
當共享內存建立後,其他進程能夠調用shmat()將其鏈接到自身的地址空間中。
void *shmat(int shmid, void *addr, int flag);
shmid爲shmget函數返回的共享存儲標識符,addr和flag參數決定了以什麼方式來肯定鏈接的地址,函數的返回值便是該進程數據段所鏈接的實際地址,進程能夠對此進程進行讀寫操做。
三種比較相似:
建立/得到:(A建立 B得到)使用函數(消息隊列:msgget),(信號量:semget),(共享內存:shmget),參數不一樣分爲建立/得到
寫:msgsnd ,semop,
讀:msgrcv,semop
上述五種方式都是指同一臺機子
六、網絡通訊,適用於不一樣機子,和相同機子(好比A進程向B進程發,不到硬件底層就到B了,速度也很快)均可以
移植性好,基本都支持,因此沒有特殊要求好比要求速率很是高或者是父子進程的狀況,通常就使用網絡通訊
見以前的文章SOCKET套接字編程有詳細的說明
網絡通訊用的最多
特別的,
對於進程間傳遞描述符(實際比較少用到)
每一個進程都擁有本身獨立的進程空間,這使得描述符在進程之間的傳遞變得有點複雜,這個屬於高級進程間通訊的內容,下面就來講說。
Linux 下的描述符傳遞 Linux 系統系下,子進程會自動繼承父進程已打開的描述符,實際應用中,可能父進程須要向子進程傳遞「後打開的描述符」,或者子進程須要向父進程傳遞;或者兩個進程多是無關的,顯然這須要一套傳遞機制。
簡單的說,首先須要在這兩個進程之間創建一個 Unix 域套接字接口做爲消息傳遞的通道( Linux 系統上使用 socketpair 函數能夠很方面便的創建起傳遞通道),而後發送進程調用 sendmsg 向通道發送一個特殊的消息,內核將對這個消息作特殊處理,從而將打開的描述符傳遞到接收進程。 而後接收方調用 recvmsg 從通道接收消息,從而獲得打開的描述符。然而實際操做起來並不像看起來那樣單純。
先來看幾個注意點:
1) 須要注意的是傳遞描述符並非傳遞一個 int 型的描述符編號,而是在接收進程中建立一個新的描述符,而且在內核的文件表中,它與發送進程發送的描述符指向相同的項。
2) 在進程之間能夠傳遞任意類型的描述符,好比能夠是 pipe , open , mkfifo 或 socket , accept 等函數返回的描述符,而不限於套接字。
3) 一個描述符在傳遞過程當中(從調用 sendmsg 發送到調用 recvmsg 接收),內核會將其標記爲「在飛行中」( in flight )。在這段時間內,即便發送方試圖關閉該描述符,內核仍會爲接收進程保持打開狀態。發送描述符會使其引用計數加 1 。
4) 描述符是經過輔助數據發送的(結構體 msghdr 的 msg_control 成員),在發送和接收描述符時,老是發送至少 1 個字節的數據,即便這個數據沒有任何實際意義。不然當接收返回 0 時,接收方將不能區分這意味着「沒有數據」(但輔助數據可能有套接字)仍是「文件結束符」。
5) 具體實現時, msghdr 的 msg_control 緩衝區必須與 cmghdr 結構對齊,能夠看到後面代碼的實現使用了一個 union 結構來保證這一點。
msghdr 和 cmsghdr 結構體
上面說過,描述符是經過結構體 msghdr 的 msg_control 成員送的,所以在繼續向下進行以前,有必要了解一下 msghdr 和 cmsghdr 結構體,先來看看 msghdr 。
struct msghdr
{
void *msg_name;
socklen_t msg_namelen;
struct iovec *msg_iov;
size_t msg_iovlen;
void *msg_control;
size_t msg_controllen;
int msg_flags; };
sendmsg 和 recvmsg 函數原型以下:
#include <sys/types.h>
#include <sys/socket.h>
int sendmsg(int s, const struct msghdr *msg, unsigned int flags);
int recvmsg(int s, struct msghdr *msg, unsigned int flags);
兩者的參數說明以下:
s,套接字通道,對於 sendmsg 是發送套接字,對於 recvmsg 則對應於接收套接字;
msg ,信息頭結構指針;
flags ,可選的標記位,這與 send 或是 sendto 函數調用的標記相同。
七、信號,主要是經過kill來發送,但要知道對方的PID,因此事先能夠先約定好,ID放到哪一個文件,而後對方去讀
詳細機制介紹:
1) 信號本質
軟中斷信號(signal,又簡稱爲信號)用來通知進程發生了異步事件。在軟件層次上是對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求能夠說是同樣的。信號是進程間通訊機制中惟一的異步通訊機制,一個進程沒必要經過任何操做來等待信號的到達,事實上,進程也不知道信號到底何時到達。進程之間能夠互相經過系統調用kill發送軟中斷信號。內核也能夠由於內部事件而給進程發送信號,通知進程發生了某個事件。信號機制除了基本通知功能外,還能夠傳遞附加信息。
收到信號的進程對各類信號有不一樣的處理方法。處理方法能夠分爲三類:
第一種是相似中斷的處理程序,對於須要處理的信號,進程能夠指定處理函數,由該函數來處理。
第二種方法是,忽略某個信號,對該信號不作任何處理,就象未發生過同樣。
第三種方法是,對該信號的處理保留系統的默認值,這種缺省操做,對大部分的信號的缺省操做是使得進程終止。進程經過系統調用signal來指定進程對某個信號的處理行爲。
2) 信號的種類
能夠從兩個不一樣的分類角度對信號進行分類:
可靠性方面:可靠信號與不可靠信號;
與時間的關係上:實時信號與非實時信號。
3) 信號處理流程
對於一個完整的信號生命週期(從信號發送到相應的處理函數執行完畢)來講,能夠分爲三個階段:
信號誕生
信號在進程中註冊
信號的執行和註銷
3.1)信號誕生
信號事件的發生有兩個來源:硬件來源(好比咱們按下了鍵盤或者其它硬件故障);軟件來源,最經常使用發送信號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操做。
這裏按發出信號的緣由簡單分類,以瞭解各類信號:
(1) 與進程終止相關的信號。當進程退出,或者子進程終止時,發出這類信號。
(2) 與進程例外事件相關的信號。如進程越界,或企圖寫一個只讀的內存區域(如程序正文區),或執行一個特權指令及其餘各類硬件錯誤。
(3) 與在系統調用期間遇到不可恢復條件相關的信號。如執行系統調用exec時,原有資源已經釋放,而目前系統資源又已經耗盡。
(4) 與執行系統調用時遇到非預測錯誤條件相關的信號。如執行一個並不存在的系統調用。
(5) 在用戶態下的進程發出的信號。如進程調用系統調用kill向其餘進程發送信號。
(6) 與終端交互相關的信號。如用戶關閉一個終端,或按下break鍵等狀況。
(7) 跟蹤進程執行的信號。
Linux支持的信號列表以下。不少信號是與機器的體系結構相關的
信號值 默認處理動做 發出信號的緣由
SIGHUP 1 A 終端掛起或者控制進程終止
SIGINT 2 A 鍵盤中斷(如break鍵被按下)
SIGQUIT 3 C 鍵盤的退出鍵被按下
SIGILL 4 C 非法指令
SIGABRT 6 C 由abort(3)發出的退出指令
SIGFPE 8 C 浮點異常
SIGKILL 9 AEF Kill信號
SIGSEGV 11 C 無效的內存引用
SIGPIPE 13 A 管道破裂: 寫一個沒有讀端口的管道
SIGALRM 14 A 由alarm(2)發出的信號
SIGTERM 15 A 終止信號
SIGUSR1 30,10,16 A 用戶自定義信號1
SIGUSR2 31,12,17 A 用戶自定義信號2
SIGCHLD 20,17,18 B 子進程結束信號
SIGCONT 19,18,25 進程繼續(曾被中止的進程)
SIGSTOP 17,19,23 DEF 終止進程
SIGTSTP 18,20,24 D 控制終端(tty)上按下中止鍵
SIGTTIN 21,21,26 D 後臺進程企圖從控制終端讀
SIGTTOU 22,22,27 D 後臺進程企圖從控制終端寫
處理動做一項中的字母含義以下
A 缺省的動做是終止進程
B 缺省的動做是忽略此信號,將該信號丟棄,不作處理
C 缺省的動做是終止進程並進行內核映像轉儲(dump core),內核映像轉儲是指將進程數據在內存的映像和進程在內核結構中的部份內容以必定格式轉儲到文件系統,而且進程退出執行,這樣作的好處是爲程序員提供了方便,使得他們能夠獲得進程當時執行時的數據值,容許他們肯定轉儲的緣由,而且能夠調試他們的程序。
D 缺省的動做是中止進程,進入中止情況之後還能從新進行下去,通常是在調試的過程當中(例如ptrace系統調用)
E 信號不能被捕獲
F 信號不能被忽略
4) 信號的安裝
若是進程要處理某一信號,那麼就要在進程中安裝該信號。安裝信號主要用來肯定信號值及進程針對該信號值的動做之間的映射關係,即進程將要處理哪一個信號;該信號被傳遞給進程時,將執行何種操做。
linux主要有兩個函數實現信號的安裝:signal()、sigaction()。其中signal()只有兩個參數,不支持信號傳遞信息,主要是用於前32種非實時信號的安裝;而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支持信號傳遞信息,主要用來與 sigqueue() 系統調用配合使用,固然,sigaction()一樣支持非實時信號的安裝。sigaction()優於signal()主要體如今支持信號帶有參數。
4.1) signal()
#include <signal.h>
void (*signal(int signum, void (*handler))(int)))(int);
若是該函數原型不容易理解的話,能夠參考下面的分解方式來理解:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));
第一個參數指定信號的值,第二個參數指定針對前面信號值的處理,能夠忽略該信號(參數設爲SIG_IGN);能夠採用系統默認方式處理信號(參數設爲SIG_DFL);也能夠本身實現處理方式(參數指定一個函數地址)。
若是signal()調用成功,返回最後一次爲安裝信號signum而調用signal()時的handler值;失敗則返回SIG_ERR。
傳遞給信號處理例程的整數參數是信號值,這樣可使得一個信號處理例程處理多個信號。
static LD_VD SignalInit(LD_VD)
{
/* signal process */
//signal(SIGINT, SignalHnd);
//signal(SIGTERM, SignalHnd);
//必定要忽略這個信號,不然當服務器關閉tcp後,程序再往這個socket
//讀寫數據將致使程序直接退出
signal(SIGPIPE, SignalHnd);//使用了管道就安裝對管道信號的處理
}
4.2) sigaction()
#include <signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
5) 信號的發送
發送信號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。
6) 信號集及信號集操做函數:
7) 信號阻塞與信號未決:
每一個進程都有一個用來描述哪些信號遞送到進程時將被阻塞的信號集,該信號集中的全部信號在遞送到進程後都將被阻塞。下面是與信號阻塞相關的幾個函數:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset));
int sigpending(sigset_t *set));
int sigsuspend(const sigset_t *mask));
信號機制詳細原文參考: http://www.cnblogs.com/hoys/archive/2012/08/19/2646377.html
八、流管道
該函數的原型是FILE * popen(const char* command, const char *type);
command:使咱們要執行的命令,即上述的運行命令,
type:有兩種可能的取值,「r」(表明讀取)或者「w"(表明寫入)
popen()會調用fork()產生子進程,而後從子進程中調用/bin/sh-c來執行參數command的指令,參數type可以使用「r」讀取 或者「w」寫入,根據type的值,popen()會建立管道鏈接到子進程的標準輸出設備或者標準輸入設備,而後返回一個文件指針。隨後進程就能夠利用此文件指針來讀取子進程的標準輸出設備或者寫入子進程的標準輸入設備。
這個函數能夠大大減小代碼的編寫量,但使用不太靈活,不能本身建立管道那麼靈活,而且popen()必須使用標準的I/o函數進行操做,也不能使用read(),wirte()這種不帶緩衝的I/O函數,必須使用pclose()來關閉管道流,該函數關閉標準I/O流,並等待命令執行結束
總結:向這個流中寫內容至關於寫入該命令(或者是該程序)標準輸入; 向這個流中讀數據至關於讀取該命令(或者是該程序)的標準輸出.
讀:
LD_S32 RunSysCmd2(LD_CS8 *pCmd, LD_S8 *pRslBuf, LD_S32 bufSz)
{
LD_S32 ret = LD_FAILURE;
FILE *pFd = popen(pCmd, "r");//建立一個流管道生成一個子進程用於執行命令
if(pFd)
{
memset(pRslBuf, 0, bufSz);
if(fread(pRslBuf, bufSz - 1, 1, pFd) >= 0)//從該命令中讀取數據
{
ret = LD_SUCCESS;
}
pclose(pFd);//關閉流管道
}
return ret;
}
寫:
popen,system和exec區別:
1).
system和popen都是執行了相似的運行流程,大體是fork->execl->return。可是咱們看到system在執行期間調用進程會一直等待shell命令執行完成(waitpid等待子進程結束)才返回,可是popen無須等待shell命令執行完成就返回了。咱們能夠理解system爲串行執行,在執行期間調用進程放棄了」控制權」,popen爲並行執行。
popen中的子進程沒人給它」收屍」了啊?是的,若是你沒有在調用popen後調用pclose那麼這個子進程就可能變成」殭屍」。
2).
對於管道已經很清楚,而管道寫可能用的地方比較少。而對於寫可能更經常使用的是system函數:
system("cat "Read pipe successfully!" > test1")
能夠看出,popen能夠控制程序的輸入或者輸出,而system的功能明顯要弱一點,好比沒法將讀取結果用於程序中。
若是不須要使用到程序的I/O數據流,那麼system是最方便的。並且system函數是C89和C99中標準定義的,能夠跨平臺使用。而popen是Posix 標準函數,可能在某些平臺沒法使用(windows應該是能夠的吧,沒作過測試)。
若是上述兩個函數還沒法知足你的交互需求,那麼能夠考慮exec函數組了。
3).
system是用shell來調用程序=fork+exec+waitpid,而exec是直接讓你的程序代替用來的程序運行。
system 是在單獨的進程中執行命令,完了還會回到你的程序中。而exec函數是直接在你的進程中執行新的程序,新的程序會把你的程序覆蓋,除非調用出錯,不然你再也回不到exec後面的代碼,就是說你的程序就變成了exec調用的那個程序了。