歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

Linux 系統應用編程——進程間通信(上)

現在再Linux應用較多的進程間通信方式主要有以下幾種:

1)無名管道(pipe)及有名管道(fifo):無名管道可用於具有親緣關系進程間的通信;有名管道除具有管道相似的功能外,它還允許無親緣關系進程使用;

2)信號(signal):信號是在軟件層次上對中斷機制的一種模擬,它是比較復雜的通信方式,用於通知進程某事件發生。一個進程收到一個信號與處理器收到一個中斷請求處理的過程類似;

3)消息隊列(message queue):消息隊列是消息的鏈接表,包括POSIX消息隊列和System V 消息隊列。它克服了前兩種通信方式中信息量有限的缺點。具有寫權限的進程可以按照一定的規則向消息隊列中添加新消息;對消息隊列有讀權限的進程則可以從消息隊列中讀取消息。

4)共享內存(shared memory):可以說這時最有效的進程間通信方式。它使得多個進程可以訪問同一塊內存空間,不同進程可以及時查看對方進程中對共享數據的更新。這種通信方式需要依靠某種同步機制,如互斥鎖和信號量等。

5)信號量(semaphore):主要作為進程之間以及統一進程的不同線程之間的同步和互斥手段。

6)套接字(socket):這時一種使用更廣泛的進程間通信機制,它可用於網絡中不同主機之間的進程間通信,應用非常廣泛。

管道通信

管道是Linux 中進程間通信的一種方式,它把一個程序的輸出直接連接到另一個程序的輸入,Linux 的管道主要包括兩種:無名管道和有名管道。

一、無名管道

無名管道是Linux中管道通信的一種原始方法,他有如下特點:

1)只能用於具有親緣關系的進程之間的通信(也就是父子進程或兄弟進程之間);

2)是一個單工的通信模式,具有固定的讀端和寫端;

3)管道也可以看成一種特殊的文件,對於它的讀寫也可是使用普通的read() 、write()等函數,但是它不屬於任何文件系統,並且只存在於內存中;(其字節大小為0)

1、無名管道的創建與關閉

無名管道是基於文件描述符的通信方式。當一個管道創建時,它會創建兩個文件描述符:fd[0] 、fd[1] 。其中 fd[0] 固定用於讀管道,而 fd[1] 固定用於寫管道,如下圖,這樣就構成了一個單向的數據通道:

 

\

 

管道關閉時只需要用 close() 函數將這兩個文件描述符關閉即可。

2、管道創建函數

創建管道可以通過 pipe() 來實現,其語法如下:

所需頭文件#include

函數原型int pipe(int fd[]);

函數傳入值fd :包含兩個元素的整型數組,存放管道對應的文件描述符

函數返回值成功:0

出錯:-1

3、管道讀寫說明

用pipe() 函數創建的管道兩端處於一個進程中。由於管道主要是用於不同進程間的通信,通常是先創建一個管道,再調用 fork () 函數創建一個子進程,該子進程會繼承父進程所創建的管道。

需要注意的是,無名管道是單工的工作方式,即進程要麼只能讀管道,要麼只能寫管道。父子進程雖然都擁有管道的讀端和寫端,但是只能使用其中一個(例如,可以約定父進程讀管道,而子進程寫管道)。這樣就應該把不使用的讀端或寫端文件描述符關閉。

 

\

 

例如:如果將父進程的寫端 fd[1] 和子進程的讀端 fd[0] 關閉。此時,父子進程之間就建立了一條“子進程寫入 父進程讀取”的通道。同樣,也可以關閉父進程的 fd[0] 和子進程的fd[1] ,這樣就可以建立一條“父進程寫入子進程讀取”的通道。另外,父進程也可以創建 多個子進程,各個子進程都繼承了管道的fd[0] 和 fd[1] ,這樣就建立子進程之間的數據通道。

4、管道讀寫注意:

1)只有管道的讀端存在時,向管道寫入數據才有意義,否則,向管道中寫入數據的進程將收到內核傳來的 SIGPIPE 信號 (通常為Broken Pipea錯誤)。

