1. 程式和程序、線程是什麼?
程式定義:計算機程式是指電腦能夠識别的機器指令的集合。
- 在linux系統中,簡單來說程式就是一種靜态的、被存儲在計算機的指定位置(磁盤)上的檔案,其中包含一些計算機能夠識别的指令和資料,也就是所謂的可執行檔案。
- 其實正如我們運作一個程式(可執行檔案),通常在 Shell中輸入指令運作就可以了,在這運作的過程中包含了程式到程序轉換的過程,整個轉換過程主要包含以下 3 個步驟:(1) 查找指令對應程式檔案的位置。(2)使用 fork()函數為啟動一個新程序。(3)在新程序中調用 exec 族函數裝載程式檔案,并執行程式檔案中的main()函數。
程序定義:程序是一個正在執行的程式的執行個體。
- 通常我們所說的程式運作,是指磁盤中的程式(靜态的實體檔案)被拿到電腦記憶體中去了,這個時候,也就産生了所謂的程序(系統動态執行某程式(可執行檔案)中的指令)。
- 程序通過享有系統資源來執行程式,例如CPU\記憶體\磁盤\外設\程序ID等資訊和資源。程序是系統中程式執行和資源配置設定的基本機關。每個程序都擁有自己的資料段、代碼段和堆棧段。
線程定義:線程(英語:thread)是作業系統能夠進行運算排程的最小機關。它被包含在程序之中,是程序中的實際運作機關。
- 程序是資源配置設定的最小機關,線程是程式執行的最小機關。一個标準的線程由線程ID,目前指令指針(PC),寄存器集合和堆棧組成。另外,線程是程序中的一個實體,是被系統獨立排程和分派的基本機關,線程是一種輕量級的程序,也就是說線程可以簡化由程序帶來的系統資源的開銷。
- 線程自己不擁有系統資源,隻擁有一點兒在運作中必不可少的資源,一個程序可以有多個線程來執行,但它可與同屬一個程序的其它線程共享程序所擁有的全部資源。一個線程可以建立和撤消另一個線程,同一程序中的多個線程之間可以并發執行。
程式并不能單獨執行,隻有将程式加載到記憶體中,系統為他配置設定資源後才能夠執行,這種執行的程式稱之為
程序,也就是說程序是系統進行資源配置設定和排程的一個獨立機關,每個程序都有自己單獨的位址空間。程式由文
件變成運作狀态,至少需要一個程序,而一個程序至少需要一個線程來幹活。線程使用的資源是程序的資源,但
線程占用的資源要遠小于程序所占到的資源,故程序崩潰則該程序下的線程也随之崩潰。
2. Linux下程序、線程的基礎知識
前言:我們都知道,Linux是一個多任務系統,它使用某種排程政策支援多個任務并發執行。事實上,單核處理器在某一個時刻隻能執行一個任務,而Linux采用先來先服務、短作業優先、優先權排程等排程算法使得任務會頻繁地切換執行,是以給使用者多個任務同時運作的感覺。而多任務會帶來多個程序,當有多個程序時,對系統而言,什麼最能區分出程序之間的差別呢?答案就是程序辨別符(PID),Linux通過每個程序的唯一辨別符來區分程序。而這麼多程序既然是通過排程算法,切換運作,那麼某一時刻未正在運作的程序、等待運作的程序、已經運作完的程序等等,它們是不是有不同的狀态呢?如果說我想讓一個程序停止,是不是要改變程序的狀态呢?這就涉及到程序的狀态和狀态轉換了:
Linux系統中終端中可使用
ps -e -l
指令檢視所有程序狀态,其中STAT一欄對應的狀态說明如下:
狀态 | 說明 |
---|---|
R | 可運作狀态。表示程序在運作隊列中,處于正在執行或即将運作狀态,隻有該狀态的程序才可能在CPU上運作,而同一時刻可能有多個程序處于可運作狀态 |
S | 可中斷的睡眠狀态。處于這個狀态的程序因為等待某種事件的發生而被挂起,比如程序在等待信号 |
D | 不可中斷的睡眠狀态。通常是在等待輸入或者輸出(I/O)完成,處于這種狀态的程序不能響應異步信号 |
T | 停止狀态。通常是被shell的工作信号控制,或因為它被追蹤,程序正處于調試器的控制之下 |
Z | 僵死狀态。程序成為僵屍程序 |
X | 退出狀态。程序即将被回收 |
I | 程序是多線程的 |
s | 程序是會話首程序 |
. | 程序屬于前台程序組 |
< | 高優先級任務 |
Linux程序間狀态轉換圖:

linux中程序相關常用指令:
把程序運作到背景: ./a.out &
jobs 檢視背景程序
ps 檢視系統程序
top 動态顯示程序
nice 按使用者指定的優先級運作程序
{
優先級大小: -20 ~ 19
nice -[n] [參數] 例子: nice -9 ./a.out 将程式的優先級設定為9 --9表示 -9
}
renice 改變正在運作程序的優先級
{
renice N PID 例子: renice 9 1024 沒有 - 字首.
renice N -u farsight 将屬于使用者farsight的程序
renice N -g group 将屬于group組的程序
}
kill 向程序發信号
bg 将挂起的程序在背景執行
fg 把背景運作的程序放到前台
(具體指令用法可以man手冊檢視或另行百度)
《Linux中ps指令使用方法超連結》
《野火開源電子書程序章節超連結》
《Linux系統中的程序檢視及管理超連結》
3. linux下程序、線程相關代碼實作(C語言)
linux中的程序分類:
- 互動式程序: 顧名思義,互動式程序需要同使用者進行互動,需要等待使用者的輸入,這類程序能夠立刻響應,常見的有shell指令程序、文本編輯器的程序等。
- 批處理程序: 這類程序不需要同使用者互動,一般都是在背景運作,不必很快響應,故往往不會被系統優先排程,常見的有gcc的編譯操作程序等等。
- 守護程序: 這類程序一直在背景運作,和任何終端都沒有關聯,系統啟動時運作,開機時結束,當然中途也可以人為用kill指令殺死,很多系統的程序,例如服務程序都是守護程序的形式。
fork()函數的使用:
我們在linux系統中程式設計時,可以調用系統的fork函數來根據我們的需要建立一個新的程序。在一個程序中調用fork函數創造出的新程序成為子程序,原來的新程序為父程序。fork得到的子程序幾乎完全複制了父程序的内容,包括程序的虛拟記憶體空間的位址、程序的上下文、代碼段、程序堆棧、記憶體資訊、檔案描述符等等,故fork出來一個新程序會消耗掉同原來的程序一樣多的系統資源,現在的Linux系統為了節約資源,采用了寫時複制(Copy on write)技術,在系統建立程序時,資源沒有立刻被複制過來,而是被推遲到需要寫入資料的時候,在此之前子程序以隻讀的形式共享父程序的資源,這種方式比調用時馬上克隆執行的更快。
我們可以通過
man 2 fork
指令使用幫助手冊來了解fork函數的資訊和用法,這裡我們重點關注傳回值,fork傳回不同的傳回值用來為供我們區分父程序(程序辨別符(PID)(>0))、子程序(傳回0)和錯誤狀态(傳回-1)。
RETURN VALUE
Upon successful completion, fork() shall return 0 to the child process and shall
return the process ID of the child process to the parent process. Both processes
shall continue to execute from the fork() function. Otherwise, −1 shall be returned
to the parent process, no child process shall be created, and errno shall be set to
indicate the error.
ERRORS
The fork() function shall fail if:
EAGAIN The system lacked the necessary resources to create another process, or the
system-imposed limit on the total number of processes under execution sys‐
tem-wide or by a single user {CHILD_MAX} would be exceeded.
The fork() function may fail if:
ENOMEM Insufficient storage space is available.
The following sections are informative.
廢話不多說,直接上代碼:
/*************************************************************************
> File Name: fork.c
> Author: 許可
> Created Time: 2020年04月04日 星期六 10時22分28秒
> Description:
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> //fork()需要包含的頭檔案
#include <unistd.h> //fork()需要包含的頭檔案
int main(int argc, char *argv[])
{
pid_t retvalue;
retvalue = fork(); //調用fork()函數,調出的子程序從本行的下一行開始執行
if (retvalue == -1) //出錯
{
perror("fork error");
return -1;
}
else if (retvalue == 0) //子程序
{
while(1)
{
printf("child process,retvaluevalue is %d,My PID is %d\n",retvalue,getpid());
sleep(5);
}
}
else //父程序
{
while(1)
{
printf("parent process.retvaluevalue is %d.My PID is %d\n",retvalue,getpid());
sleep(5);
}
}
return 0;
}
運作代碼,試驗現象如下:
exec函數族的使用:
fork函數建立了一個跟父程序幾乎一樣的子程序,子程序能幹的活父程序也能自己幹,如果我們想讓子程序執行另外一個程式,做和父程序不一樣的事要怎麼辦呢?這裡我們就要用到exec函數,它可以根據指定的檔案名或目錄名找到可執行程式,并用它來取代目前程序的資料段、代碼段和堆棧段。當某個程序執行完exec函數後,除了程序号,其他的内容都被替換了,這樣就使得原來調用exec函數的程序另辟天地、重新做人了。
exec函數族基本可以滿足你在程式設計中操作任何一個linux中的可執行檔案(包括腳本)的需求,它是一種非常強大的API,強大到使用他的人也必須對他非常了解,不然的話使用時容易出錯,對于exec函數族的用法,我們可以使用
man 3 exec
指令來擷取相關資訊:
EXEC(3) Linux Programmer's Manual EXEC(3)
NAME
execl, execlp, execle, execv, execvp - 執行某個檔案
總覽 (SYNOPSIS)
#include <unistd.h>
extern char **environ;
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[]);
描述 (DESCRIPTION)
exec 系列 函數 用 新的 程序 映象 置換 目前的 程序 映象. 本 手冊頁 描述的 這些 函數
實際上 是 對 execve(2) 函數 的 前端(front-end) 包裝. (關于 目前程序 的 置換 詳見
execve 的 手冊頁.)
這些 函數 的 第一個 參數 是 待執行 程式 的 路徑名(檔案名).
在 函數 execl, execlp, 和 execle 中, const char *arg 以及 省略号 代表 的 參數 可被
視為 arg0, arg1, ..., argn. 他們 合起來 描述了 指向 null 結尾的 字元串 的 指針 列
表, 即 執行程式 的 參數清單. 作為 約定, 第一個 arg 參數 應該 指向 執行程式名 自身.
參數清單 必須 用 NULL 指針 結束!
execv 和 execvp 函數 提供 指向 null 結尾的 字元串 的 指針數組 作為 新程式 的 參數列
表. 作為 約定, 指針數組 中 第一個 元素 應該 指向 執行程式名 自身. 指針數組 必須 用
NULL 指針 結束!
execle 函數 同時 說明了 執行程序 的 環境(environment), 他 在 NULL 指針 後面 要求 一
個 附加參數, NULL 指針 用于 結束 參數清單, 或者說, argv 數組. 這個 附加參數 是 指向
null 結尾的 字元串 的 指針數組, 他 必須 用 NULL 指針 結束! 其他 函數 從 目前程序 的
environ 外部變量 中 擷取 新程序 的 環境.
某些 函數 有 特殊的 語義.
如果 提供的 檔案名 中 不包含 斜杠符(/), 函數 execlp 和 execvp 将 同 shell 一樣 搜尋
執行檔案. 搜尋路徑 由 環境變量 PATH 指定. 如果 該 變量 不存在, 則 使用 預設路徑
``:/bin:/usr/bin''. 另外, 某些 錯誤 要 特殊處理.
如果 對 某個 檔案 的 通路 遭到 拒絕 ( execve 傳回 EACCES), 這些 函數 将 在 搜尋路徑
中 繼續 尋找. 如果 沒有 找到 符合的 檔案, 他們 傳回時 把 errno 置為 EACCES.
如果 無法 識别 檔案首部 ( execve 傳回 ENOEXEC), 這些 函數 将 以 檔案名 作為 第一個
參數 調用 shell. (如果 這個 嘗試 失敗 就 不再 進行 搜尋 了.)
傳回值 (RETURN VALUE)
任何 exec 函數 傳回 均 表明 發生了 錯誤. 傳回值 是 -1, 全局變量 errno 指出 錯誤類
型.
檔案 (FILE)
/bin/sh
錯誤 (ERROR)
這些 函數 均可能 失敗, errno 被 置為 庫函數 execve(2) 設定的 各種 錯誤類型.
另見 (SEE ALSO)
sh(1), execve(2), fork(2), environ(5), ptrace(2)
相容性 (COMPATIBILITY)
在 某些 其他系統 中, 預設路徑 (當 環境變量 PATH 不存在) 把 目前目錄 列在 /bin 和
/usr/bin 後面, 這是 為了 防止 特洛伊木馬. Linux 在這兒 采取了 傳統的 "目前目錄優先"
預設路徑.
例如如果我們要通過父程序fork出來的一個子程序去幫我們自動執行
ls -l -a
指令,我們可以用以下c代碼實作:
/*************************************************************************
> File Name: exec.c
> Author: 許可
> Created Time: 2020年04月04日 星期六 12時49分45秒
> Description:
************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char *argv[])
{
pid_t ret;
ret = fork(); //建立子程序
if (ret == -1) //出錯
{
perror("fork error");
return -1;
}
else if (ret == 0) //子程序
{
printf("this is child process, id is %d\n",getpid());
printf("the process will use ls -l -a:\n");
if (execlp("/usr/bin/ls","-a","-l",NULL)<0) //調用execp()運作ls
{
perror("execlp error");
return -1;
}
//exit(EXIT_SUCCESS);
return 0;
}
else //父程序
{
printf("this is patent process ,id is %d,child procrss id is%d\n",getpid(),ret);
exit(EXIT_SUCCESS); //退出程序
}
return 0;
}
運作a.out試驗現象: