Linux-應用程式設計-學習筆記(17):程序全解
前言:當程式被系統調用到記憶體以後,系統會給程式配置設定一定的資源(記憶體,裝置等等)然後進行一系列的複雜操作,使程式變成程序以供系統調用,是以程序是linux系統中非常重要的一個概念。
一、程式和環境變量
1. 程式的開始和結束
1.1 運作前的準備
之前一直在說一個程式的開始是main函數,但是在main函數執行之前,作業系統下的應用程式還是需要執行一段引導代碼才能去執行main函數。然後這段引導代碼一般不需要我們自己去編寫,而是在編譯連接配接時由連結器将編譯器中事先準備好的引導代碼給連接配接進去和我們的應用程式一起構成最終的可執行程式。
1.2 加載時
加載器是作業系統中的程式,當我們去執行一個程式時(譬如./a.out,譬如代碼中用exec族函數來運作)加載器負責将這個程式加載到記憶體中去執行這個程式。
是以一個程式在編譯連接配接時用連結器,運作時用加載器。
1.3 程式的退出
(1)正常終止:return、exit、_exit(return -1和return 0都是自己知道為什麼終止)。
(2)非正常終止:自己或他人發信号(Ctrl C就是一個信号)。
(3)還可以通過atexit函數來注冊程序終止處理函數(也就是退出前執行某一個程式)。當用atexit注冊多個程序終止處理函數,先注冊的後執行(先進後出,和棧一樣)。
atexit函數用法如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void func1(void)
{
printf("func1\n");
}
void func2(void)
{
printf("func2\n");
}
int main(void)
{
printf("hello world.\n");
// 當程序被正常終止時,系統會自動調用這裡注冊的func1執行
atexit(func2);
atexit(func1);
printf("my name is lilei hanmeimei\n");
//return 0;
exit(0);
}
2. 程序環境
(1)我們可以在ubuntu中執行export指令來檢視程序環境變量。