2)向管道寫入數據時,Linux 將不保證寫入的原子性 , 管道緩沖區只要有空間,寫進程就會試圖向管道寫入數據。如果管道緩沖區已滿,那麼寫操作將一直阻塞。

3)父進程在運行時,它們的先後次序必不能保證。為了確保父子進程已經關閉了相應的文件描述符,可在兩個進程中調用 sleep() 函數,當然,用互斥和同步會更好;

下面是一個實例:

view plaincopy

#include

#include

#include

#include

intpid,pid1,pid2;

intmain(intargc,constchar*argv[])

{

intfd[2];

charoutpipe[100],inpipe[100];

if(pipe(fd)<0)

{

perror("pipeerror!");

return-1;

}

if((pid1=fork())<0)

{

perror("forkpid1error");

return-1;

}

elseif(pid1==0)

{

printf("Child1'spidis%d\n",getpid());

close(fd[0]);

strcpy(outpipe,"Child1issendingamessage!");

if(write(fd[1],outpipe,50)==-1)

{

perror("Child1writetooutpipeerror");

return-1;

}

exit(0);

}

if((pid2=fork())<0)

{

perror("forkpid2error");

return-1;

}

elseif(pid2==0)

{

printf("Child2'spidis%d\n",getpid());

close(fd[0]);

strcpy(outpipe,"Child2issendingamessage!");

sleep(1);

if(write(fd[1],outpipe,50)==-1)

{

perror("Child2writetooutpipeerror");

return-1;

}

exit(0);

}

close(fd[1]);

pid=wait(NULL);

printf("%dprocessisover!\n",pid);

if(read(fd[0],inpipe,50)==-1)

{

perror("readChild1pipeerror");

return-1;

}

printf("%s\n",inpipe);

pid=wait(NULL);//回收第二個結束的子進程

printf("%dprocessisover!\n",pid);

if(read(fd[0],inpipe,50)==-1)

{

perror("readChild1pipeerror");

return-1;

}

printf("%s\n",inpipe);

return0;

}

執行結果如下:

view plaincopy

fs@ubuntu:~/qiang/pipe$./pipe

Child2'spidis8504

Child1'spidis8503

8503processisover!

Child1issendingamessage!

8504processisover!

Child2issendingamessage!

fs@ubuntu:~/qiang/pipe$

二、有名管道

有名管道(FIFO)是對無名管道的一種改進,它具有如下特點:

1)它可以使互不相關的兩個進程實現彼此通信;

2)該管道可以通過路徑名來指出,並且在文件系統中是可見的。在建立了管道之後,兩個進程就可以把它當做普通文件一樣進行讀寫操作,使用非常方便;

3)FIFO嚴格地遵循先進先出規則,對管道及 FIFO 的讀總是從開始處返回數據,對它們的寫則把數據添加到末尾。有名管道不支持如lseek()等文件定位操作;

有名管道(FIFO)的創建可以使用 mkfifo() 函數,該函數類似文件中的open() 操作,可以指定管道的路徑和訪問權限 (用戶也可以在命令行使用 “mknod <管道名>”來創建有名管道)。

在創建管道成功以後,就可以使用open()、read() 和 write() 這些函數了。與普通文件一樣,對於為讀而打開的管道可在 open() 中設置 O_RDONLY,對於為寫而打開的管道可在 open() 中設置O_WRONLY。

1、對於讀進程

缺省情況下,如果當前FIFO內沒有數據,讀進程將一直阻塞到有數據寫入或是FIFO寫端都被關閉。

2、對於寫進程

只要FIFO有空間,數據就可以被寫入。若空間不足,寫進程會阻塞,知道數據都寫入為止;

mkfifo() 函數語法如下:

所需頭文件#include

#include

函數原型int mkfifo( const char *filename,mode_t mode)

參數mode:管道的訪問權限

函數返回值成功:0

出粗:-1

下面是個實例,來學習有名管道的使用

create.c

view plaincopy

#include

#include

#include

#include

#include

intmain(intargc,char*argv[])

{

if(argc<2)

{

printf("Usage:%s",argv[0]);

return-1;

}

if(mkfifo(argv[1],0664)<0)

{

perror("mkfifofails");

exit(-1);

}

return0;

} write_fifo.c view plaincopy

