文章目錄
- Linux下行程相關操作
- 1. 行程概念
- 1.1 行程基本概念
- 1.2 Linux下PCB的定義
- 2.行程查看
- 3. 行程創建
- 3.1 fork函式
- 3.2 vfork函式
- 3.3 fork與vfork的異同
- 3.4 寫時拷貝技術
- 4. 行程退出
- 4.1 exit函式
- 4.2 _exit函式
- 4.3 exit與_exit的異同
- 5. 行程等待
- 5.1 wait函式
- 5.2 waitpid函式
- 6. 程式替換
- 6.1 execl函式和execv函式
- 6.2 execlp函式和execvp函式
- 6.3 execle函式和execve函式
Linux下行程相關操作
1. 行程概念
1.1 行程基本概念
行程定義:一個具有一定獨立功能的程式在一個資料集合上的一次動態執行程序,
簡言之:行程是程式的一次執行程序
行程特性:
- 動態性:行程是動態產生的,往往都會經歷創建、運行、消亡三個狀態
- 獨立性:各個行程之間的地址空間相互獨立
- 并發性:任何行程都可以一起向前推進
- 異步性:每個行程都以其不可預知的速度向前推進
- 結構化:行程 = 代碼段 + 資料段 + PCB(行程控制塊)
行程與程式的區別:
- 行程是動態的執行程序;程式是靜態的代碼
- 行程是暫時的,運行在記憶體中的一個狀態變化的程序;程式是永久的,保存在外存中
- 通過多次執行一個程式,進而產生多個行程,所以一個程式可以對應多個行程;
- 行程通過呼叫多個程式,進而一個行程可以執行多個程式,所以一個行程可以對應多個程式
行程是競爭計算機資源的基本單位
1.2 Linux下PCB的定義
-
行程控制塊(PCB)是用來記錄行程相關資訊和管理行程而設定的一種資料結構
-
行程控制塊(PCB)是由作業系統(OS)維護的
-
系統通過PCB感知行程的存在
-
PCB隨行程的創建而創建并填寫,隨著行程的消亡而釋放
PCB的組成:
- 行程識別符號:用于唯一標識該行程的整數
- 行程名:通常是可執行檔案名
- 優先級:行程優先運行的權重
- 暫存器值:用于保存當前行程運行到某一時刻各種資料資訊
- …
在Linux作業系統中,每一個行程都有一個PCB,每一個PCB都對應一個task_struct結構體,簡言之,每創建一個行程就相當于創建一個task_struct結構體并填寫其中的資料,
Linux中的PCB定義在sched.h檔案中
若想獲得sched.h檔案,需要到Linux內核官網下載內核原始碼
以Linux Kernel Source Code 2.6.32 為例展示task_struct部分原始碼

