1.進(jìn)程相關(guān)概念
1.1程序和進(jìn)程
程序,是指編譯好的二進(jìn)制文件,在磁盤上,不占用系統(tǒng)資源(cpu、內(nèi)存、打開的文件、設(shè)備、鎖....)
進(jìn)程,是一個(gè)抽象的概念,與操作系統(tǒng)原理聯(lián)系緊密。進(jìn)程是活躍的程序,占用系統(tǒng)資源。在內(nèi)存中執(zhí)行。(程序運(yùn)行起來(lái),產(chǎn)生一個(gè)進(jìn)程)
程序 → 劇本(紙) 進(jìn)程 → 戲(舞臺(tái)、演員、燈光、道具...)
同一個(gè)劇本可以在多個(gè)舞臺(tái)同時(shí)上演。同樣,同一個(gè)程序也可以加載為不同的進(jìn)程(彼此之間互不影響)
如:同時(shí)開兩個(gè)終端。各自都有一個(gè)bash但彼此ID不同。
1.2并發(fā)
并發(fā),在操作系統(tǒng)中,一個(gè)時(shí)間段中有多個(gè)進(jìn)程都處于已啟動(dòng)運(yùn)行到運(yùn)行完畢之間的狀態(tài)。但,任一個(gè)時(shí)刻點(diǎn)上仍只有一個(gè)進(jìn)程在運(yùn)行。
例如,當(dāng)下,我們使用計(jì)算機(jī)時(shí)可以邊聽音樂(lè)邊聊天邊上網(wǎng)。 若籠統(tǒng)的將他們均看做一個(gè)進(jìn)程的話,為什么可以同時(shí)運(yùn)行呢,因?yàn)椴l(fā)。

1.3單道程序設(shè)計(jì)
所有進(jìn)程一個(gè)一個(gè)排對(duì)執(zhí)行。若A阻塞,B只能等待,即使CPU處于空閑狀態(tài)。而在人機(jī)交互時(shí)阻塞的出現(xiàn)時(shí)必然的。所有這種模型在系統(tǒng)資源利用上及其不合理,在計(jì)算機(jī)發(fā)展歷史上存在不久,大部分便被淘汰了。
1.4多道程序設(shè)計(jì)
在計(jì)算機(jī)內(nèi)存中同時(shí)存放幾道相互獨(dú)立的程序,它們?cè)诠芾沓绦蚩刂浦?,相互穿插的運(yùn)行。多道程序設(shè)計(jì)必須有硬件基礎(chǔ)作為保證。
時(shí)鐘中斷即為多道程序設(shè)計(jì)模型的理論基礎(chǔ)。 并發(fā)時(shí),任意進(jìn)程在執(zhí)行期間都不希望放棄cpu。因此系統(tǒng)需要一種強(qiáng)制讓進(jìn)程讓出cpu資源的手段。時(shí)鐘中斷有硬件基礎(chǔ)作為保障,對(duì)進(jìn)程而言不可抗拒。 操作系統(tǒng)中的中斷處理函數(shù),來(lái)負(fù)責(zé)調(diào)度程序執(zhí)行。
在多道程序設(shè)計(jì)模型中,多個(gè)進(jìn)程輪流使用CPU (分時(shí)復(fù)用CPU資源)。而當(dāng)下常見CPU為納秒級(jí),1秒可以執(zhí)行大約10億條指令。由于人眼的反應(yīng)速度是毫秒級(jí),所以看似同時(shí)在運(yùn)行。
1s = 1000ms, 1ms = 1000us, 1us = 1000ns 1000000000
實(shí)質(zhì)上,并發(fā)是宏觀并行,微觀串行!
1.5CPU和MMU


1.6進(jìn)程控制塊PCB
我們知道,每個(gè)進(jìn)程在內(nèi)核中都有一個(gè)進(jìn)程控制塊(PCB)來(lái)維護(hù)進(jìn)程相關(guān)的信息,Linux內(nèi)核的進(jìn)程控制塊是task_struct結(jié)構(gòu)體。
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h
文件中可以查看struct task_struct 結(jié)構(gòu)體定義。其內(nèi)部成員有很多,我們重點(diǎn)掌握以下部分即可:
- 進(jìn)程id。系統(tǒng)中每個(gè)進(jìn)程有唯一的id,在C語(yǔ)言中用pid_t類型表示,其實(shí)就是一個(gè)非負(fù)整數(shù)。
- 進(jìn)程的狀態(tài),有就緒、運(yùn)行、掛起、停止等狀態(tài)。
- 進(jìn)程切換時(shí)需要保存和恢復(fù)的一些CPU寄存器。
- 描述虛擬地址空間的信息。
- 描述控制終端的信息。
- 當(dāng)前工作目錄(Current Working Directory)。
- umask掩碼。
- 文件描述符表,包含很多指向file結(jié)構(gòu)體的指針。
- 和信號(hào)相關(guān)的信息。
- 用戶id和組id。
- 會(huì)話(Session)和進(jìn)程組。
- 進(jìn)程可以使用的資源上限(Resource Limit)。
1.7進(jìn)程狀態(tài)
進(jìn)程基本的狀態(tài)有5種。分別為初始態(tài),就緒態(tài),運(yùn)行態(tài),掛起態(tài)與終止態(tài)。其中初始態(tài)為進(jìn)程準(zhǔn)備階段,常與就緒態(tài)結(jié)合來(lái)看。

2.環(huán)境變量
環(huán)境變量,是指在操作系統(tǒng)中用來(lái)指定操作系統(tǒng)運(yùn)行環(huán)境的一些參數(shù)。通常具備以下特征:
① 字符串(本質(zhì)) ② 有統(tǒng)一的格式:名=值[:值] ③ 值用來(lái)描述進(jìn)程環(huán)境信息。
存儲(chǔ)形式:與命令行參數(shù)類似。char *[]數(shù)組,數(shù)組名environ,內(nèi)部存儲(chǔ)字符串,NULL作為哨兵結(jié)尾。
使用形式:與命令行參數(shù)類似。
加載位置:與命令行參數(shù)類似。位于用戶區(qū),高于stack的起始位置。
引入環(huán)境變量表:須聲明環(huán)境變量。extern char ** environ;
練習(xí):打印當(dāng)前進(jìn)程的所有環(huán)境變量
#include <stdio.h>
extern char **environ;
int main(void)
{
int i;
for(i = 0; environ[i] != NULL; i++){
printf("%s\n", environ[i]);
}
return 0;
}

2.1 常見環(huán)境變量
按照慣例,環(huán)境變量字符串都是name=value這樣的形式,大多數(shù)name由大寫字母加下劃線組成,一般把name的部分叫做環(huán)境變量,value的部分則是環(huán)境變量的值。環(huán)境變量定義了進(jìn)程的運(yùn)行環(huán)境,一些比較重要的環(huán)境變量的含義如下:
PATH
可執(zhí)行文件的搜索路徑。ls命令也是一個(gè)程序,執(zhí)行它不需要提供完整的路徑名/bin/ls,然而通常我們執(zhí)行當(dāng)前目錄下的程序a.out卻需要提供完整的路徑名./a.out,這是因?yàn)镻ATH環(huán)境變量的值里面包含了ls命令所在的目錄/bin,卻不包含a.out所在的目錄。PATH環(huán)境變量的值可以包含多個(gè)目錄,用:號(hào)隔開。在Shell中用echo命令可以查看這個(gè)環(huán)境變量的值:
echo $PATH
SHELL
當(dāng)前Shell,它的值通常是/bin/bash。
TERM
當(dāng)前終端類型,在圖形界面終端下它的值通常是xterm。xterm是一個(gè)X Window System上的終端模擬器,用來(lái)提供多個(gè)獨(dú)立的SHELL輸入輸出。終端類型決定了一些程序的輸出顯示方式,比如圖形界面終端可以顯示漢字,而字符終端一般不行。
LANG
語(yǔ)言和locale,決定了字符編碼以及時(shí)間、貨幣等信息的顯示格式。
HOME
當(dāng)前用戶主目錄的路徑,很多程序需要在主目錄下保存配置文件,使得每個(gè)用戶在運(yùn)行該程序時(shí)都有自己的一套配置。
2.2getenv函數(shù)
獲取環(huán)境變量值
#include <stdlib.h>
char *getenv(const char *name);
成功:返回環(huán)境變量的值;失?。篘ULL (name不存在)
2.3setenv函數(shù)
設(shè)置環(huán)境變量的值
int setenv(const char *name, const char *value, int overwrite);
成功:0;失?。?1
參數(shù)overwrite取值:
1:覆蓋原環(huán)境變量 (改變只是在該進(jìn)程中修改,進(jìn)程完成后和進(jìn)程開始前環(huán)境變量是不變的)
0:不覆蓋。(該參數(shù)常用于設(shè)置新環(huán)境變量,如:ABC = haha-day-night)
2.4unsetenv函數(shù)
刪除環(huán)境變量name的定義
int unsetenv(const char *name);
成功:0;失敗:-1
注意事項(xiàng):name不存在仍返回0(成功),當(dāng)name命名為"ABC="時(shí)則會(huì)出錯(cuò)。
3.進(jìn)程控制
3.1fork函數(shù)
創(chuàng)建一個(gè)子進(jìn)程。
pid_t fork(void);
失敗返回-1;成功返回(兩個(gè)):① 父進(jìn)程返回子進(jìn)程的ID(非負(fù)) ②子進(jìn)程返回 0
pid_t類型表示進(jìn)程ID,但為了表示-1,它是有符號(hào)整型。(0不是有效進(jìn)程ID,init最小,為1)
注意返回值,不是fork函數(shù)能返回兩個(gè)值,而是fork后,fork函數(shù)變?yōu)閮蓚€(gè),父子需【各自】返回一個(gè)
3.2getpid函數(shù)
獲取當(dāng)前進(jìn)程ID
pid_t getpid(void);
3.3getppid函數(shù)
獲取當(dāng)前進(jìn)程的父進(jìn)程ID
pid_t getppid(void);
區(qū)分一個(gè)函數(shù)是“系統(tǒng)函數(shù)”還是“庫(kù)函數(shù)”依據(jù):
1 是否訪問(wèn)內(nèi)核數(shù)據(jù)結(jié)構(gòu)
2 是否訪問(wèn)外部硬件資源
二者有任一 → 系統(tǒng)函數(shù);二者均無(wú) → 庫(kù)函數(shù)
練習(xí)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>//exit
int main(void)
{
pid_t pid;
pid = fork();//執(zhí)行fork() 就出現(xiàn)分支了
if (pid == -1 ) {
perror("fork");
exit(1);
} else if (pid > 0) {
printf("I'm parent pid = %d, parentID = %d\n", getpid(), getppid());
//getpid()獲取當(dāng)前進(jìn)程ID,getppid()獲取父進(jìn)程ID
} else if (pid == 0) {
printf("child pid = %d, parentID=%d\n", getpid(), getppid());
}
printf("-----------------finish-----------------------\n");//finish出現(xiàn)了2次
return 0;
}
3.4循環(huán)創(chuàng)建n個(gè)子進(jìn)程
一次fork函數(shù)調(diào)用可以創(chuàng)建一個(gè)子進(jìn)程。那么創(chuàng)建N個(gè)子進(jìn)程應(yīng)該怎樣實(shí)現(xiàn)呢?
簡(jiǎn)單想,for(i = 0; i < n; i++) { fork() }即可。但這樣創(chuàng)建的是N個(gè)子進(jìn)程嗎?

從上圖我們可以很清晰的看到,當(dāng)n為3時(shí)候,循環(huán)創(chuàng)建了(2^n)-1個(gè)子進(jìn)程,而不是N的子進(jìn)程。需要在循環(huán)的過(guò)程,保證子進(jìn)程不再執(zhí)行fork ,因此當(dāng)(fork() == 0)時(shí),子進(jìn)程應(yīng)該立即break;才正確。
練習(xí):通過(guò)命令行參數(shù)指定創(chuàng)建進(jìn)程的個(gè)數(shù),每個(gè)進(jìn)程休眠1S打印自己是第幾個(gè)被創(chuàng)建的進(jìn)程。如:第1個(gè)子進(jìn)程休眠0秒打印:“我是第1個(gè)子進(jìn)程”;第2個(gè)進(jìn)程休眠1秒打印:“我是第2個(gè)子進(jìn)程”;第3個(gè)進(jìn)程休眠2秒打印:“我是第3個(gè)子進(jìn)程”。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int n = 5, i; //默認(rèn)創(chuàng)建5個(gè)子進(jìn)程
if (argc == 2) {
n = atoi(argv[1]);
}
for (i = 0; i < n; i++) //出口1,父進(jìn)程專用出口
if (fork() == 0)
break; //出口2,子進(jìn)程出口,i不自增
//------------------------------------------------------------在這之前fork()出5個(gè)進(jìn)程,只有fork()!=0的,才能與i具增
//用循環(huán)因子i區(qū)分父進(jìn)程、子進(jìn)程
if (n == i) {
sleep(n);
printf("I am parent, pid = %d\n", getpid());
} else {
sleep(i);
printf("I'm %dth child, pid = %d\n", i+1, getpid());
}
return 0;
}
3.5子進(jìn)程和父進(jìn)程的關(guān)系
關(guān)于資源:子進(jìn)程得到的是除了代碼段是與父進(jìn)程共享的以外,其他所有的都是得到父進(jìn)程的一個(gè)副本,子進(jìn)程的所有資源都繼承父進(jìn)程,得到父進(jìn)程資源的副本,既然為副本,也就是說(shuō),二者并不共享地址空間。兩個(gè)是單獨(dú)的進(jìn)程,繼承了以后二者就沒(méi)有什么關(guān)聯(lián)了,子進(jìn)程單獨(dú)運(yùn)行。(采用寫時(shí)復(fù)制技術(shù))
關(guān)于文件描述符:繼承父進(jìn)程的文件描述符時(shí),相當(dāng)于調(diào)用了dup函數(shù),父子進(jìn)程共享文件表項(xiàng),即共同操作同一個(gè)文件,一個(gè)進(jìn)程修改了文件,另一個(gè)進(jìn)程也知道此文件被修改了。