#include

#include

#include

#include

#include

#include

#include

#defineBUFFER_SIZE1024

intmain(intargc,constchar*argv[])

{

intfd;

if(argc<2)

{

printf("Usage:%s",argv[0]);

return-1;

}

if((fd=open(argv[1],O_WRONLY))<0)

{

perror("openerror");

exit(-1);

}

printf("openfifo%sforwritingsuccess!\n",argv[0]);

charbuffer[BUFFER_SIZE];

ssize_tn;

while(fgets(buffer,BUFFER_SIZE,stdin))

{

if((n=write(fd,buffer,strlen(buffer)))==-1)

{

perror("writefails");

break;

}

}

return0;

} read_fifo.c view plaincopy

#include

#include

#include

#include

#include

#include

#include

#defineBUFFER_SIZE1024

intmain(intargc,constchar*argv[])

{

intfd;

if(argc<2)

{

printf("Usage:%s",argv[0]);

return-1;

}

if((fd=open(argv[1],O_RDONLY))<0)

{

perror("openerror");

exit(-1);

}

printf("openfifo%sforreadingsuccess!\n",argv[0]);

charbuffer[BUFFER_SIZE];

ssize_tn;

while(1)

{

if((n=read(fd,buffer,BUFFER_SIZE))==-1)

{

perror("readfails");

return-1;

}

elseif(n==0)

{

printf("peerclosefifo\n");

break;

}

else

{

buffer[n]='\0';

printf("read%dbytesfromfifo:%s\n",n,buffer);

}

}

return0;

}

執行結果如下:

寫端:

view plaincopy

fs@ubuntu:~/qiang/fifo$./create_fifotmp

fs@ubuntu:~/qiang/fifo$./write_fifotmp

openfifo./write_fifoforwritingsuccess!

xiao

zhi

qiang

^C

fs@ubuntu:~/qiang/fifo$

讀端:

view plaincopy

fs@ubuntu:~/qiang/fifo$./read_fifotmp

openfifo./read_fifoforreadingsuccess!

read5bytesfromfifo:xiao

read4bytesfromfifo:zhi

read6bytesfromfifo:qiang

peerclosefifo

fs@ubuntu:~/qiang/fifo$

這裡執行時,可以看到,單獨打開讀或寫,二者會一直阻塞,直到都打開,才會打印第一句話,當寫端關閉時,讀端也會停止。

三、信號通信

信號是在軟件層次上對中斷機制的一種模擬。在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是異步的:一個進程不必通過任何操作在等待信號的到達。事實上,進程也不知道信號到底什麼時候到達。事實上,進程也不知道信號到底什麼時候到達。信號可以直接進行用戶空間進程和內核進程之間的交互,內核進程也可以利用它來通知用戶空間進程發生了那些系統事件。它可以在任何時候發給某一進程,而無需知道該進程的狀態。如果該進程當前並未處於執行態,則該信號就由內核保存起來,知道該進程回恢復行再傳遞給它為止;如果一個信號被進程設置為阻塞,則該信號的傳遞被延遲,直道阻塞被取消時才被傳遞給進程。

 

\

 

1、信號的生存周期:

 

\

 

2、進程可以通過3種方式來響應一個信號

1)忽略信號

即對信號不做任何處理,其中,有兩個信號不能忽略:SIGKILL及 SIGSTOP;

2)捕捉信號

定義信號處理函數,當信號發生時,執行相應的處理函數。

3)執行默認操作

Linux 對每種信號都規定了默認操作;(後面會給出信號列表)

這裡介紹幾個常用的信號

信號名含義默認操作

SIGINT該信號在用戶輸入INTR字符(通常是Ctrl + C)時發出,

終端驅動程序發送該信號並送到前台進程中的每一個進程終止進程

SIGQUIT該信號和SIGINT類似,但由QUIT字符(通常是Ctrl + \)來

控制終止進程

SIGKILL該信號用來立即結束程序的運行;

不能被阻塞、處理和忽略;終止進程