2.行程查看
- 使用ps命令可以查看正在運行的行程瞬時的資訊,而不是動態連續的資訊
引數:
- -a : 顯示所有用戶的行程
- -u : 以用戶為主的行程狀態
- -x : 列出較完整的資訊
列印顯示出來的資訊:
PID:行程識別符號
TTY:命令所運行的位置
STAT:行程狀態
TIME:運行該命令所占用的CPU處理時間
COMMAND:該行程所運行的命令
1.顯示當前所有行程及詳細資訊
[gongruiyang@localhost ~]$ ps -ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:01 /usr/lib/systemd/systemd --switched-root --system --deserialize 21
2 ? S 0:00 [kthreadd]
3 ? S 0:00 [ksoftirqd/0]
5 ? S< 0:00 [kworker/0:0H]
7 ? S 0:00 [migration/0]
...................................................
2.顯示某用戶的行程及詳細資訊
[gongruiyang@localhost ~]$ ps -u gongruiyang
PID TTY TIME CMD
1736 ? 00:00:00 gnome-keyring-d
1741 ? 00:00:00 gnome-session-b
1748 ? 00:00:00 dbus-launch
1749 ? 00:00:00 dbus-daemon
1807 ? 00:00:00 gvfsd
1812 ? 00:00:00 gvfsd-fuse
1904 ? 00:00:00 ssh-agent
1923 ? 00:00:00 at-spi-bus-laun
1928 ? 00:00:00 dbus-daemon
...................................................
3.顯示行程及其CPU和記憶體占用情況
[gongruiyang@localhost ~]$ ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 128212 6836 ? Ss 11:00 0:01 /usr/li
root 2 0.0 0.0 0 0 ? S 11:00 0:00 [kthrea
root 3 0.0 0.0 0 0 ? S 11:00 0:00 [ksofti
root 5 0.0 0.0 0 0 ? S< 11:00 0:00 [kworke
root 7 0.0 0.0 0 0 ? S 11:00 0:00 [migrat
root 8 0.0 0.0 0 0 ? S 11:00 0:00 [rcu_bh
3. 行程創建
3.1 fork函式
pid_t fork(void)
fork呼叫一次,會在父行程中回傳一個值,會在子行程中回傳一個值
父行程中:回傳子行程PID
子行程中:回傳 0
Q:為什么fork會回傳兩次呢?
A:子行程復制父行程的堆和堆疊中的內容,此時,兩個行程都處于fork函式中,都在等待fork函式執行結束并回傳一個pid_t,所以會有兩個回傳值
pid_t實際上就是int,被定義在sys/typpes.h中
代碼演示:
getpid()函式是獲得當前行程的行程號
getppid()函式是獲得當前行程父行程的行程號
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main()
{
printf(" [fpid] [pid] [ppid]\n");
pid_t fpid = fork();
if(fpid < 0) //創建行程失敗
perror("fork");
else if(fpid == 0)//子行程執行這一段代碼
printf("child: %4d %4d %4d\n",fpid,getpid(),getppid());
else //父行程執行這一段代碼
printf("father: %4d %4d %4d\n",fpid,getpid(),getppid());
while(1) //父子行程都執行
{
sleep(1);
}
return 0;
}
運行結果:
[fpid] [pid] [ppid]
father: 5081 5080 2717
child: 0 5081 5080
解釋:pid為5080的父行程 創建了一個 pid為5081的子行程,其中 父行程的 父行程號為2717,子行程的 父行程號為5080
fork特性:
子行程將父行程中打開的所有檔案描述符都復制了一遍,父子行程中相同編號的檔案描述符在內核中指向同一個file結構體,也就是說,file結構體的參考數量增加了
子行程不會繼承父行程的一些資料:
- 子行程不復制父行程設定的鎖(若繼承會導致排它鎖矛盾)
- 子行程不復制父行程的pid,而是產生自己的pid
- 子行程不復制父行程中的pending alarms和pending signals,而是將自己的pending alarms清除,將pending signals置為空
fork產生失敗可能的兩個原因:
- 當前運行中的行程數已經達到了系統規定的上限,此時錯誤碼(errno)的值為EAGAIN
- 當前系統記憶體容量不足以開辟一個新的行程,此時錯誤碼(errno)的值為ENOMEM
3.2 vfork函式
pid_t vfork(void)
-
該函式回傳值特點與fork相同
-
vfork創建出來的子行程并不會直接將父行程的虛擬空間內容拷貝一份,而是與父行程共享一份虛擬空間,當子行程需要修改資料時,才會進行拷貝,這稱之為寫時拷貝技術
-
父行程使用vfork創建子行程后,父行程會被掛起,直到子行程終止或被替換后,才能繼續推進
-
應當使用exit或_exit來終止vfork創建的子執行緒,不能使用return來終止,若使用return來終止子執行緒會導父行程回到呼叫vfork處,進而無限創建子行程進而產生段錯誤
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t fpid = vfork();
if(fpid < 0)
perror("vfork");
else if(fpid == 0)
{
printf("child:%d\n",getpid());
while(1)
{
printf("while\n");
sleep(1);
}
}
else
printf("father:%d\n",getpid());
printf("========================\n");
return 0;
}
輸出結果:
child:5898
while
while
while
while
while
^C
解釋:由于子行程處于回圈sleep中,導致父行程一直被掛起無法執行下面的代碼
3.3 fork與vfork的異同
異:
- fork創建的子行程是父行程的一個副本,子行程將父行程的堆疊中存的資料資訊拷貝一份到另外開辟的記憶體中去,并不能共享這些資料;vfork創建的子行程并不會立刻開辟新記憶體拷貝資料,而是共享父行程的堆疊中的資料,直到子行程終止或者被替換之前,都是在父行程的空間中運行
- vfork保證子行程先運行;fork是讓兩個行程異步運行
同:
- fork和vfork都是呼叫一次,但是回傳兩次
3.4 寫時拷貝技術
傳統拷貝方法:
- 為子行程的頁表分配頁幀
- 為子行程的頁分配頁幀
- 初始化子行程的頁表
- 把父行程的頁內容復制到子行程對應頁中
- 傳統的拷貝父行程資源實作過于簡單且效率低下
- 寫時拷貝是一種可以推遲甚至免除拷貝資料的技術
寫時拷貝(Copy - On - Write)技術:
子行程并不拷貝父行程的資料資源,而是父子行程共享父行程原有的資料資源,只有當要寫入的時候,才進行資源的復制
實際上,COW不但在Linux行程上有實際應用,而且在C++的String類在g++環境下也支持COW技術
#include <iostream>
#include <stdio.h>
#include <string>
using namespace std;
int main()
{
string str1 = "Hello\n";
string str2 = str1;
printf("Befor Write: str1[%x] str2[%x]\n", str1.c_str(), str2.c_str());
str2.clear();
str2 = "H\n";
printf("After Write: str1[%x] str2[%x]\n", str1.c_str(), str2.c_str());
return 0;
}
輸出結果:
[gongruiyang@localhost TestCOW]$ g++ COWtest.cpp -o test
[gongruiyang@localhost TestCOW]$ ./test
Befor Write: str1[18cec38] str2[18cec38]
After Write: str1[18cec38] str2[18cec98]
解釋:從輸出結果來看,str1和str2一開始指向同一記憶體地址,共享Hello這個資料,當str2要進行寫入的時候,str2指向的地址發生改變,重新開辟空間存放資料,即只有寫入東西的時候才進行記憶體的再分配
4. 行程退出
4.1 exit函式
void exit(int status)
- 功能:正常終止行程
- 頭檔案:stdlib.h
- status:回傳給父行程的狀態值,通常用0或EXIT_SUCCESS表示成功,通常用非0或EXIT_FAILURE表示例外程式終止
代碼測驗:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t fpid = fork();
int status;
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
printf("ChildP: childPID[%d]\n",getpid());
exit(EXIT_SUCCESS); //正常退出子行程
}
else
{
int ret_pid = wait(&status); //父行程阻塞等待子行程退出
printf("FatherP: %d is normally exited with status:%d\n",ret_pid,status);
}
return 0;
}
輸出結果:
ChildP: childPID[8071]
FatherP: 8071 is normally exited with status:0
4.2 _exit函式
void _exit(int status);
- 功能:立即終止子行程,并關閉所有屬于該行程的檔案描述符,該行程的所有子行程過繼給init行程,并向父行程發送SIGCHLD信號,將status作為子行程退出狀態回傳給父行程
- 頭檔案:unistd.h
- status:回傳給父行程的狀態值,通常用0或EXIT_SUCCESS表示成功,通常用非0或EXIT_FAILURE表示例外程式終止
代碼測驗:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t fpid = fork();
int status;
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
printf("ChildP: childPID[%d]\n",getpid());
_exit(EXIT_SUCCESS);
}
else
{
int ret_pid = wait(&status);
printf("FatherP: %d is normally exited with status:%d\n",ret_pid,status);
}
return 0;
}
輸出結果:
ChildP: childPID[8586]
FatherP: 8586 is normally exited with status:0
4.3 exit與_exit的異同
異:
- exit定義在stdlib.h中;_exit定義在unistd.h中
- exit會進行緩沖區重繪;_exit不會重繪緩沖區可能導致資料丟失
同:
- 退出子行程并向父行程回傳退出狀態資訊
- exit是_exit的封裝形式
代碼測驗:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("Hello!\n");
printf("World!");
exit(0);
return 0;
}
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("Hello!\n");
printf("World!");
_exit(0);
return 0;
}
exit輸出結果:
Hello!
World!
_exit輸出結果:
Hello!
程式解釋:由測驗代碼可以看出_exit由于不重繪緩沖區導致了資料丟失,而exit將__exit進行封裝了,便得到更加安全,保全了資料
5. 行程等待
等待子行程退出->獲取子行程回傳值->釋放子行程資源->防止僵尸行程產生->防止資源泄漏
5.1 wait函式
pid_t wait (int* status)
- 包含頭檔案:sys/types.h和sys/wait.h
- status:出參,用于保存子行程退出時的狀態,
- 正常退出情況下,一個int中的低16位中的高8位保存回傳值(取出回傳值:status & 0x7f )
- 例外退出情況下,一個Int中的低7位保存例外退出信號值(取出例外信號值:(status >> 8) & 0xff)
- 回傳值:退出行程的pid
功能:阻塞父行程一直等待子行程的退出,當子行程退出后,父行程才脫離阻塞繼續推進
WIFEXITED(int status)
該宏是用于檢測行程是否正在退出:
宏值若為0代表非正常退出,若為非0則代表正常退出
代碼演示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t fpid = fork();
int status = 0;
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
printf("Child:[%d]\n",getpid());
exit(3);
}
else
{
printf("Father:[%d]\n",getpid());
pid_t ret_pid = wait(&status);
if(WIFEXITED(status))
printf("%d has exited with return code %d\n",ret_pid,WEXITSTATUS(status));
else
perror("wait");
}
return 0;
}
輸出結果:
Father:[6985]
Child:[6986]
6986 has exited with return code 3
5.2 waitpid函式
pid_t waitpid(pid_t pid, int* status, int options)
引數:
- status:出參,用于保存子行程的退出狀態
- pid
| > 0 | 只等待行程ID為指定pid的子行程 |
|---|---|
| = -1 | 等待任何一個子行程,與wait功能一樣 |
| = 0 | 等待任一個【子行程組ID=父行程組ID】的子行程 |
| < -1 | 等待任一個【子行程組ID= |父行程組ID|】的子行程 (||意為絕對值) |
- options
| WNOHANG | 指定子行程結束才阻塞父行程,回傳值為0 |
|---|---|
| 0 | 與wait相同,阻塞父行程,一直等待子行程退出 |
| WUNTRACED | 子行程若處于暫停狀態:立刻回傳 子行程若處于結束狀態:不予理會 |
- 回傳值
| > 0 | 已經結束的子行程的pid |
|---|---|
| 0 | 使用options為WUNTRACED且無子行程退出 |
| -1 | 呼叫出錯(例如:無子行程) |
功能:非阻塞父行程版本wait
wait中其實呼叫了waitpid
代碼測驗:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t fpid = fork();
pid_t ret_waitpid = 0;
int status;
if(fpid < 0)
perror("fork");
else if(fpid == 0) //子行程
{
printf("Child:%d\n",getpid());
sleep(3);
exit(0); //子行程退出
}
else //父行程
{
while(!ret_waitpid) //回圈waitpid等待子行程的退出
{
ret_waitpid = waitpid(fpid,&status,WNOHANG);
if(ret_waitpid == 0)
{
printf("%d has not exit\n",fpid); //子行程未退出就列印
sleep(1);
}
}//while結束:說明此事ret_waitpid接收到了子行程exit后回傳的pid
if(ret_waitpid == fpid)
printf("%d has exited with return code:%d\n",fpid,status);
}
return 0;
}
輸出結果:
9311 has not exit
Child:9311
9311 has not exit
9311 has not exit
9311 has exited with return code:0
6. 程式替換
創建子行程的目的是為了完成其他的事情,完成其他任務,這個時候就用到了程式替換
輔助代碼:子行程需要做的任務,可執行檔案名為child_task,絕對路徑為:/home/gongruiyang/ClassLinunx/pidTest/child_task
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void child_task(int argc, char* argv[])
{
for(int i = 0; i < argc; i++)
printf("argv[%d] = %s\n",i,argv[i]);
}
//main函式有倆引數:argc表示程式的運行引數個數,argv用于保存程式引數
int main(int argc,char* argv[])
{
child_task(argc,argv );
return 0;
}
6.1 execl函式和execv函式
int execl(const char *path, const char *arg, ...);
- path:用于保存想要執行的可執行檔案絕對路徑
- arg:執行可執行檔案所需要的引數,如果不需要引數可以填NULL
- …:不定引數,說明后面的引數可以是1個,也可以是多個
代碼測驗:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
execl("/home/gongruiyang/ClassLinunx/pidTest/child_task","-a","-b");
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
輸出結果:
[gongruiyang@localhost pidTest]$ ./exectest
Father Do!
argv[0] = -a
argv[1] = -b
int execv(const char *path, char *const argv[]);
- path:用于保存想要執行的可執行檔案絕對路徑
- argv:引數串列
代碼測驗:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
char* child_argv[32];
child_argv[0] = "-a";
child_argv[1] = "-b";
child_argv[2] = "-c";
child_argv[3] = NULL; //必須要有
execv("/home/gongruiyang/ClassLinunx/pidTest/child_task",child_argv)
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
輸出結果:
Father Do!
argv[0] = -a
argv[1] = -b
argv[2] = -c
6.2 execlp函式和execvp函式
加了p之后,其中path可以不填絕對路徑,該函式會去PATH環境變數中尋找
int execlp(const char *path, const char *arg, ...);
- path:用于保存想要執行的可執行檔案路徑
- arg:執行可執行檔案所需要的引數,如果不需要引數可以填NULL
代碼測驗:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
execlp("ls","ls","-l",NULL); //在環境變數中尋找ls并執行
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
輸出結果:
Father Do!
總用量 144
-rwxrwxr-x. 1 gongruiyang gongruiyang 8561 12月 30 17:33 child_task
-rw-rw-r--. 1 gongruiyang gongruiyang 348 12月 30 17:34 child_task.c
............................................................
int execvp(const char *path, char *const argv[]);
- path:用于保存想要執行的可執行檔案路徑
- argv:引數串列
測驗代碼:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
char* child_argv[10];
child_argv[0] = "ls";
child_argv[1] = "-l";
child_argv[2] = NULL;
execvp("ls",child_argv);
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
結果輸出:
Father Do!
總用量 144
-rwxrwxr-x. 1 gongruiyang gongruiyang 8561 12月 30 17:33 child_task
-rw-rw-r--. 1 gongruiyang gongruiyang 348 12月 30 17:34 child_task.c
6.3 execle函式和execve函式
輔助程式:列印環境變數
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char* argv[],char* env[])
{
printf("env-var:");
for(int i = 0 ; env[i] != NULL; i++)
printf("env[%d]:%s\n",i,env[i]);
return 0;
}
int execle(const char *path, const char *arg,..., char * const envp[]);
- path:用于保存想要執行的可執行檔案絕對路徑
- arg:執行可執行檔案所需要的引數,如果不需要引數可以填NULL
- …:不定引數,說明后面的引數可以是1個,也可以是多個
- envp:自定義環境變數,可以填NULL
int execvpe(const char *path, char *const argv[], char *const envp[]);
- path:用于保存想要執行的可執行檔案路徑
- argv:引數串列
- envp:自定義環境變數,可以填NULL
加了e之后,相比于之前,增加了一個環境變數引數,可以使用自定義環境變數
測驗代碼:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
char* child_env[10];
child_env[0] = "MYVAL=1000";
child_env[1] = "TMP=12";
child_env[2] = NULL;
execvpe("/home/gongruiyang/ClassLinunx/pidTest/envtest",NULL,child_env);
//execvle("/home/gongruiyang/ClassLinunx/pidTest/envtest",NULL,child_env);
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
輸出結果:
Father Do!
env-var:env[0]:MYVAL=1000
env[1]:TMP=12
用于保存想要執行的可執行檔案絕對路徑
- arg:執行可執行檔案所需要的引數,如果不需要引數可以填NULL
- …:不定引數,說明后面的引數可以是1個,也可以是多個
- envp:自定義環境變數,可以填NULL
int execvpe(const char *path, char *const argv[], char *const envp[]);
- path:用于保存想要執行的可執行檔案路徑
- argv:引數串列
- envp:自定義環境變數,可以填NULL
加了e之后,相比于之前,增加了一個環境變數引數,可以使用自定義環境變數
測驗代碼:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t fpid = fork();
if(fpid < 0)
perror("fork");
else if(fpid == 0)
{
char* child_env[10];
child_env[0] = "MYVAL=1000";
child_env[1] = "TMP=12";
child_env[2] = NULL;
execvpe("/home/gongruiyang/ClassLinunx/pidTest/envtest",NULL,child_env);
//execvle("/home/gongruiyang/ClassLinunx/pidTest/envtest",NULL,child_env);
}
else
{
printf("Father Do!\n");
sleep(1);
}
sleep(1);
return 0;
}
輸出結果:
Father Do!
env-var:env[0]:MYVAL=1000
env[1]:TMP=12
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/242821.html
標籤:其他