(2)每一個程序中都有一份所有環境變量構成的一個表格,也就是說我們目前程序中可以直接使用這些環境變量。程序環境表其實是一個字元串數組,用environ變量指向它。
//包含庫檔案
#include <stdio.h>
//用指針指向變量
extern char **environ;
//然後直接使用它
printf("%s\n", environ[i]);
我們寫的程式中可以無條件直接使用系統中的環境變量,是以一旦程式中用到了環境變量那麼程式就和作業系統環境有關了。
二、程序解析
1. 程序的引入
1.1 什麼是程序?
首先,程序是一個動态過程而不是靜态實物。其次,程序就是程式的一次運作過程,一個靜态的可執行程式a.out的一次運作過程(./a.out去運作到結束)就是一個程序。核心中建構了一種資料結構,又稱程序控制塊PCB(process control block),用來專門管理一個程序。
1.2 程序運作的虛拟位址空間
(1)作業系統中每個程序在獨立位址空間中運作,是以程序直接的運作不會互相打擾。這提供了一種程序隔離的方法,進而能夠提供一種多程序同時運作的方式。
(2)如果程序運作所在的系統為4GB記憶體時,那麼每個程序的邏輯位址空間均為4GB(32位系統)。但是在實際的情況下,一個程序所占用的記憶體基本不會達到4GB這麼多,也就是不會用滿。是以,記憶體的使用一般會被劃分為2部分,0-1G為OS,1-4G為應用,對于每個程序來說,它們自己都會認為它們在獨享着這4GB的記憶體,但是實際情況是這4GB的記憶體被合理地供多個程序一起使用(這樣也是為了能夠提高系統效率)。
(3)那麼怎樣做到了每個程序能夠獨享4GB空間?這就用到了虛拟位址到實體位址空間的映射的方法,也就是程序用到多少記憶體,就在實體位址上映射多少。是以記憶體進行分時複用的方式,實作讓每個程序覺得自己都用到了大記憶體。
1.3 程序的ID
作業系統給每一個程序一個ID号來辨別這個程序。我們可以通過ps指令在ubuntu中檢視目前運作的程序資訊,其中PID即為程序的ID号。
同時,也可以通過系統API來實作函數擷取程序ID的方法。
getpid(擷取目前程序ID)、getppid(擷取父程序ID)、getuid(擷取目前程序的使用者ID)、geteuid(有效使用者ID)、getgid(擷取組ID)、getegid(有效組ID)
//包含的頭檔案
#include <sys/types.h>
#include <unistd.h>
//定義
pid_t p1 = -1, p2 = -1;
//使用
p1 = getpid();
printf("pid = %d.\n", p1);
1.4 多程序排程原理
作業系統是一種多程序的方式,是以在作業系統中,可以同時運作多個程序。宏觀上多程序是一種并行的方式,但是在微觀上CPU需要在多個程序之間實作切換,對于CPU是一種串行的方式。如何實作在多個程序之間的切換?這就用到了排程器。
作業系統的排程器其實就是一個算法,它用來決定先運作誰,後運作誰,誰運作多長時間。(因為CPU的速度非常快,通過多程序與排程系統來提高CPU的使用率)。實際上現代作業系統最小的排程單元是線程而不是程序。
2. 父子程序關系
2.1 子程序的建立
(1)每一次程式的運作都需要一個程序,是以誕生一個新的程序即标準着一個子程序的建立。
(2)由于作業系統中建構一個全新的程序是一件非常複雜的事情,是以為了簡化這件事情,建構新的程序采用分裂生長模式。如果作業系統需要一個新程序來運作一個程式,那麼作業系統會用一個現有的程序來複制生成一個新程序。老程序叫父程序,複制生成的新程序叫子程序。
(3)建立完成之後的父子程序會同時運作。
我們可以通過系統API中的fork函數來實作子程序的建立,原理就是上面提到的程序複制。
//頭檔案包含
#include <unistd.h>
//代碼用法
pid_t fork(void);
//使用舉例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t p1 = -1;
//建立一個子程序
p1 = fork(); // 傳回2次
//fork函數調用一次會傳回2次,傳回值等于0的就是子程序,而傳回值大于0的就是父程序
if (p1 == 0)
{
// 這裡一定是子程序
// 先sleep一下讓父程序先運作,先死
sleep(1);
printf("子程序, pid = %d.\n", getpid());
printf("hello world.\n");
printf("子程序, 父程序ID = %d.\n", getppid());
}
if (p1 > 0)
{
// 這裡一定是父程序
printf("父程序, pid = %d.\n", getpid());
printf("父程序, p1 = %d.\n", p1);
}
if (p1 < 0)
{
// 這裡一定是fork出錯了
}
return 0;
}
從最後的運作結果可以看出,main函數相當于被運作了2次,這證明了父子程序是同時運作的。通過函數中,我們可以知道,fork函數調用一次會傳回2次,傳回值等于0的就是子程序,而傳回值大于0的就是父程序,是以我們可以通過if判斷傳回值來指定父程序和子程序的任務。
對于新生成的子程序來說,它繼承了一些父程序的屬性,但是它有着自己獨立的PCB,同時它也能被核心進行同等排程。
2.2 父子程序對檔案的操作
(1)父程序先open打開一個檔案得到fd,然後在fork建立子程序。之後在父子程序中各自write向fd中寫入内容。
測試結論:接續寫。實際上本質原因是父子程序之間的fd對應的檔案指針是彼此關聯的(很像O_APPEND标志後的樣子)。
實際測試時有時候會看到隻有一個,有點像分别寫。但是實際不是,原因是父程序執行完就close了,子程序就寫不進去了(解決方法是給每個程序中添加sleep,這樣就不會存在一個關閉了,另一個沒法寫)。
(2)父程序open打開1.txt然後寫入,子程序打開1.txt然後寫入,也就是各自獨立打開同一檔案。
測試結論:分開寫。原因是父子程序分離後才各自打開的1.txt,這時候這兩個程序的PCB已經獨立了,檔案表也獨立了,是以2次讀寫是完全獨立的。
open時使用O_APPEND标志看看會如何?實際測試結果标明O_APPEND标志可以把父子程序各自獨立打開的fd的檔案指針給關聯起來,實作分别寫。
總結:
父子程序間終究多了一些牽絆(存在一些繼承的東西)。父程序在沒有fork之前自己做的事情對子程序有很大影響,但是父程序fork之後在自己的if裡做的事情就對子程序沒有影響了。本質原因就是因為fork内部實際上已經複制父程序的PCB生成了一個新的子程序,并且fork傳回時子程序已經完全和父程序脫離并且獨立被OS排程執行。總之就是一句話,子程序最終目的是要獨立去運作另外的程式。
3. 程序的誕生和消亡
3.1 程序的誕生
我們知道在最開始的時候先有的是程序0和程序1,程序0為核心态,程序1為核心态過度到使用者态的一個重要程序,也可以說所有的程序都源于程序1。程序産生的原理就是上述提到的fork形式建立。
3.2 程序的消亡
程序終止有兩種方式,一種是自己設定的終止方式,又稱正常終止;另一種是由于一些信号或者外部緣故導緻的終止,成為異常終止。
程序在運作時需要消耗系統資源(記憶體、IO)。linux系統設計時規定:每一個程序退出時,作業系統會自動回收這個程序涉及到的所有的資源(譬如malloc申請的内容沒有free時,目前程序結束時這個記憶體會被釋放,譬如open打開的檔案沒有close的在程式終止時也會被關閉)。但是作業系統隻是回收了這個程序工作時消耗的記憶體和IO,而并沒有回收這個程序本身占用的記憶體(8KB,主要是task_struct和棧記憶體)。那麼這8KB的記憶體作業系統該怎麼辦?答案是需要通過他的父程序來幫它處理這些殘餘的資源。
3.3 僵屍程序和孤兒程序
(1)僵屍程序
子程序先于父程序結束。子程序結束後父程序此時并不一定立即就能幫子程序“收屍”,在這一段(子程序已經結束且父程序尚未幫其收屍)子程序就被成為僵屍程序。
在這種情況下,子程序結束時,作業系統會将其占用的大量資源進行回收。但是8KB的參與需要通過父程序使用wait或waitpid以顯式回收子程序的剩餘待回收記憶體資源并且擷取子程序退出狀态。
當父程序結束時一樣會回收子程序的剩餘待回收記憶體資源。(這樣設計是為了防止父程序忘記顯式調用wait/waitpid來回收子程序進而造成記憶體洩漏)
(2)孤兒程序
父程序先于子程序結束,子程序成為一個孤兒程序。
linux系統規定:所有的孤兒程序都自動成為一個特殊程序(程序1,也就是init程序)的子程序。或者對于ubuntu這種作業系統會提供一個統一的程序進行孤兒程序的管理。
3.4 父程序對子程序回收
(1)wait回收方式
工作原理:子程序結束時,系統會向其父程序發送SIGCHILD信号。是以,父程序調用wait函數後阻塞,等待被SIGCHILD信号喚醒然後去回收僵屍子程序。(如果父程序沒有任何子程序則wait傳回錯誤)
父子程序之間是異步的,SIGCHILD信号機制就是為了解決父子程序之間的異步通信問題,讓父程序可以及時的去回收僵屍子程序。
//包含頭檔案
#include <sys/types.h>
#include <sys/wait.h>
//函數用法
pid_t wait(int *status);
//函數舉例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(void)
{
pid_t pid = -1;
pid_t ret = -1;
int status = -1;
//建立一個子程序
pid = fork();
if (pid > 0)
{
// 父程序
sleep(1);
printf("parent.\n");
ret = wait(&status);
printf("子程序已經被回收,子程序pid = %d.\n", ret);
printf("子程序是否正常退出:%d\n", WIFEXITED(status));
printf("子程序是否非正常退出:%d\n", WIFSIGNALED(status));
printf("正常終止的終止值是:%d.\n", WEXITSTATUS(status));
}
else if (pid == 0)
{
// 子程序
printf("child pid = %d.\n", getpid());
return 51;
//exit(0);
}
else
{
perror("fork");
return -1;
}
return 0;
}
wait的參數status。status用來傳回子程序結束時的狀态,父程序通過wait得到status後就可以知道子程序的一些結束狀态資訊。wait的傳回值pid_t,這個傳回值就是本次wait回收的子程序的PID。目前程序有可能有多個子程序,wait函數阻塞直到其中一個子程序結束wait就會傳回,wait的傳回值就可以用來判斷到底是哪一個子程序本次被回收了。
對wait做個總結:wait主要是用來回收子程序資源,回收同時還可以得知被回收子程序的pid和退出狀态。
(2)waitpid回收方式
工作原理:與wait原理相同,唯一差別是waitpid支援非阻塞的方式(也就是我waitpid看一下有沒有信号表示程序需要被回收,如果沒有就直接過去了)。
//包含頭檔案
#include <sys/types.h>
#include <sys/wait.h>
//函數用法
pid_t waitpid(pid_t pid, int *status, int options);
//函數舉例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(void)
{
pid_t pid = -1;
pid_t ret = -1;
int status = -1;
pid = fork();
if (pid > 0)
{
// 父程序
sleep(1); //這裡相當于一種消滅竟态的方法
printf("parent, 子程序id = %d.\n", pid);
//ret = wait(&status);
//ret = waitpid(-1, &status, 0);
//ret = waitpid(pid, &status, 0);
ret = waitpid(pid, &status, WNOHANG); // 非阻塞式
printf("子程序已經被回收,子程序pid = %d.\n", ret);
printf("子程序是否正常退出:%d\n", WIFEXITED(status));
printf("子程序是否非正常退出:%d\n", WIFSIGNALED(status));
printf("正常終止的終止值是:%d.\n", WEXITSTATUS(status));
}
else if (pid == 0)
{
// 子程序
//sleep(1);
printf("child pid = %d.\n", getpid());
return 51;
//exit(0);
}
else
{
perror("fork");
return -1;
}
return 0;
}
(1)ret = waitpid(-1, &status, 0); -1表示不等待某個特定PID的子程序而是回收任意一個子程序,0表示用預設的方式(阻塞式)來進行等待,傳回值ret是本次回收的子程序的PID。
(2)ret = waitpid(pid, &status, 0); 等待回收PID為pid的這個子程序,如果目前程序并沒有一個ID号為pid的子程序,則傳回值為-1;如果成功回收了pid這個子程序則傳回值為回收的程序的PID。
(3)ret = waitpid(pid, &status, WNOHANG);這種表示父程序要非阻塞式的回收子程序。此時如果父程序執行waitpid時子程序已經先結束等待回收則waitpid直接回收成功,傳回值是回收的子程序的PID;如果父程序waitpid時子程序尚未結束則父程序立刻傳回(非阻塞),但是傳回值為0(表示回收不成功)。
4. exec族函數
4.1 為什麼需要exec函數?
fork子程序是為了執行新程式(fork建立了子程序後,子程序和父程序同時被OS排程執行,是以子程序可以單獨的執行一個程式,這個程式宏觀上将會和父程序程式同時進行)。
可以直接在子程序的if中寫入新程式的代碼。這樣可以,但是不夠靈活,因為我們隻能把子程序程式的源代碼貼過來執行(必須知道源代碼,而且源代碼太長了也不好控制),譬如說我們希望子程序來執行ls -la 指令就不行了(沒有源代碼,隻有編譯好的可執行程式)。使用exec族運作新的可執行程式(exec族函數可以直接把一個編譯好的可執行程式直接加載運作)。
我們有了exec族函數後,我們典型的父子程序程式是這樣的:子程序需要運作的程式被單獨編寫、單獨編譯連接配接成一個可執行程式(叫hello),(項目是一個多程序項目)主程式為父程序,fork建立了子程序後在子程序中exec來執行hello,達到父子程序分别做不同程式同時(宏觀上)運作的效果。
4.2 exec族的6個參數
(1)execl和execv:這兩個函數是最基本的exec,都可以用來執行一個程式,差別是傳參的格式不同。execl是把參數清單(本質上是多個字元串,必須以NULL結尾)依次排列而成(l其實就是list的縮寫),execv是把參數清單事先放入一個字元串數組中,再把這個字元串數組傳給execv函數。
(2)execlp和execvp:這兩個函數在上面2個基礎上加了p,較上面2個來說,差別是:上面2個執行程式時必須指定可執行程式的全路徑(如果exec沒有找到path這個檔案則直接報錯),而加了p的傳遞的可以是file(也可以是path,隻不過相容了file。加了p的這兩個函數會首先去找file,如果找到則直接執行,如果沒找到則會去環境變量PATH所指定的目錄下去找,如果找到則執行如果沒找到則報錯)
(3)execle和execvpe:這兩個函數較基本exec來說加了e,函數的參數清單中也多了一個字元串數組envp形參,e就是environment環境變量的意思,和基本版本的exec的差別就是:執行可執行程式時會多傳一個環境變量的字元串數組給待執行的程式。
//頭檔案包含
#include <unistd.h>
//函數用法
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);
//函數舉例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(void)
{
pid_t pid = -1;
pid_t ret = -1;
int status = -1;
pid = fork();
if (pid > 0)
{
// 父程序
printf("parent, 子程序id = %d.\n", pid);
}
else if (pid == 0)
{
// 子程序
//execl("/bin/ls", "ls", "-l", "-a", NULL); // ls -l -a
//char * const arg[] = {"ls", "-l", "-a", NULL};
//execv("/bin/ls", arg);
//execl("hello", "aaa", "bbb", NULL);
//char * const arg[] = {"aaa", "bbb", NULL};
//execv("hello", arg);
//execlp("ls", "ls", "-l", "-a", NULL);
char * const envp[] = {"AA=aaaa", "XX=abcd", NULL};
execle("hello", "hello", "-l", "-a", NULL, envp);
return 0;
}
else
{
perror("fork");
return -1;
}
return 0;
}
5. 程序狀态和關系
5.1 程序的5種狀态
(1)就緒态。目前所有運作條件均就緒,隻要得到了CPU時間就能直接運作。
(2)運作态。就緒态時得到了CPU就進入運作态開始運作。
(3)僵屍态。程序已經結束但是父程序還沒來得及回收。
(4)等待态(淺度睡眠&深度睡眠),程序在等待某種條件,條件成熟後可進入就緒态。等待态下就算你給他CPU排程程序也無法執行。淺度睡眠等待時程序可以被(信号)喚醒,而深度睡眠等待時不能被喚醒隻能等待的條件到了才能結束睡眠狀态。
(5)暫停态。暫停并不是程序的終止,隻是被被人(信号)暫停了,還可以恢複的。
5.2 程序間的關系
(1)無關系:兩個程序之間是完全獨立的。無關的程序之間不能随便通路。
(2)父子程序:父程序繼承給子程序的東西都是fork之前的。子程序變為僵屍程序後,需要父程序對它進行回收。
(3)程序組(group):由若幹程序構成一個程序組。也就是屬主所在的組,他們的程序組ID相同,放在一個組的目的是為了友善管理這些程序。組内程序的關系比組外的更有聯系。
(4)會話(session):會話就是程序組的組。由若幹程序組構成的組,就像幾個班構成一個年級一樣。
6. 守護程序
6.1 什麼是守護程序?
daemon,表示守護程序,簡稱為d。通過ps指令檢視的程序,有一些名字後面帶d的(基本上就是守護程序)。守護程序是一種長期運作的狀态(一般是開機運作直到關機時關閉)。它與控制台脫離(普通程序都和運作該程序的控制台相綁定,表現為如果終端被強制關閉了則這個終端中運作的所有程序都會被關閉,背後的問題還在于會話,一個終端運作的所有程序同屬于一個會話,是以終端關閉之後,也就表示會話關閉,是以裡面的程序會全部被關閉。然而對于守護程序來說,隻能通過kill -9 xxx來實作程序的退出)。
注:ps檢視目前目錄下的程序,ps -ajx偏向顯示各種有關的ID号,ps -aux偏向顯示程序各種占用資源。
從圖中我們可以看到,對于守護程序,它的TTY為?,表示不依賴于某個終端。
6.2 守護程序的作用
守護程序就是一個背景一直運作的程式,它可以背景幫助我們完成一些任務。例如伺服器(Server),伺服器程式就是一個一直在運作的程式,可以給我們提供某種服務(譬如nfs伺服器給我們提供nfs通信方式),當我們程式需要這種服務時我們可以調用伺服器程式(和伺服器程式通信以得到伺服器程式的幫助)來進行這種服務操作。伺服器程式一般都實作為守護程序。守護程序的打開為打開某個程式時,該程式内部有打開該守護程序的代碼,即使該程式結束了,它内部的守護程序也不會随之結束。
常見的守護程序還有:syslogd,系統日志守護程序,提供syslog功能。cron,用來實作作業系統的時間管理,linux中實作定時執行程式的功能就要用到cron。
6.3 實作簡單的守護程序
任何一個程序都可以将自己實作成守護程序,實作守護程序需要具有如下幾個要素:
(1)子程序等待父程序退出(變為孤兒程序)
(2)子程序使用setsid建立新的會話期,脫離控制台
(3)調用chdir将目前工作目錄設定為/
(4)umask設定為0以取消任何檔案權限屏蔽
(5)關閉所有檔案描述符
(6)将0、1、2定位到/dev/null(這個相當于資源回收筒的存在)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void create_daemon(void);
int main(void)
{
create_daemon();
while (1)
{
printf("I am running.\n");
sleep(1);
}
return 0;
}
// 函數作用就是把調用該函數的程序變成一個守護程序
void create_daemon(void)
{
pid_t pid = 0;
//建立一個子程序
pid = fork();
if (pid < 0)
{
perror("fork");
exit(-1);
}
if (pid > 0)
{
exit(0); // (1)父程序直接退出
}
// 執行到這裡就是子程序
//(2)setsid将目前程序設定為一個新的會話期session,目的就是讓目前程序脫離控制台。
pid = setsid();
if (pid < 0)
{
perror("setsid");
exit(-1);
}
//(3)将目前程序工作目錄設定為根目錄
chdir("/");
//(4)umask設定為0確定将來程序有最大的檔案操作權限
umask(0);
//(5)關閉所有檔案描述符
// 先要擷取目前系統中所允許打開的最大檔案描述符數目
int cnt = sysconf(_SC_OPEN_MAX);
int i = 0;
for (i=0; i<cnt; i++)
{
close(i);
}
//(6)将0、1、2定位到/dev/null
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
}
由于守護程序無法通過正常的關閉控制台來退出,每執行一次就會出現一個程序,那麼有什麼方法來保證程式不被多次運作呢?
答:我們希望我們的程式具有一個單例運作的功能。意思就是說當我們./a.out去運作程式時,如果目前還沒有這個程式的程序運作則運作之,如果之前已經有一個這個程式的程序在運作則本次運作直接退出(提示程式已經在運作)。最常用的一種方法就是:用一個檔案的存在與否來做标志。具體做法是程式在執行之初去判斷一個特定的檔案是否存在,若存在則标明程序已經在運作,若不存在則标明程序沒有在運作。然後運作程式時去建立這個檔案。當程式結束的時候去删除這個檔案即可。
7. 程序間通信
7.1 為什麼要進行程序間通信?
(1)程序間通信(IPC)指的是2個任意程序之間的通信。(通信說白了就是在我這裡變了,你那裡能夠知道我的改變)
(2)同一個程序在一個位址空間中,是以同一個程序的不同子產品(不同函數、不同檔案)之間都是很簡單的(很多時候都是全局變量、也可以通過函數形參實參傳遞)
(3)2個不同的程序處于不同的位址空間,是以要互相通信很難。
7.2 什麼樣的程式設計需要程序間通信?
(1)99%的程式是不需要考慮程序間通信的。因為大部分程式都是單程序的(可以多線程)
(2)複雜、大型的程式,因為設計的需要就必須被設計成多程序程式(我們整個程式就設計成多個程序同時工作來完成的模式),常見的如GUI、伺服器。
(3)結論:IPC技術在一般中小型程式中用不到,在大型程式中才會用到。
7.3 程序間通信方式
1、管道
管道分為無名管道和有名管道,預設情況下說管道的話都指的是無名管道。無名管道指抽象出來的檔案是沒有名字的,有名管道表現形式為一個有名字的檔案(相當于一個暗号)。
(1)無名管道:
管道通信的原理:核心維護的一塊記憶體,有讀端和寫端(管道是單向通信的),通過一種讀寫檔案的方式來實作程序禦錦城之間的通信(A程序将資訊寫入到記憶體檔案中,B程序去記憶體檔案中讀取内容)。
管道通信的限制:由于管道通信是一種類似于讀寫檔案的形式。是以隻能在父子程序間通信(因為是通過繼承fd來完成的)。并且有可能被别人搶讀,或者自己寫的自己讀了。
管道通信的函數:pipe、write、read、close
為了解決管道通信的穩定性,我們采用了将每個程序閹割一部分讀寫功能的方式(例如上圖中将程序200的寫功能和程序201的讀功能去除,實作一個單工管道),同樣的方法進行第二條管道的設計,最終實作一種2個單工通道構成的半雙工通道,實作雙管道的通信方式。
(2)有名管道:
管道通信的原理:與無名管道的原理相同,隻不過是讀寫的檔案是一個有名字的檔案。
有名管道的使用方法:固定一個檔案名,2個程序分别使用mkfifo建立fifo檔案,然後分别open打開擷取到fd,然後一個讀一個寫。
管道通信限制:半雙工(注意不限父子程序,任意2個程序都可,這是因為讀寫的檔案是一個實際存在的有名檔案)。
管道通信的函數:mkfifo、open、write、read、close。
2、SystemV IPC
系統通過一些專用API來提供SystemV IPC功能,它分為信号量、消息隊列、共享記憶體。它的實質也是核心提供的公共記憶體。
(1)消息隊列:
本質上是一個隊列,隊列可以了解為(核心維護的一個)FIFO。入隊列就相當于往隊尾放,出隊列就相當于從隊列頭取。
工作時A和B2個程序進行通信,A向隊列中放入消息,B從隊列中讀出消息。
(2)信号量:
實質就是個計數器(其實就是一個可以用來計數的變量,可以了解為int a)。
通過計數值來提供互斥(某個程序在使用某個東西時,先去檢查信号量為0還是1,如果為0表示空閑,則該程序可以去用這個東西,但是在用之前要先将信号量變為1,這樣别的程序想使用這個東西時檢查信号量為1,它就不可以使用。并且在用完時要将信号量重新變為0)和同步(有一個公共的信号量為S,A程序走一步後給S加1,B程序去檢查這個S來調整自己的步伐,我用完了給你你用完了給我,相當于一個鎖)。互斥和同步便是信号量所完成的兩個程序之間的2種常用的通信需求。
(3)共享記憶體:
大片記憶體直接映射(兩個程序共享一塊記憶體,一個程序寫入指定的地方,然後另一個程序去指定地方讀,最終實作了一種好像複制過去的感覺)。類似于LCD顯示時的顯存用法。
3、信号:見第三章信号。
4、Socket域套接字:是一種網絡程式設計的形式。
三、linux中的信号
1. 什麼是信号?
信号是内容受限的一種異步通信機制,它的目的是用來實作通信。信号可以被了解成一種軟體中斷,它本質上是int型的數字編号(大部分是實作設定好的)。
信号由誰發出?
(1)使用者在終端按下按鍵(比如ctrl+C)
(2)硬體異常後由作業系統核心發出信号(比如除以0以後,計算機檢查到會自動報錯)
(3)使用者使用kill指令向其他程序發出信号
(4)某種軟體條件滿足後也會發出信号,如alarm鬧鐘時間到會産生SIGALARM信号,向一個讀端已經關閉的管道write時會産生SIGPIPE信号
信号有什麼處理方法?
(1)忽略信号(相當于别人發送了資訊,我不回)
(2)捕獲信号(信号綁定了一個函數,通過這個信号去做了有意義的事情)
(3)預設處理(目前程序沒有明顯的管這個信号,預設:忽略或終止程序),作業系統給每個信号定義了一個預設的動作,如果不是顯示的操作這個信号,那麼則會對應系統制定的預設方式。主動的處理方式會去覆寫預設處理方式。
2. 常見的信号介紹
信号名字 | 信号編号 | 處理内容 |
---|---|---|
SIGINT | 2 | Ctrl+C時OS送給前台程序組中每個程序(int為interrupt,有打斷中斷的意思) |
SIGABRT | 6 | 調用abort函數,程序異常終止 |
SIGPOLL/SIGIO | 8 | 訓示一個異步IO事件,在進階IO中提及 |
SIGKILL | 9 | 殺死程序的終極辦法(該信号不能被忽略) |
SIGSEGV | 11 | 無效存儲通路時OS發出該信号(通路了不該通路的位置) |
SIGPIPE | 13 | 涉及管道和socket |
SIGALARM | 14 | 涉及alarm函數的實作 |
SIGTERM | 15 | kill指令發送的OS預設終止信号 |
SIGCHLD | 17 | 子程序終止或停止時OS向其父程序發此信号(回收僵屍程序時候使用) |
SIGALARM | 14 | 涉及alarm函數的實作 |
SIGUSR1 | 10 | 使用者自定義信号,作用和意義由應用自己定義 |
SIGUSR2 | 12 | 使用者自定義信号,作用和意義由應用自己定義 |
信号是一開始就定義好了名字,編号和作用内容的,是以調用對應的信号就可以實作對應的效果。SIGUSR1和SIGUSR2這兩個信号是需要使用者去指定作用内容的,沒有進行事先定義(一般用來程序間通信)。
3. 程序對信号的處理
我們可以使用signal函數或sigaction函數來進行信号處理函數的注冊和綁定。這包括注冊事先未被定義的信号或修改已經被定義的信号。
signal函數使用方法
//頭檔案包含
#include <signal.h>
//函數用法
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//用法舉例
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
typedef void (*sighandler_t)(int);
void func(int sig)
{
if (SIGINT != sig)
return;
//設計一個傳回信号值的函數
printf("func for signal: %d.\n", sig);
}
int main(void)
{
//這裡指定信号值為2,相當于把Ctrl^C綁定為了信号處理
sighandler_t ret = (sighandler_t)-2;
signal(SIGINT, func);
//signal(SIGINT, SIG_DFL); // 指定信号SIGINT為預設處理
//ret = signal(SIGINT, SIG_IGN); // 指定信号SIGINT為忽略處理
if (SIG_ERR == ret)
{
perror("signal:");
exit(-1);
}
printf("before while(1)\n");
while(1);
printf("after while(1)\n");
return 0;
}
signal函數綁定一個捕獲函數後信号發生後會自動執行綁定的捕獲函數,并且把信号編号作為傳參傳給捕獲函數。
signal的傳回值在出錯時為SIG_ERR,綁定成功時傳回舊的捕獲函數。
signal函數的優點和缺點
(1)優點:簡單好用,捕獲信号常用
(2)缺點:無法簡單直接得知之前設定的對信号的處理方法
sigaction函數使用方法
sigaction比signal好的一點:sigaction可以一次得到設定新捕獲函數和擷取舊的捕獲函數(其實還可以單獨設定新的捕獲或者單獨隻擷取舊的捕獲函數),而signal函數不能單獨擷取舊的捕獲函數而必須在設定新的捕獲函數的同時才擷取舊的捕獲函數。
//函數用法
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
//1.如果想要擷取舊的捕獲函數
rtn = sigaction(signum, NULL, oldact);
//2.如果想要擷取新的捕獲函數
rtn = sigaction(signum, newact, NULL);
//3.如果替換駁捕獲函數
rtn = sigaction(signum, newact, oldact);
利用alarm和pause來模拟sleep
因為alarm的作用是定時,然後到時間産生信号,pause的作用是将核心挂起(暫停)。兩者相結合即可實作挂起核心,然後到時間産生信号(喚醒暫停态)使得程式繼續運作。
pause函數的作用就是讓目前程序暫停運作,交出CPU給其他程序去執行。當目前程序進入pause狀态後目前程序會表現為“卡住、阻塞住”,要退出pause狀态目前程序需要被信号喚醒。
#include <stdio.h>
#include <unistd.h> // unix standand
#include <signal.h>
//定義了一個空函數
void func(int sig)
{
}
void mysleep(unsigned int seconds);
int main(void)
{
printf("before mysleep.\n");
mysleep(3);
printf("after mysleep.\n");
return 0;
}
void mysleep(unsigned int seconds)
{
struct sigaction act = {0};
//将函數與結構體内部的函數指針元素進行綁定
act.sa_handler = func;
//對信号處理函數進行注冊
sigaction(SIGALRM, &act, NULL);
alarm(seconds);
pause();
}