SIGALARM該信號當一個定時器到時的時候發出;終止進程

SIGSTOP該信號用於暫停一個進程;

不能被阻塞、處理和忽略;暫停進程

SIGTSTP該信號用於交互停止進程(掛起),由Ctrl + Z 來發出終止進程

3、信號處理流程

 

\

 

下面是內核如何實現信號機制,即內核如何向一個進程發送信號、進程如何接收一個信號、進程怎樣控制自己對信號的反應、內核在什麼實際處理和怎樣處理進程收到的信號。

內核對信號的基本處理方法

內核給一個進程發送軟中斷信號的方法是,在進程所在的進程表項的信號域設置對於該信號的位(內核通過在進程的 struct task_struct 結構中的信號域中設置相應的位來實現向一個進程發送信號)。這裡要補充的是,如果信號發送給一個正在睡眠的進程,那麼要看該進程進入睡眠的優先級,如果進程睡眠在可被中斷的優先級上,則喚醒進程;否則僅設置進程表中信號域相應的位,而不喚醒進程。這一點比較重要,因為進程檢查是否收到信號的時機是:一個進程在即將從內核態返回到用戶態時;或者,在一個進程要進入或離開一個適當的低調度優先級睡眠狀態時。

內核處理一個進程收到的信號的時機是一個進程從內核態返回用戶態時。所以,當一個進程在內核態運行時,軟中斷信號並不立即起作用,要等到將返回用戶態時才處理。進程只有處理完信號才會返回用戶態,進程在用戶態下不會有未處理完的信號。

內核處理一個進程收到的軟中斷信號是在該進程的上下文中,因此,進程必須處於運行狀態。處理信號有三種類型:進程接收到信號後退出;進程忽略該信號;進程收到信號後執行用戶自定義的使用系統調用signal() 注冊的函數。當進程接收到一個它忽略的信號時,進程丟棄該信號,就像從來沒有收到該信號似得,而繼續運行。如果進程收到一個要捕捉的信號,那麼進程從內核態返回用戶態時執行用戶定義的函數。而且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上創建一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的處理函數處,從函數返回再彈出棧頂時,才返回原來進入內核的地方。這樣做的原因是用戶定義的處理函數不能且不允許在內核態下執行(如果用戶定義的函數在內核態下運行的話,用戶就可以獲得任何權限)。

在信號的處理方法中有幾點特別要引起注意:

1)在一些系統中,當一個進程處理完中斷信號返回用戶態之前,內核清除用戶區中設定的對該信號的處理例程的地址,即下一次進程對該信號的處理方法又改為默認值,除非在下一次信號到來之前再次調用 signal() 系統調用。這可能會使得進程在調用 signal() 之前又得到該信號而導致退出。在BSD系統中,內核不再清除該地址。但不清楚該地址可能使得進程因為過多過快的得到某個信號而導致堆棧溢出。為了避免出現上述情況。在BSD中,內核模擬了對硬件中斷的處理方法,即在處理某個中斷時,阻止接收新的該類中斷。

4、信號相關函數

1)信號發送:kill() 和 raise()

kill() 函數同讀者熟知的kill 系統命令一樣,可以發送信號給進程或進程組(實際上,kill 系統命令就是由 kill () 函數實現的)。需要注意的是,它不僅可以終止進程,也可以向進程發送其他信號;

與kill() 函數不同的是,raise() 函數只允許進程向自身發送信號;

kill() 函數語法

所需頭文件#include

#include

函數原型int kill(pid_t pid,int sig);

函數傳入值pid 為正數: 發送信號給進程號為pid 的進程

pid 為 0 : 信號被發送到所有和當前進程在同一個進程組的進程

pid 為 -1 :信號發送給所有進程表中的進程(除了進程號最大的進程外)

pid 為 < -1 :信號發送給進程組號為 -pid 的每一個進程

sig :信號類型

函數返回值成功 :0

出錯: -1

raise() 函數的語法

所需頭文件#include

#include

函數原型int raise(int sig);

函數傳入值sig :信號類型

函數返回值成功:0

出錯: -1

這裡 raise() 等價於 kill ( getpid() , sig) ;

