程序概念
使用者空間&& 核心空間
- 核心空間:作業系統(系統調用函數)和驅動程式運作在核心空間,核心空間是程序共享的。
- 使用者空間:應用程式運作在使用者空間,使用者空間是各程序私有的。注意:應用程式中的系統調用是運作在核心空間的,涉及到使用者空間到核心空間再到使用者空間的切換。
程式&& 程序
- 程式:靜态的源代碼或可執行檔案;
- 程序:動态的運作起來的程式執行個體;
- 作業系統用程序控制塊PCB表示建立的每一個程序,并将所有程序以連結清單的形式組織起來。
- 作業系統用程序号pid唯一辨別每一個程序,當所辨別的程序退出後,原來的辨別号便又可以被再次使用。
通常利用 ps aux | grep process_name
來檢視某程序的程序号,在程式内部使用getpid()函數檢視目前程序的pid。
程序狀态
- 運作态:程序占用CPU資源正在執行自己的指令;
- 就緒态:萬事俱備,隻欠CPU;
- 阻塞态:等待某事件(IO輸入事件、阻塞函數傳回)發生。
- ps指令下的程序狀态
- aux選項組合
-
ps aux | more
- a:顯示一個終端的所有程序;
- u:顯示程序的歸屬使用者及記憶體使用情況;
- x:顯示沒有關聯控制終端的程序。
- 參數解釋
- USER:程序的歸屬使用者(建立者)
- PID:程序id
- %CPU:程序占用CPU資源的百分比
- %MEM:程序占用記憶體資源的百分比
- VSZ:程序使用的虛拟記憶體大小
- RSS:程序使用的實體記憶體大小
- TTY:目前程序關聯的終端
- STAT:目前程序的狀态
- D:disinterruptible,不可被打斷的睡眠狀态,通常是等待某IO的結束。
- R:running,程序正在運作或已就緒(隻要被排程就随時可執行)
- S:sleep,表示可被打斷的、因被阻塞而進入的睡眠狀态的程序
- T:terminal,暫停狀态(CTRL+z,位于背景暫停或處于除錯狀态)
- X:死掉的狀态,ps指令看不到該狀态,因為一死程序就退出了。
- Z:zombie,僵屍狀态(雖已退出,但未被回收)
- t:被跟蹤狀态,即程序正在被gdb調試
- +:狀态字後面跟着+号表示這是一個前台程序(占用終端),沒有+表示這是一個背景程序。
-
axjf組合
ps axjf | more
- a:顯示一個終端的所有程序;
- x:顯示沒有關聯控制終端的程序。
- j:顯示程序歸屬的組gid、會話sid、父程序id
- f:以ASCII碼的形式顯示出程序的層次關系。
程序排程
- 程序是搶占式執行的。
- 排程算法
- 先來先服務
- 短作業優先
- 高優先級優先
- 時間片輪轉(毫秒級别:5—800)
- 并發/并行
- 并發:微觀上看是交替執行,即多個程序輪流占用一個CPU資源,隻是CPU時間片在人看來很短,讓你以為多個程序是同時運作的。
-
并行:微觀上看是同時執行,即多個程序分别占用一個CPU資源,同時運作各自的代碼。
介紹ps aux指令檢視到的資訊含義:
程序資訊
- 程序上下文
- 涵義:在程序運作時,系統中的各個寄存器中儲存了程序目前運作時的資訊,這些資訊就是程序上下文。當程序被排程器調離時,為了下一次能接着目前狀态執行,就要将目前寄存器中的值儲存到棧幀中,以便下一次程序被排程時恢複程序上次的執行狀态。
- 程式計數器(pc寄存器):最重要的一個上下文資訊,它記錄了程序下一次執行的開始位置(某一條彙編指令的位址)。
- 記憶體指針:指向程式位址空間
- 記賬資訊:記錄使用CPU時長、占用記憶體大小
- IO資訊:儲存程序打開的檔案資訊
- 每一個程序被建立的時候都會預設打開三個檔案:
- stdin:标準輸入(scanf()、getchar())
- stdout:标準輸出( printf())
- stderr:标準錯誤輸出(perror())
對于每一個程序,作業系統都會以程序号pid在/proc目錄下建立一個檔案夾,裡面存放該程序的相關資訊。在 /proc/程序号/fd
目錄下有三個軟連接配接檔案: 0(标準輸入域)、1(标準輸出)、2(标準錯誤)
-
一文了解程式及其通信方法
程序退出
- 正常退出
- 從
語句傳回return
- 調用
函數傳回(stdlib.h)exit()
- 調用
函數傳回(unistd.h)_exit()
- 異常退出
- Ctrl + c
- 指令異常(通路不存在的位址,如NULL等)
- 運算錯誤(除0)
- exit & _exit的差別
- exit函數比_exit函數多兩步:
- 執行使用者自定義的清理函數
#include<stdlib.h>
/*
* 功能:注冊一個函數,在程序終止的時候調用
* 被調用的函數隻能是傳回值類型為void的無參函數
*/
int atexit( void(*function)(void) )
- 沖刷緩沖區、關閉流等。
- 緩沖區:C标準庫定義的,而非核心。建立緩沖區的目的是減少IO次數(IO操作比較耗費時間)。當觸發重新整理緩沖區的條件後,緩沖區的内容才會繼續進行IO操作。
- 觸發重新整理緩沖區的條件
- exit()
- main()函數中的return語句
- fflush函數
- 回車符\n
- 沖刷方式
- 全緩沖(當緩沖區寫滿了一次性進行IO)
- 行緩沖(在輸入輸出中,遇到換行符時标準IO庫進行IO操作)
- 不帶緩沖(标準IO庫不對字元進行緩沖)
- 關閉流:标準輸入、标準輸出、标準錯誤
程序等待
- 為什麼要程序等待
- 已知子程序先于父程序退出,父程序如果不管不顧,子程序就會變成僵屍程序,進而造成記憶體洩漏問題。
- 程序一旦進入僵屍狀态,就會刀槍不入,“殺人魔王”kill -9也無能為力,因為誰也沒有辦法殺死一個死去的程序。但是,父程序給子程序的任務它完成的如何,我們需要知道。
- 父程序通過程序等待的方式,回收子程序資源,進而擷取子程序退出狀态資訊。
- 總而言之:父程序進行程序等待,等待子程序退出之後回收子程序的退出狀态資訊,防止子程序變成僵屍程序
- 程序等待函數
-
wait
- 原型:
#include<sys/wait.h>
/*
* 傳回值:成功傳回被等待程序的pid;失敗傳回-1
* 參數:輸出型參數,擷取子程序狀态,不關心可以設定為NULL
*/
pid_t wait(int* status);
- 特點
- 阻塞,直到等待的子程序退出。
-
waitpid
- 原型:
pid_t waitpid(pid_t pid,int* status,int options);
/*
傳回值: 1.成功傳回收集到的程序的pid;
2.如果設定了WNOHANG選項,且沒有子程序可以收集,則傳回0;
3.失敗傳回-1 并設定errno
參數:
1、pid:
pid = -1: 等待任意一個子程序,與wait等效
pid > 0 : 等待程序ID與pid相等的子程序
2、status:輸出型參數,擷取子程序狀态,不關心可以設定為NULL
3、options:
WNOHANG:非阻塞
*/
- 特點
- 當參數options被設定為WNOHANG後,為非阻塞:
- 當調用一個非阻塞函數的時候,函數會判斷資源是否準備好。如果準備好則執行函數功能并傳回;如果沒準備好,則函數報錯後傳回(注:函數功能并沒有完成)
- 要點:非阻塞要搭配循環來使用。
- 關于ststus參數
- 子程序正常退出:高位元組存儲子程序的退出狀态,第7位的coredump标志位設為0,低位元組的低7位也設為0。
- 子程序非正常退出:低位元組存儲子程序的終止信号,第7位的coredump标志位設為1。
- 子程序正常退出情況下,擷取status的值
//wait.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t ret = fork();
if(-1 == ret)
{
return -1;
}
else if(0 == ret)
{
//子程序
printf("I am child process,pid is %d\n",getpid());
sleep(5);
exit(100);
}
else
{
//父程序
int status = 0;
pid_t result = wait(&status);
if(-1 == result)
{
return -1;
}
else if(result > 0)
{
if((status&0x7f) == 0)
{
//子程序是正常退出的
printf("child process return code is %d\n",(status>>8)&0xff);
}
else
{
//子程序異常退出
printf("child process receive signal is %d, coredump flag is %d\n ",status&0x7f,(status>>7)&0x1);
}
}
}
return 0;
}
//waitpid.c
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t pid = fork();
if(-1 == pid)
{
return -1;
}
else if(0 == pid)
{
//child
printf("I am child, my pid is %d\n",getpid());
sleep(5);
exit(100);
}
else
{
//parent
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(pid,&status,WNOHANG);
}while(ret == 0);
if(ret == 0)
{
//沒有已退出的程序可以回收
return 0;
}
else if(-1 == ret)
{
//調用出錯
return -1;
}
else
{
//正常傳回,傳回收集到的子程序的pid
if((status&0x7f) == 0)
{
//子程序正常退出
printf("child process return code id %d\n",(status>>8)&0xff);
}
else
{
printf("child process recivice signal is %d,coredump flag is %d\n",(status&0x7f),(status>>7)&0x1);
}
}
}
return 0;
}
- 異常情況下擷取status的值
//wait.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t ret = fork();
if(-1 == ret)
{
return -1;
}
else if(0 == ret)
{
//在子程序中構造異常(非法通路)退出場景
int* point = NULL;
*point = 100;
}
else
{
//父程序
int status = 0;
pid_t result = wait(&status);
if(-1 == result)
{
return -1;
}
else if(result > 0)
{
if((status&0x7f) == 0)
{
//子程序是正常退出的
printf("child process return code is %d\n",(status>>8)&0xff);
}
else
{
//子程序異常退出
printf("child process receive signal is %d, coredump flag is %d\n ",status&0x7f,(status>>7)&0x1);
}
}
}
return 0;
}
此處你測試出來的coredump标志位如果是0,那是因為你沒有設定coredump檔案。
可以通過
檢視core file size是否為0,若是,則用
ulimit -a
将其設定為無限制大小。
ulimit -c unlimited
//waitpid.c
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
pid_t pid = fork();
if(-1 == pid)
{
return -1;
}
else if(0 == pid)
{
//child
printf("I am child, my pid is %d\n",getpid());
sleep(5);
//測試異常退出
int* p = NULL;
*p = 100;
}
else
{
//parent
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(pid,&status,WNOHANG);
}while(ret == 0);
if(ret == 0)
{
//沒有已退出的程序可以回收
return 0;
}
else if(-1 == ret)
{
//調用出錯
return -1;
}
else
{
//正常傳回,傳回收集到的子程序的pid
if((status&0x7f) == 0)
{
//子程序正常退出
printf("child process return code id %d\n",(status>>8)&0xff);
}
else
{
printf("child process recivice signal is %d,coredump flag is %d\n",(status&0x7f),(status>>7)&0x1);
}
}
}
return 0;
}
程序程式替換
父子程序共享代碼段,當我們要讓子程序執行不同程式的時候,就需要讓子程序調用程序替換函數,進而讓子程序執行不一樣的代碼。本質上就是替換程序的代碼段和資料段,以及更新堆棧。
-
exec函數族
函數名帶有l(list):以可變參數清單的方式傳遞參數,例如(execl、execlp、execle);
函數名帶有p(path):使用PATH環境變量搜尋程式,是以不必寫絕對路徑,如(execlp、execvp);
函數名帶有e(env):需要使用者自己維護環境變量,如(execle、execve)
函數名帶有v(vector):以字元指針數組的方式傳遞參數,例如(execv、execvp、execve);
-
execl
int execl(const char* path,const char* arg ...);
/*
參數:
path:程式的路徑名
arg :傳遞給可執行程式的指令行參數,第一個參數是可執行程式名;
如果要傳遞多個參數,則用逗号将其隔開,最後以NULL結尾。
傳回值:
調用成功:加載新的程式,不再傳回
調用失敗:傳回-1
*/
例如:execl("/usr/bin/ls","ls","-a","-l",NULL);
-
execlp
int execlp(const char* file,const char* arg ...)
/*
參數:
file:可執行程式(可以不帶路徑,也可以帶路徑)
剩餘參數與execl函數一緻
*/
例如:execlp("ls","ls","-a","-l",NULL);
為什麼execlp第一個參數不用帶路徑呢?
execlp這個函數會去搜尋PATH這個環境變量,若可執行程式在PATH中則正常替換,執行替換後的程式;若不在PATH中,則報錯傳回。
-
execle
int execle(const char* path,const char* arg,...,char* const envp[])
/*
參數:
相較于execl,增加了一個envp[],剩下的完全一緻;
envp:使用者傳遞的環境變量(使用者在調用該函數的時候,需要自己組織環境變量傳遞給函數)
*/
例如:
extern char** environ; //系統自帶的全局環境變量
int ret = execle("/home/mtgetenv","mygetenv",NULL,environ);
-
execv
int execv(const char* path,char* const argv[]);
/*
參數:
argv:以指針數組的方式傳遞給可執行程式的指令行參數;
剩下的與execl一緻
*/
例如:
char* argv[10] = {NULL};
argv[0] = "ls";
argv[1] = "-a";
argv[2] = "-l";
int ret = execv("/usr/bin/ls",argv);
-
execvp
int execvp(const char* file,char* const argv[]);
/*
參數:
file:可執行程式,可以不用帶有路徑,也可以帶
argv:以指針數組的方式傳遞給可執行程式的指令行參數,
傳回值與execl一緻
*/
-
execve
int execve(const char* path,char* const argv[],char* const envp[]);
/*
參數:
path:需要帶路徑的可執行程式
argv:傳遞給可執行程式的指令行參數,以指針數組的方式傳遞
envp:程式員自己組織的環境變量
傳回值與execl一緻
*/
例如:
extern char** environ;
char* argv[10] = {NULL};
argv[0] = "ls";
argv[1] = "-a";
argv[2] = "-l";
int ret = execve("/usr/bin/ls",argv,environ);
- 函數之間的差別
- execve是系統調用函數,其他五個函數都屬于C标準庫函數;
- 執行個體:
#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid < 0)
{
return 0;
}
else if(pid == 0)
{
printf("Before:I start replace!\n");
int ret = execl("/usr/bin/ls","ls","-a","-l",NULL);
printf("replace failed:%d",ret);
}
else
{
printf("I am father, I prepare to wait child process!\n");
wait(NULL);
}
return 0;
}
建立子程序
- fork函數
- 頭檔案:
#include <unistd.h>
- 函數原型:
pid_t fork()
- 傳回值:成功會有兩個傳回值,子程序号(>0)傳回給父程序,0傳回給子程序;失敗傳回-1。
- 特性
- 在指令行當中啟動的程序,它的父程序就是目前的bash。
- 父子程序是代碼共享、資料獨有且是互相獨立運作的,各自有各自的虛拟位址空間和頁表,互不幹擾。
- 父子程序是搶占式運作的。誰先誰後由排程器決定。
- 子程序是從fork語句之後開始運作的。
- 主要應用場景
- 守護程序:子程序執行真正的業務(程序程式替換),父程序負責守護子程序(當子程序在執行業務的時候意外“挂掉了”,父程序負責重新啟動子程序,讓子程序繼續提供服務)。
- fork之後的内部機制
- 系統配置設定新的記憶體和核心資料結構(task_struct)給子程序;
- 将父程序部分資料結構拷貝至子程序;
- 添加子程序到系統清單中,添加到雙向連結清單當中;
- fork傳回,開始排程器排程(作業系統開始排程)。
- 父子程序的執行流
- 寫時拷貝
- 通常,父子程序代碼共享,父子不再寫入時,資料也是共享的。但當任意一方試圖寫入,便會以寫時拷貝的方式各自複制一份副本。具體步驟如下:
- 子程序的PCB和頁表都是拷貝父程序的。
- 起初,系統并沒有給子程序當中的變量重新配置設定空間,它還是原來父程序實體位址當中的内容。
- 如果父子程序都沒改變某變量的值,則子程序就沒必要為該變量新配置設定一個空間,子程序可以共享父程序的資料資源。
- 但如果有任意一方改變了某變量值(例如下圖的頁表項100),那系統就需要另外配置設定一塊實體記憶體給子程序。此時父子程序通過各自的頁表,指向不同的實體位址。
-
可以擷取目前程序的父程序的程序号。getppid()
#include<stdio.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if(-1 == pid) //建立子程序失敗
{
return -1;
}
else if(0 == pid)//子程序
{
printf("This is child process,with pid:%d,ppid:%d\n",getpid(),getppid());
sleep(3);
}
else //父程序
{
printf("This is father process,with pid:%d,ppid:%d\n",getpid(),getppid());
sleep(3);
}
return 0;
}
- 父子程序的關系
- 代碼共享性:子程序複制父程序的PCB,即共享代碼空間和打開的檔案資源
- 程序獨立性:父子程序各有各的虛拟位址空間,確定在執行的時候資料不會互相幹擾。
子程序從fork函數後的下一條指令處開始執行,此時,父子程序競争使用CPU來運作自己的代碼,而在一個程序被剝離CPU的時候,程式計數器就會記錄下一條要執行的指令。是以,子程序的程式計數器起始記錄的一定是fork函數執行完畢後的第一條彙編指令(其實就是将函數傳回值移動到某個寄存器的彙編指令)
一般來說,父程序主要起管家的作用,主要是安排各個子程序什麼時候幹幹什麼,而子程序就相當于保姆,具體負責幹實事的。
環境變量
- 概念:用來指定作業系統運作的一些參數。也就是說,作業系統通過環境變量來找到運作時的一些資源。執行指令的時候,幫助使用者找到該指令在哪一個位置。
- 常見的環境變量
- PATH
- 指定可執行程式的搜尋路徑。程式員執行的指令之是以能夠被找到,就是環境變量的作用。
- 驗證:使用
查找該指令所在的路徑。which + 指令
- HOME
- 登入到Linux作業系統的家目錄
- SHELL
- 目前的指令行解釋器,預設是"/bin/bash"
檢視目前環境變量,使用env指令來檢視;
檢視某環境變量值,使用echo $[環境變量名稱]
- 環境變量的組織方式
- 環境變量名稱 = 環境變量的值(使用:進行間隔)
- 系統當中的環境變量是有多個時,每一個環境變量的組織方式都是key(環境變量名稱)= value(環境變量的值,多個值之間用:隔開)
- 通過字元指針數組的方式組織,數組最後的元素以NULL結尾(當程式拿到環境變量的時候,讀取到NULL,說明已經讀取完畢)
-
:本質上是一個數組,數組的元素是char* env[]
,每一個char *
都指向一個環境變量(key = value)char *
- 環境變量對應的檔案
- 系統級檔案
- 使用者級檔案
- 修改環境變量
-
指令範式
export 環境變量名稱 = $環境變量名稱 :新添加的環境變量内容
- 修改方式
- 指令行當中直接修改
- 檔案中修改
- 擴充
- 如何讓自己的程式。不加 ./ 直接使用程式名稱執行?兩種方式:
- 将我們的程式放在/user/bin下面(不推薦)
-
設定環境變量:在PATH環境變量當中增加可執行程式的路徑
環境變量的組織方式
- 擷取環境變量
- 通過main函數的參數擷取
- main函數參數的含義:可以在main函數内通過循環的方式列印環境變量的内容(循環條件:env[i] != NULL)
- 驗證:for循環列印的内容和指令行直接輸入env的結果一緻
#include<stdio.h>
#include <unistd.h>
int main(int argc,char* argv[],char* env[])
{
int i = 0;
//列印參數個數
printf("參數個數為%d\n",argc);
//列印參數
for(; argv[i] != NULL; i++)
{
printf("%s\n",argv[i]);
}
//列印環境變量
i = 0;
for(i = 0;env[i]!=NULL;i++)
{
printf("%s\n",env[i]);
}
return 0;
}
- 使用env指令
- 使用getenv函數:檢視特定PTAH環境變量的内容
#include<stdio.h>
#include<stdlib.h>
int main()
{
char* ret = NULL;
ret = getenv("PATH");
printf("%s\n",ret);
return 0;
}
- environ——全局環境變量
-
:這個是全局的外部變量,在lib.so當中定義,使用的時候需要extern關鍵字。extern char** environ
#include<stdio.h>
#include <unistd.h>
int main()
{
extern char** environ;
int i = 0;
for(; environ[i]!=NULL;i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
程序的分類
- 程序的正常退出步驟:
- 子程序調用exit函數退出
- 父程序調用wait函數對子程序進行回收
- 僵屍程序:執行了步驟1,但還未執行步驟2。
- 托孤程序:父程序先于子程序退出,子程序變為托孤程序,且交予linux的1号程序(init程序)回收。
僵屍程序
- 具體形成過程
- 子程序退出後,自動給父程序發送SIG_CHLD信号;
- 父程序收到子程序的SIG_CHLD信号,但該信号的處理方式為忽略;
- 子程序因未被父程序回收,導緻在核心中的PCB未得到釋放;
- 是以,子程序就變成了僵屍程序,通過ps指令可發現其狀态被系統标記為Z。
#include <stdio.h>
#include <unistd.h>
//僵屍程序:子程序先于父程序退出
int main()
{
int ret = fork();
if(ret<0)
{
return -1;
}
else if(ret == 0)
{
//子程序代碼
printf("the child process exit!\n");
}
else
{
//父程序代碼
while(1)
{
printf("I am parent process!\n");
sleep(1);
}
}
}
- 解決方案
- 過多的僵屍程序的存在,必然會大量占用系統記憶體(PCB資源不能得到釋放),是以就會造成記憶體洩漏。是以,強烈推薦由父程序進行程序等待。
- 通過指令行的
指令進行回收kill
- 普通終止:
,但可能會殺不死。kill pid
- 強行終止:
。kill -9 pid
孤兒程序
-
具體形成過程
父程序先于子程序退出後,因為父程序沒有了,是以子程序就變成孤兒了。注意:沒有孤兒狀态!!!
- 模拟代碼
#include <stdio.h>
#include <unistd.h>
//孤兒程序:父程序先于子程序退出
int main()
{
int ret = fork();
if(ret<0)
{
return -1;
}
else if(ret == 0)
{
//子程序代碼
while(1)
{
printf("I am child process!\n");
printf("pid:%d ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//父程序代碼
sleep(1);
printf("I am parent process!\n");
printf("pid:%d ppid:%d\n",getpid(),getppid());
}
}
- 解決方案
- 雖然孤兒程序的父程序已經被殺死了,但父程序在死前将它所有的子程序都托付給了1号程序,是以孤兒程序又叫托孤程序。在孤兒程序退出的時候,系統的1号程序(init程序)便會對托付給他的孤兒程序進行回收,不會像僵屍程序那樣一直占用系統記憶體(PCB資源不能得到釋放)。
- 什麼是1号程序:1号程序(核心态)由0号程序建立,負責執行核心的部分初始化工作及進 行系統配置,并建立若幹個用于高速緩存和虛拟主存管理的核心線程。随後,1号程序調用execve()運作可執行程式init,并演變成使用者态1号程序, 即init程序。它按照配置檔案/etc/initab的要求,完成系統啟動工作,建立編号為1号、2号…的若幹終端注冊程序getty。
-
孤兒程序有危害嗎?
孤兒程序沒有危害。因為孤兒程序在正常退出後,被一号程序領養,不會形成僵屍程序
程序的屬性
程序的虛拟位址空間
- 因為系統的實體空間資源是有限的,且不可能為每一個程序都配置設定4G的位址空間,是以為了提高多程序并行運作及最大限度的提高存儲資源的使用率,作業系統引入了MMU(記憶體管理系統),它負責為每一個程序配置設定虛拟的4G位址空間,并在程式運作時将程式當中的虛拟位址轉換為實體位址。
- 既然是虛拟出來的位址,那當程式在通路某虛拟位址的時候,是需要MMU将其轉化成實體位址的,且這些虛拟位址隻有在使用的時候才會由系統映射為實體位址的。
- 映射方式:
- 分段式:通過段表建立連接配接
- 實體位址 = 段号 + 段内偏移
- 段号 :指向某段的起始位址
- 段頁式
- 實體位址 = 段号 + 頁号 + 頁内偏移
- 段号:指向某頁表位址
- 頁号:指向某一頁(塊)的位址
- 頁表式:虛拟位址和實體位址事先都以頁為機關進行劃分,并通過頁表建立聯系。
- 實體位址 = 頁号 + 頁内偏移;
- 頁号 = 虛拟位址/頁大小;(通常1頁=4KB)
- 頁内偏移 = 虛拟位址%頁大小
每個程序都有自己的頁表(在程序控制塊PCB中有頁表位址),子程序最初的頁表映射的内容就是來自父程序的。但後面子程序在運作的時候,可能就會有不同的映射了。
程序優先級
- 為什麼要有優先級
- 系統程序多,CPU少,程序之間具有競争性。為了高效完成任務,更合理競争相關資源,于是便有了優先級
- 概念
- 程序擷取CPU資源配置設定的先後順序就是程序的優先級。
- 系統用優先級PRI和友好值NI來表示一個程序的優先關系。
- PRI & NI
- 在未引入NI前,PRI值越小的程序就擁有越高的優先級;
- 引入友好值NI後,程序的優先級 = PRI + NI
- NI(nice)的取值範圍是[-20, 19]
- 修改程序優先級
- 在Linux下就是通過調整程序的nice值來調整程序優先級的。
- 指令行執行
,動态檢視系統目前各程序的運作情況top
- 鍵入
鍵,再輸入程序号pid,選擇你要修改的程序。r
- 輸入[-20, 19]之間的值,為程序設定友好度NI。
程序間通信
每一個程序通過各自的程序虛拟位址空間對存儲在實體記憶體中的程序資料進行通路(通過各自的頁表的映射關系,通路到實體記憶體)。從程序的角度看,每個程序都認為自己有4G(在32位作業系統平台下)的空間,至于實體記憶體當中是如何存儲,頁表如何映射,程序是不清楚的。這也造就了程序的獨立性,確定程序間的資料不會竄。但當兩個程序之間需要交換資料時,就無法友善的交換資訊了。是以就出現了程序間通信這個課題。
通信目的:
- 資料傳輸
- 資源共享
- 事件通知
- 程序控制
通信方式:
- 早期Unix系統的ipc
- 管道
- 信号
- fifo
- system-v的ipc——貝爾實驗室
- system-v 消息隊列
- system-v 信号量
- system-v 共享記憶體套接字ipc——BSD伯克利大學
- 。。。
- p作業系統ix的ipc——IEEE
- p作業系統ix 消息隊列
- p作業系統ix 信号量
- p作業系統ix 共享記憶體
無名管道
無名管道的本質就是核心當中的一塊緩沖區,供程序進行讀寫,達到交換資料的目的。
- 頭檔案
#include <unistd.h>
- 函數原型:
int pipe(int pipefd[2])
- pipefd為輸出型參數,其中中
是管道的讀端,pipefd[0]
是管道的寫端。pipefd[1]
- 成功傳回0,失敗傳回-1
- 從核心角度窺探管道建立動作
- 程序調用pipe接口後,就會在核心當中産生一塊緩沖區。該緩沖區有讀寫兩端,相應的,也會産生兩個檔案描述符,分别與讀端和寫端相對應。
- 目前的程序控制塊PCB中有一個
結構體指針struct files_struct
,在files_struct結構體中有一個結構體指針數組files
fd_array[ ]
,
該數組中的每一個元素都是一個檔案結構體指針
,該指針指向的就是一個描述檔案資訊的結構體,而該數組的下标就是檔案的檔案描述符。struct file*
- 特點:
- 它是一個沒有名字的特殊檔案,無法用open函數打開,但可以用cl作業系統e關閉。
- 隻能通過子程序繼承父程序的檔案描述符的形式來使用;
- 管道(緩沖區)的大小為64k。
- 管道是基于位元組流服務的,管道裡的資料被讀一次就自動删除了,如果沒有資料繼續寫入,則第二次讀便會因為讀不到資料而被阻塞。
- write和read操作無名管道的輸入輸出檔案描述符時,預設是阻塞性的。當然使用者可以通過
函數手動改變它們為非阻塞性的。以設定非阻塞讀為例,其步驟如下:fcntl()
- 擷取fd[0]本身屬性:
read_ret = fcntl(fd[0], F_GETFL);
- 給fd[0]加上非阻塞屬性:
fcntl(fd[0], F_SETFL, read_ret | O_NONBLOCK);
- 資料傳輸是單向的(隻能從管道的寫端流向管道的讀端),即半雙工。
- 在使用read函數讀取管道資料的時候,可以自定義每次讀取的位元組數,且當讀取的位元組數小于4096位元組的時候,能確定本次讀取操作是原子性的。
- 所有檔案描述符被關閉(程序退出)之後,無名管道被銷毀。
- 使用步驟:
- 父程序調用pipe函數建立無名管道;
- 父程序調用fork函數建立子程序;
- 分别在父子程序中利用cl作業系統e函數關閉沒用到的端口(讀或寫)
- 調用write/read函數讀寫資料
- 利用cl作業系統e函數關閉讀寫端口
- 代碼執行個體:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <error.h>
#include <string.h>
#define MAX_DATA_LEN 256
int main()
{
pid_t pid;
int pipe_fd[2];
int status;
char buf[MAX_DATA_LEN];
const char data[] = "Pipe test program\n";
int real_read,real_write;
memset((void *)buf,0,sizeof(buf));
/*建立管道*/
if(pipe(pipe_fd) < 0){
printf("pipe create failed\n");
exit(1);
}
/*建立子程序*/
if((pid = fork()) == 0){
/*子程序關閉寫描述符*/
cl作業系統e(pipe_fd[1]);
/*子程序讀管道内容*/
if((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0){
printf("%d bytes read from the pipe is: %s\n",real_read,buf);
}
/*關閉子程序讀描述符*/
cl作業系統e(pipe_fd[0]);
exit(0);
}else if(pid > 0){
/*父程序關閉讀描述符*/
cl作業系統e(pipe_fd[0]);
if((real_write = write(pipe_fd[1], data, strlen(data)) != -1)){
printf("%d bytes write to the pipe which is: %s\n",real_write,data);
}
/*關閉父程序寫描述符*/
cl作業系統e(pipe_fd[1]);
/*回收子程序*/
wait(&status);
exit(0);
}
}
有名管道
有名管道可以支援非父子程序間的通信。
- 頭檔案
#include <unistd.h>
- 函數原型:
int mkfifo(const char* pathname, mode_t moed)
- pathname:要建立的有名管道的路徑名。
- mode:有名管道的讀寫權限,用八進制數表示(如0664)
- 成功傳回0,失敗傳回-1
- 特性
- 可通過指令行建立有名管道:
mkfifo fifo_name
- 代碼執行個體
system-V 信号量
- 作用:保護共享資源(同步/互斥),本質就是一個計數器。
- 互斥是指每個程序需要先擷取到信号量才可以通路臨界資源,如果擷取失敗,就會阻塞等待。
- 同步是指某程序在某個點試圖擷取信号量(阻塞操作),即等待另一個程序完成某項工作到達某一個同步點的時候釋放得信号量。
- 用法
- 定義一個唯一的key(ftok)
- 構造一個信号量(semget)
- 初始化信号量(semctl + SETVA)
- 對信号量進行PV操作(semop)
- 删除信号量(semctl + RMID)
- 函數介紹
-
semget()
- 功能:擷取信号量對象的ID
- 函數原型
int semget(key_t key, int nsems, int semflg);
- key:信号量鍵值
- nsems:信号量數量
- shmflg:
- IPC_CREAT:信号量不存在則建立
- mode:新建立的信号量權限
- 傳回值:成功則傳回信号量ID,失敗傳回-1
-
semclt()
- 功能:擷取/設定/删除信号量的相關屬性
- 函數原型:
int semctl(int semid, int semnum, int cmd, union semun arg);
- semid:信号量ID
- semnum:信号量編号
- cmd:
- IPC_STAT:擷取信号量的屬性資訊
- IPC_SET:設定信号量的屬性
- IPC_RMID:删除信号量
- IPC_SETVAL:設定信号量的值
-
arg:
union semun
{
int val;
struct semid_ds *buf;
}
- 傳回值:成功由cmd類型決定,失敗傳回-1
-
semop()
- 功能:信号量的PV操作函數
- 函數原型:
int semctl(int semid, struct sembuf *sops, size_t nsops);
- semid:信号量ID
-
sops:信号量操作結構體
struct sembuf
{
short sem_num; //信号量編号
short sem_op; //信号量PV操作
short sem_flg; //信号量行為(SEM_UNDO表示程序推出後由系統回收信号量)
}
- nops:信号量數量
- 傳回值:成功則傳回0,失敗傳回-1
- 函數封裝
為了在具體程式設計時更加簡易的使用信号量,一般我們會編寫一個信号量使用的API
//sem.c
#include <sys/ipc.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
union semun
{
int val;
struct semid_ds *buf;
}
/*初始化信号量*/
int init_sem(int sem_id, int init_value)
{
union semun sem_union;
sem_unino.val = init_value;
if(semctl(sem_id, 0, SETVAL, sem_union) == -1){
printf("initialize semaphor failed\n");
exit(-1);
}
return 0;
}
/*删除信号量*/
int del_sem(int sem_id)
{
union semun sem_union;
if(semctl(sem_id, 0, IPC_RMID, sem_union) == -1){
perror("delete semaphor");
exit(-1);
}
return 0;
}
/*P操作*/
int sem_p(int sem_id)
{
struct sembuf sops;
sops.sem_num = 0; //單個信号量的編号為0,即從0開始編
sops.sem_op = -1; //P操作用減1
sops.sem_flg = SEM_UNDO; //表示系統自動釋放程序退出後未回收的信号量
if(semop(sem_id, &sops, 1) == -1){
perror("P operation");
exit(-1);
}
return 0;
}
/*V操作*/
int sem_v(int sem_id)
{
struct sembuf sops;
sops.sem_num = 0;
sops.sem_op = 1; //V操作用加1
sops.sem_flg = SEM_UNDO;
if(semop(sem_id, &sops, 1) == -1){
perror("V operation");
exit(-1);
}
return 0;
}
測試代碼:
//test.c
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#define DELAY_TIME 3
int main(void)
{
pid_t pid;
int sem_id;
sem_id = semget((key_t)6666, 1, 0666|IPC_CREAT); //建立一個信号量
init_sem(sem_id);
pid = fork();
if(pid == -1){
perror("fork");
}else if(pid == 0){
printf("Child process will wait for some seconds...\n");
sleep(DELAY_TIME);
printf("the child process is running...\n");
sem_v(sem_id);
}else{
sem_p(sem_id); //等待子程序執行完
printf("the parent process is running\n");
sem_v(sem_id);
del_sem(sem_id);
}
return 0;
}
system-V 共享記憶體
- 作用:高效率程序間傳輸大量資料,不同的程序通過各自的頁表将某同一段實體記憶體空間映射到自己程序的虛拟空間中,然後不同的程序就可以通過操作自己虛拟空間的位址來達到讀寫資料(通信)的目的。
- 用法
- 定義一個唯一的key(ftok)
- 構造一個共享記憶體對象(shmget)
- 共享記憶體映射(shmat)
- 解除共享記憶體映射(shmdt)
- 删除共享記憶體(shmctl)
- 頭檔案:
#include <sys/shm.h>
- 函數介紹
-
shmget()
- 功能:擷取共享記憶體對象的ID(操作句柄)
- 函數原型
-
int shmget(key_t key, int size, int shmflg);
- key:共享對象鍵值(辨別符)
- size:共享記憶體大小
- shmflg:屬性資訊
- IPC_CREAT:若共享記憶體不存在則建立,否則傳回該共享記憶體的ID。
- IPC_CREAT | IPC_EXCL:若共享記憶體存在則報錯,若不存在就建立,意思就是說傳回的一定是建立的共享記憶體ID。
- mode:新建立的共享記憶體權限,與前面的屬性相或。
- 傳回值:成功則傳回共享記憶體ID,失敗傳回-1
-
shmat()
- 功能:将共享記憶體映射到程序的虛拟空間
- 函數原型
-
void* shmat(int shmid, const void *shmaddr, int shmflg);
- shmid:共享記憶體ID
- shmaddr:共享記憶體的映射位址,NULL為自動配置設定
- shmflg:以什麼權限将共享記憶體附加到程序當中
- SHM_RDONLY:隻讀方式映射
- 0:可讀寫方式映射
- 傳回值:成功則傳回映射到的虛拟位址,失敗傳回NULL
-
shmdt()
- 功能:解除共享記憶體映射
- 函數原型
-
int shmdt(const void *shmaddr);
- shmaddr:共享記憶體的映射位址(虛拟位址)
- 傳回值:成功則傳回0,失敗傳回-1
-
shmctl()
- 功能:擷取/設定/删除共享記憶體的相關屬性
- 函數原型
-
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- shmid:共享記憶體ID
- cmd:告訴函數它需要完成什麼任務
- IPC_STAT:擷取共享記憶體的屬性資訊
- IPC_SET:設定共享記憶體的屬性
- IPC_RMID:删除共享記憶體,此時buf參數填NULL
- buf:屬性緩沖區,是一個輸出型參數(使用者提供位址空間,函數負責填寫内容),其結構體定義為:
struct shmid_ds {
struct ipc_perm shm_perm; //共享記憶體權限
size_t shm_segsz; //緩沖區位元組數
time_t shm_atime; //最後修改時間
time_t shm_dtime;
time_t shm_ctime;
pid_t shm_cpid; //所有者的程序号
pid_t shm_lpid; //最後映射到的程序号
shmatt_t shm_nattch; //連接配接數
...
}
-
一文了解程式及其通信方法 - 傳回值:成功傳回值由cmd類型決定,失敗傳回-1
- 特性
- 覆寫寫:向共享記憶體寫資料時,會覆寫舊資料(清空共享記憶體區)
- 反複讀:從共享記憶體讀資料時,不會影響舊資料(差別于位元組流的管道)
- 可通過指令行删除現有共享記憶體:
- 執行
檢視系統目前程序間通信情況(消息隊列、共享記憶體、信号量)ipcs
- 執行
删除id為shmid的共享記憶體ipcrm -m shmid
- 删除共享記憶體的時候,若共享記憶體附加的程序數量不為0,則會将該共享記憶體的key變成0x00000000表示目前共享記憶體不能被其他程序所附加,共享記憶體的狀态會被設定為destory。附加的程序一旦全部退出之後,該共享記憶體在核心的結構體會被作業系統釋放。
- 代碼示例
- 兩個程序(以父子程序為例,但也可以是非父子程序)分别通過
函數建立/擷取共享記憶體,且這兩個程序在建立/擷取共享記憶體時,其參數設定(大小)必須一緻,否則擷取到就不是同一個共享記憶體。shmget
- 分别調用
接口将共享記憶體附加到自己的程序虛拟位址空間去。shmat
- 程序間通信。
- 通信結束後将程序和共享記憶體分離。
//test.c
#include <sys/types.h>
#include "sem.h"
#define DELAY_TIME 3
int main(void)
{
pid_t pid;
int sem_id;
int shm_id;
char *addr;
sem_id = semget((key_t)6666, 1, 0666|IPC_CREAT); //建立一個信号量
shm_id = shmget((key_t)7777, 1024, 0666|IPC_CREAT); //建立共享記憶體對象
/*初始化信号量*/
init_sem(sem_id,0);
/*調用fork()函數*/
pid = fork();
if(pid == -1){
perror("fork failed\n");
}else if(pid == 0){
printf("Child process will wait for some seconds...\n");
sleep(DELAY_TIME);
/*映射可讀寫的共享記憶體*/
addr = shmat(shm_id, NULL, 0);
if(addr == (void *)-1){
printf("shmat in child error\n");
exit(-1);
}
/*向共享記憶體中寫入内容*/
memcpy(addr, "hello world!",strlen("hello world!")+1);
printf("the child process is running...\n");
sem_v(sem_id);
}else{
sem_p(sem_id); //等待子程序執行完
printf("the parent process is running\n");
/*映射共享記憶體位址*/
addr = shmat(shm_id, NULL, 0);
if(addr == (void *)-1){
printf("shmat in parent error\n");
exit(-1);
}
printf("shared memory string:%s\n",addr);
/*解除共享記憶體映射*/
shmdt(addr);
/*删除共享記憶體映射*/
shmctl(shm_id, IPC_RMID, NULL);
}
return 0;
}
消息隊列
- 原理:由核心維護消息的連結清單,系統中存在多個
,并用消息隊列ID(qid)唯一區分。在進行程序間通信的時候,一個程序将消息追加到MQ的尾端,另一個程序從消息隊列裡取走資料,但不一定嚴格按照先進先出的原則取資料,也可以按照消息類型字段來取。msgqueue
- 接口函數
-
int msgget(key_t key, int msgflg);
- key:消息隊列的辨別符
- msgflg:建立标志
- IPC_CREAT:若不存在則建立;
- mode:按位或上權限(八進制數,如0664)
- 傳回值:成功則傳回消息隊列的qid,失敗傳回-1,并設定errno
-
int masgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);
- msgid:消息隊列的ID(qid)
- msgp:指向待發送消息結構體(struct msgbuf)的指針;
struct msgbuf{
long mtype; //消息類型,必須大于0
char mtext[1]; //消息資料,但程式員可根據實際需要修改此數組大小
};
- msgsz:要發送消息的長度(就是struct msgbuf結構體中char mtext[]數組的大小)
- msgflg:建立标記
- 0:阻塞發送
- IPC_NOWAIT:非阻塞發送
- 傳回值:成功傳回0,失敗傳回-1,并設定errno
-
int msgrcv(int msgid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- msgid:消息隊列的ID(qid)
- msgp:指向待接收消息結構體(struct msgbuf)的指針;
- msgsz:要接收消息的長度(就是由struct msgbuf結構體中char mtext[]數組的長度)
- msgtyp:
- 等于0:讀取隊列中第一條消息
- 大于0:讀取隊列中類型為msgtyp的第一條消息,但如果指定了MSG_EXCEPT,則讀取類型為非msgtyp的第一條消息。
- 小于0:讀取隊列中最小類型小于或等于msgtyp絕對值的第一條消息
- msgflg:建立标記
- 0:阻塞發送
- IPC_NOWAIT:非阻塞接收
- 傳回值:成功傳回實際讀取消息的位元組數,失敗傳回-1,并設定errno
-
int msgctl(int msgid, int cmd, struct msgid_ds *buf);
- msgid:消息隊列的ID(qid)
- cmd:控制指令
- IPC_RMID:删除消息隊列
- IPC_STAT:擷取消息隊列的目前狀态。
- buf:存儲消息隊列的相關資訊
- 0:阻塞發送
- 傳回值:成功傳回實際讀取消息的位元組數,失敗傳回-1,并設定errno
- 用法
- 用
函數建立/擷取一個消息隊列;msgget
- 定義一個消息緩沖區
,用來發送或接收資訊msgbuf
- 對消息隊列進行發送(
)或接收(msgsnd
)msgrcv
- 删除消息隊列(什麼周期跟随系統)。
- 代碼執行個體:一個程序發送,一個程序接收。注意:不同程序間使用消息隊列進行通信的時候,需要擷取相同的消息隊列辨別符。
//msg_send.c
#include <string.h>
#include <stdio.h>
#include <sys/msg.h>
#include <fcntl.h>
struct msgbuf
{
long mtype;
char mtext[255];
};
int main(void)
{
int ret;
int qid = msgget(0x12341234, IPC_CREAT|0664); //kyy為12341234,由使用者自己定義
if(qid < 0){
perror("msgget:");
exit(1);
}
printf("queue ID is %d\n", qid);
/*建立并初始化消息的buffer*/
struct msgbuf msgbuffer;
msgbuffer.mtype = 2; //指定消息類型為2
const char * str_send = "I will send to ...";
strcpy(msgbuffer.mtext, str_send);
//發送消息
ret = msgsnd(qid, &msgbuffer, sizeof(msgbuffer.mtext), 0);
if(ret < 0){
perror("msgsnd:");
exit(1);
}
return 0;
}
//msg_receive.c
#include <string.h>
#include <stdio.h>
#include <sys/msg.h>
#include <fcntl.h>
struct msgbuf
{
long mtype;
char mtext[255];
};
int main(void)
{
int ret;
int qid = msgget(0x12341234, IPC_CREAT|0664);
if(qid < 0){
perror("msgget:");
exit(1);
}
printf("queue ID is %d\n", qid);
/*建立接收消息的buffer*/
struct msgbuf msgbuffer;
/*接收消息*/
ret = sgrcv(qid, &msgbuffer, sizeof(msgbuffer.mtext), 0, 0);
if(ret < 0){
perror("msgrcv:");
exit(1);
}
/*列印*/
printf("Received strings:%s\n",msgbuffer.mtext);
return 0;
}