1. 程式人生 > >程序控制與程序間通訊

程序控制與程序間通訊

在學習這部分之前,我對Linux系統基本不瞭解,只是做一些簡單的工作,使用一些常見命令,使用Makefile編譯工程,做arm交叉編譯等。

所以這部分內容也會對用到的相關內容做一些總結。

1 程序

1.1 程序(Process)

程式本身是指令的有序集合,程序是程式在處理器上的一次執行過程。

程式中包含了建立程序需要的資訊。

A program is a file containing a range of information that describes how to construct
a process at run time.

從核心角度來看,process包含user-space中為程式和變數分配的儲存,和核心中的一些資料結構。

和程序相關的資訊儲存在 task_struct 結構體中,這個結構體定義在linux/sched.h中。

但《The Linux Programming Interface》中沒有task的概念,有process,thread的概念,PID指的就是Process ID。除了一些system process,PID和程式沒有固定的關係,也就是執行一個程式時建立的程序是不固定的。PID數值是有上限的,可以調整。

Process & Thread:

A process is an instance of an executing program.

A single process can contain multiple threads.

All of these threads are independently executing the same program, and they all share the same global memory, including the initialized data, uninitialized data, and heap segments. (A traditional UNIX process is simply a special case of a multithreaded processes; it is a process that contains just one thread.)

這裡還沒有具體看程序的屬性和生命週期等概念。

1.2 建立程序

1.2.1 理解程序的建立

建立程序對使用者來說有兩種方式:

  • 在shell中執行命令或可執行檔案,其實是由shell程序呼叫了fork函式建立子程序
  • 在自己寫的程式碼中呼叫fork函式來建立子程序

pstree 命令可以通過一個樹的方式,列出系統中的所有程序。可以看到下圖中所有的程序都是由init程序建立的(systemd是init程序的一種實現)。init程序即程序1,,其PID固定為1,它是由程序0(PID為0的程序)建立的,程序0由核心建立。程序0在建立程序1後,轉換為交換程序或空閒程序。

Linux系統中除了程序0,所有的程序都是由父程序呼叫fork函式建立的。
在這裡插入圖片描述

1.2.2 fork函式

函式原型為pid_t fork(void), 標頭檔案為unistd.h

pid_t 就是 unsigned int

在程式中,fork(); 這條語句之後的程式(包括fork()這個語句)會以兩個程序來執行。之前的不受影響。同時fork 函式在被呼叫後,在子程序和父程序中的分別返回,返回值不同。 fork函式有三種返回值:

  1. 子程序中返回值為0 ,提提示當前執行在子程序中
  2. 父程序中返回值為子程序PID,讓父程序掌握所建立子程序的PID
  3. 出錯返回-1

因此可以判斷fork函式的返回值,從而在父程序和子程序中執行不同的程式。使用fork函式時,返回值非常重要。

父子程序的異同:

  • 相同:

    1. 環境變數
    2. 開啟的檔案
    3. 相同的使用者ID
  • 不同

    1. fork返回值

    2. 程序ID

    3. 子程序的tms_utime,tms_stime,tms_cutime,tms_ustime都被清零

1.2.3 fork的應用場景

  1. 希望複製父程序(共享程式碼,複製資料空間),但父子程序通過條件語句執行相同程式碼中的不同分支。

    比如:網路服務程式,父程序等待客戶端的服務請求,當請求到達後,就呼叫fork建立一個子程序來處理該請求。而父程序繼續等待下一個服務請求。

  2. 父子程序執行不同的可執行檔案。即在子程序中,呼叫exec類函式執行另一個可執行檔案。

    vfork函式用於建立新程序,目的是執行另一個可執行檔案。vfork函式保證子程序先執行。

1.3 程序的終止

正常終止:

  1. main函式返回
  2. 呼叫函式exit(), #include<stdlib.h>
  3. 呼叫函式_exit(),#include<unistd.h>

exit()和 _exit()的區別在於_exit()會直接中止程式,而exit()會先“重新整理I/O緩衝”。

關於I/O緩衝:比如printf()執行後,此時會立刻執行下一條語句,而不是等待I/O完成後再執行下一條語句,以此來節省系統呼叫的次數,從而節省程式執行時間。而具體的write()系統呼叫是等到I/O緩衝滿足某些條件後才會實際執行。

I/O緩衝區屬於記憶體,所以速度很快。即使我們不是對硬碟進行讀寫,是輸出到控制檯,肯定也同樣需要時間,顯示器也是屬於外設,不可能比訪問記憶體更快。

img

I/O緩衝區在特定的條件下才會執行系統呼叫write()來向外設進行寫程式。I/O緩衝區也有不同的型別,不同型別的I/O緩衝執行系統呼叫的條件不同,鍵盤,顯示器這種字元裝置是行緩衝,因此會在緩衝區中出現\n時執行系統呼叫。

可使用int fflush (FILE *__stream); 來手動重新整理緩衝區。

異常終止:

  1. abort()函式
  2. 程序接收到某訊號

1.4 子程序的結束

1.4.1 獲知子程序執行狀態

子程序執行結束後,不管正常或異常,都需要等待父程序來回收它的狀態資訊,之後才會從程序分配表中消失,也就是子程序結束,同時子程序的PCB等核心空間資源才被釋放。對於已經終止,但父程序尚未對其呼叫wait函式或waitpid函式的程序,其狀態為TASK_ZOMBIE,稱為殭屍程序。

回收子程序狀態資訊的函式有:

// sys/wait.h
pid_t wait(int *stat_loc);
pid_t waitpid(pid_t pid, int *stat_loc, int options);

pid_t wait(int *stat_loc) 其返回值是子程序的id,*stat_loc其實也算是一個返回值,wait()函式內會對其進行賦值,其意義是子程序的狀態。 當傳入空指標時,只是為了等待子程序終止,傳入非空指標時,才會將子程序狀態改變資訊存放在它指向的儲存空間中。

系統提供一些帶參巨集來解析 stat_loc。

父程序是有可能在子程序終止之前終止的,因為父子程序是獨立的,沒有關聯。此時子程序的父程序將變為init程序,由init程序進行回收。

1.4.2 wait函式和waitpid函式的區別

如果一個程序有多個子程序,只要有一個子程序狀態改變,那麼wait函式就會返回。 此時可以通過返回的PID來判斷是不是我們想要的子程序,不是的話,則迴圈呼叫wait函式。

waitpid函式可以等待某個特定子程序的狀態改變,第一個引數是PID,第三個引數是等待的選項,可以為0,也可以設為三種常量,實現特定的功能,比如可將waitpid設定為非阻塞。waitpid顯然也可以傳入適當的引數實現和wait函式相同的功能。

wait函式功能:等待一個子程序狀態改變,會阻塞父程序(實現同步)。

waitpid函式功能

  1. 等待一個特定程序的狀態改變

  2. 實現非阻塞的等待操作,只是取得子程序狀態改變資訊,不等待其改變

  3. 支援程序組的控制(通過第一個引數)

1.5 執行一個新程式

  • execute 系列函式。有6個函式,函式名以exec開頭,之後為4個字母的組合。四個字母是:
    1. l 表示list,每個命令列引數作為一個單獨的引數
    2. v 表示vector,命令列引數放在陣列中
    3. e 表示Environment,表示由函式呼叫者提供環境變數
    4. p 表示PATH,表示通過環境變數來指定路徑,查詢可執行檔案

1.5.1 execve函式

// unistd.h
int execve(const char *path, const char *argv[], const char *envp[]);

execve函式是核心級系統呼叫,其他函式都是依賴execve函式。

execve()啟動的新程式會覆蓋當前程式的記憶體空間,execve()如果返回負值,表示呼叫失敗。如果呼叫成功,就直接執行新程式,不會返回。

The most frequent use of execve() is in the child produced by a fork(), although
it is also occasionally used in applications without a preceding fork().

注意 char *argv[]char *envp[]是字元指標的陣列。我還沒理解第三個引數具體做什麼,似乎是用於引數傳遞。

TLPI是這麼寫的:

The envp argument corresponds to the environ array of the new program;

unistd.h的原始碼,其中寫了一句extern char **environ; 。沒有看到execve函式的具體實現,但是應該是在這個函式中,將全域性變數environ賦值為envp。 environ是個全域性變數。在被啟動的程式中,可以宣告extern **environ,然後就可以取得啟動它的程式傳過來的引數。

關於char **achar *b[] 的區別:

char **a 是多級指標,只能用指標為其賦值,比如char **a = b; 這樣是沒有問題的。但是不能直接用字串為其賦值。

char *b[] 是一個字串常量的陣列,因此可以這樣賦值:char *b[] = {"12","34",NULL};char *定義的是字串常量。)

例子:

#include<unistd.h>
#include<stdio.h>

int main(int argc, char *argv[])
{
        char *envVec[] = {NULL};

        char *argVec[] = {"echo", "Hello World",NULL};

        char **tt = argVec;
		//execve("/bin/echo", argVec, envVec); 
        //Have same result as the next line.
        execve("/bin/echo", tt, envVec);
        return 0;
}

1.5.2 其他函式

比如:

int execl(const char *pathname,const char *arg0, ...,NULL);

可以看到這個函式exec後是字母l,以可變引數的形式傳入多個命令列引數,以空指標NULL結束。同時這個函式沒有傳入新的環境變數。

2 執行緒(Thread)

  • 本來主要是為了做作業,看程序的相關概念。但為了避免執行緒和程序概念混淆,以及為了更好地理解程序,所以把執行緒相關概念也看了。

程序是資源分配的單位。同一個程序中的不同執行緒可以共享程序的全域性變數,開啟的檔案等等。

對沒有通過程式顯式建立執行緒的程式,執行時可以看成只有一個執行緒的程序。

程序操作和執行緒操作:

在這裡插入圖片描述

執行緒操作的標頭檔案是 pthread.h。

執行緒ID的型別是pthread_t ,實際上是unsigned long int , 型別定義在 /usr/include/bits/pthreadtypes.h中。

3 程序間通訊

3.1 Interprocess Communication (IPC)

The Linux Programming Interface 的Chapter 43開始對IPC的介紹。

This chapter presents a brief overview of the facilities that processes and threads
can use to communicate with one another and to synchronize their actions.

也包括了threads的通訊.

按照功能分三個大類:

  1. communication
  2. signal
  3. synchronization

IPC現在仍有兩種標準: System V和POSIX。
兩種標準IPC實現方式有所不同,但是都有訊息佇列,訊號量和共享記憶體。
對使用者(我)來說,現在比較關心的是如何使用。
見我的另一篇總結 對程序的理解和POSIX 訊號量的使用

參考:

[1] 《The Linux Programming Interface》
[2] 作業系統
[3] linux下多種鎖的比較
[4] Linux作業系統程式設計