下面舉一個實例:

view plaincopy

#include

#include

#include

#include

#include

#include

intmain(intargc,char*argv[])

{

pid_tpid;

intret;

if((pid=fork())<0)

{

perror("forkerror");

exit(-1);

}

if(pid==0)

{

printf("child(pid:%d)iswaitingforanysignal\n",getpid());

raise(SIGSTOP);

exit(0);

}

sleep(1);

if((waitpid(pid,NULL,WNOHANG))==0)

{

kill(pid,SIGKILL);

printf("parentkillchildprocess%d\n",pid);

}

waitpid(pid,NULL,0);

return0;

} 執行結果如下: view plaincopy

fs@ubuntu:~/qiang/signal$./kill

child(pid:9977)iswaitingforanysignal

parentkillchildprocess9977

fs@ubuntu:~/qiang/signal$

2)、定時器信號:alarm() 、pause()

alarm() 也稱鬧鐘信號,它可以在進程中設置一個定時器。當定時器指定的時間到時,它就向進程發送SIGALRAM信號。要注意的是,一個進程只能有一個鬧鐘時間,如果在調用alarm()函數之前已設置過鬧鐘信號,則任何以前的鬧鐘時間都被新值所代替。

pause()函數是用於將調用進程掛起直至收到信號為止。

alarm()函數語法:

所需頭文件#include

函數原型unsigned int alarm(unsigned int second);

函數傳入值seconds:指定秒數,系統經過seconds秒之後向該進程發送SIGALARM信號

函數返回值成功:如果調用次alarm()前,進程中已經設置了鬧鐘時間,

則返回上一個鬧鐘剩余的時間,否則返回 0;

出錯: -1

pause() 函數語法

所需頭文件#include

函數原型int pause(void);

函數返回值-1;並且把 errno值設為RINTR

下面一個實例,完成一個簡單的sleep() 函數的功能,由於SIGALARM 默認的系統動作為終止該進程,因此程序在打印信息之前就已經結束了;

執行結果如下:

view plaincopy

fs@ubuntu:~/qiang/signal$./alarm

Alarmclock

fs@ubuntu:~/qiang/signal$

可以看到printf() 裡面的內容並沒有被打印, Alarm clock 是SIGALARM信號默認處理函數打印。

3)、信號的設置 signal() 和 sigaction()

signal() 函數

要對一個信號進行處理,就需要給出此信號發生時系統所調用的處理函數。可以為一個特定的信號(除去無法捕捉的SIGKILL和SIGSTOP信號)注冊相應的處理函數。如果正在運行的程序源代碼裡注冊了針對某一特定信號的處理程序,不論當時程序執行到何處,一旦進程接收到該信號,相應的調用就會發生。

signal()函數使用時,只需要指定的信號類型和信號處理函數即可。它主要用於前32種非實時信號的處理,不支持信號傳遞信息。

其語法格式如下:

所需頭文件#include

函數原型typeef void (*sighandle_t)(int) ; 函數指針類型

sighandle_t signal(int signum,sighandle_t handler);

函數傳入值signum:指定信號代碼

Handler:SIG_IGN:忽略該信號

SIG_DFL:采用系統默認方式處理信號

自定義的信號處理函數;

函數返回值成功:以前的信號處理函數

出錯:-1

該函數第二個參數和返回值類型都是指向一個無返回值並且帶一個整型參數的函數的指針;且只要signal() 調用了自定義的信號處理函數,即使這個函數什麼也不做,這個進程也不會被終止;

下面一個程序利用signal來實現發送信號和接受信號的原理:

程序內容:創建子進程代表售票員,父進程代表司機,同步過程如下:

售票員捕捉 SIGINT(代表開車),發送信號SIGUSR1給司機,司機打印(“let's gogogo!”);

售票員捕捉 SIGQUIT(代表停止),發送信號SIGUSR2給司機,司機打印(“stop the bus!”);

司機捕捉 SIGTSTP (代表車到總站),發SIGUSR1給售票員,售票員打印(“Please get off the bus”);

代碼如下:

view plaincopy

#include

#include

#include

#include

#include

pid_tpid;

voiddriver_handler(intsigno);

voidsaler_handler(intsigno);

intmain(intargc,char*argv[])

{

if((pid=fork())<0)

{

perror("forkerror");

return-1;

}

if(pid>0)

{

signal(SIGTSTP,driver_handler);

signal(SIGINT,SIG_IGN);

signal(SIGQUIT,SIG_IGN);

signal(SIGUSR1,driver_handler);

signal(SIGUSR2,driver_handler);

while(1)

pause();

}

if(pid==0)

{

signal(SIGINT,saler_handler);

signal(SIGTSTP,SIG_IGN);

signal(SIGQUIT,saler_handler);

signal(SIGUSR1,saler_handler);

signal(SIGUSR2,SIG_IGN);

while(1)

pause();

}

return0;

}

voiddriver_handler(intsigno)

{

if(signo==SIGUSR1)

printf("Let'sgogogo!\n");

if(signo==SIGUSR2)

printf("Stopthebus!\n");

if(signo==SIGTSTP)

kill(pid,SIGUSR1);

}

voidsaler_handler(intsigno)

{

pid_tppid=getppid();

if(signo==SIGINT)

kill(ppid,SIGUSR1);

if(signo==SIGQUIT)

kill(ppid,SIGUSR2);

if(signo==SIGUSR1)

{

printf("pleasegetoffthebus\n");

kill(ppid,SIGKILL);

exit(0);

}

}

執行結果如下:

view plaincopy

fs@ubuntu:~/qiang/signal$./signal

^CLet'sgogogo!

^\Stopthebus!

^CLet'sgogogo!

^\Stopthebus!

^CLet'sgogogo!

^\Stopthebus!

^CLet'sgogogo!

^\Stopthebus!

^Zpleasegetoffthebus

Killed

fs@ubuntu:~/qiang/signal$

sigaction() 函數

sigaction() 函數的功能是檢查或修改(或兩者)與指定信號相關聯的處理動作,此函數可以完全代替signal 函數。

函數原型如下:

所需頭文件#include

函數原型int sigaction(int signum, const struct sigaction *act ,

struct sigaction *oldact );

函數傳入值signum:可以指定SIGKILL和SIGSTOP以外的所有信號

act :act 是一個結構體,裡面包含信號處理函數的地址、

處理方式等信息;

oldact :參數oldact 是一個傳出參數,sigaction 函數調用成功後,

oldact 裡面包含以前對 signum 信號的處理方式的信息;

函數返回值成功:0

出錯:-1

其中參數signo 是要檢測或修改其具體動作的信號編號。若act 指針非NULL,則要修改其動作。如果oact 指針非空,則系統經由 oact 指針返回該信號的上一個動作;

參數結構sigaction定義如下:

view plaincopy

structsigaction

{

void(*sa_handler)(int);

void(*sa_sigaction)(int,siginfo_t*,void*);

sigset_tsa_mask;

intsa_flags;

void(*sa_restorer)(void);

} ① sa_handler:此參數和signal()的參數handler相同,此參數主要用來對信號舊的安裝函數signal()處理形式的支持;

② sa_sigaction:新的信號安裝機制,處理函數被調用的時候,不但可以得到信號編號,而且可以獲悉被調用的原因以及產生問題的上下文的相關信息。

③ sa_mask:用來設置在處理該信號時暫時將sa_mask指定的信號擱置;

④ sa_restorer: 此參數沒有使用;

⑤ sa_flags:用來設置信號處理的其他相關操作,下列的數值可用。可用OR 運算(|)組合:

?A_NOCLDSTOP:如果參數signum為SIGCHLD,則當子進程暫停時並不會通知父進程

SA_ONESHOT/SA_RESETHAND:當調用新的信號處理函數前,將此信號處理方式改為系統預設的方式

SA_RESTART:被信號中斷的系統調用會自行重啟

SA_NOMASK/SA_NODEFER:在處理此信號未結束前不理會此信號的再次到來

SA_SIGINFO:信號處理函數是帶有三個參數的sa_sigaction。

Copyright © Linux教程網 All Rights Reserved