【Linux篇】進程控制

📌 個人主頁: 孫同學_
🔧 文章專欄:Liunx
💡 關注我,分享經驗,助你少走彎路!
在這里插入圖片描述

1. 進程創建

1.1 fork函數

linuxfork函數是非常重要的函數,它從已存在進程中創建一個新進程。新進程為子進程,而原進程為父進程。

#include <unistd.h>
pid_t fork(void);
返回值:?進程中返回0,?進程返回?進程id,出錯返回-1

進程調用fork,當控制轉移到內核中的fork代碼后,內核做:

  • 分配新的內存塊和內核數據結構給子進程
  • 將父進程部分數據結構內容拷貝至子進程
  • 添加子進程到系統進程列表當中
  • fork返回,開始調度器調度
    在這里插入圖片描述
    當一個進程調用fork之后,就有兩個二進制代碼相同的進程。而且它們都運行到相同的地方。但每個進程都將可以開始它們自己的旅程,看如下程序。
int main( void )
{pid_t pid;printf("Before: pid is %d\n", getpid());if ( (pid=fork()) == -1 )perror("fork()"),exit(1);printf("After:pid is %d, fork return %d\n", getpid(), pid);sleep(1);return 0;
} 
運?結果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0

這里看到了三行輸出,一行before,兩行after。進程43676先打印before消息,然后它有打印after。另一個after消息有43677打印的。注意到進程43677沒有打印before,為什么呢?如下圖所示
在這里插入圖片描述
所以,fork之前父進程獨立執行,fork之后,父子兩個執行流分別執行。
注意fork之后,誰先執行完全由調度器決定。

1.2 寫時拷貝

通常,父子代碼共享,父子再不寫入時,數據也是共享的,當任意一方試圖寫入,便以寫時拷備的方式各自一份副本。具體見下圖:
在這里插入圖片描述
上圖顯示父進程代碼段在自己的頁表中是只讀的,包括我們以前定義的字符常量區,代碼是不可寫的,可是數據段為什么也是只讀的?起始在我們的父進程還沒創建子進程前,代碼段是只讀的沒問題,但是數據段對應的映射關系,可能有一百個一千個映射地址,這些映射地址的權限實際上是讀寫的,但一旦創建了子進程,操作系統就會把數據段的權限也改成只讀的。 然后后面的父子進程,比如說子進程嘗試對它的數據進行寫入,當它寫入時,操作系統就會發現你要訪問的數據,第一,數據是合法的,因為虛擬地址物理地址都有,而且它發現訪問的區域是數據段,如果是代碼段肯定在start_code,end_code這個區間里面,如果是數據段肯定在start_data,start_end這個區間里面,發現你是數據段,而且頁表的映射關系是正確的,但是發現數據段怎么是只讀的,所以這時候操作系統就會出錯,這種出錯不是真的錯了,是操作系統檢測到一個用戶對一個只讀的區域進行寫入,但操作系統經過檢查發現它是數據段,而且是子進程,這時候操作系統就會觸發寫時拷貝。寫時拷貝是通過設置頁表的權限,讓頁表讓操作系統出錯的行為。讓操作系統知道我們正在訪問一個只讀的區域,進而在錯誤的驅使之下讓操作系統完成對應的寫時拷貝這樣的任務。

因為有寫時拷貝技術的存在,所以父子進程得以徹底分離離!完成了進程獨立性的技術保證!
寫時拷貝,是一種延時申請技術,可以提高整機內存的使用率

為什么要寫時拷貝?

  1. 減少子進程的創建時間
  2. 減少內存浪費

1.3 fork調用失敗的原因

  • 系統中有太多的進程
  • 實際用戶的進程數超過了限制

2. 進程終止

進程終止的本質是釋放系統資源,就是釋放進程申請的相關內核數據結構和對應的數據和代碼。

2.1 進程退出場景

  1. 代碼運行完畢,結果正確
  2. 代碼運行完畢,結果不正確
  3. 代碼異常終止(退出碼無意義)

2.2 進程常見退出方式

正常終止(可以通過 echo $? 查看最近進程的退出碼):

  1. main返回(main函數結束表示進程結束,其他函數只表示自己函數調用完成)
  2. 調用exit(status)(任何地方調用exit表示進程結束,并返回給父進程bash子進程的退出碼)
  3. _exit:終止一個調用進程(相當于誰調用它,它把誰“弄死”)
2.2.1 exit函數
#include <unistd.h>
void exit(int status);

exit最后也會調用_exit,但在調用_exit之前,還做了其他工作:

  1. 執行用戶通過atexiton_exit定義的清理函數。
  2. 關閉所有打開的流,所有的緩存數據均被寫入
  3. 調用_exit
    在這里插入圖片描述
2.2.2 _exit函數
#include <unistd.h>
void _exit(int status);
參數:status 定義了進程的終?狀態,?進程通過wait來獲取該值

說明:雖然statusint,但是僅有低8位可以被父進程所用。所以_exit(-1)時,在終端執行$?發現返回值是255。

2.2.3 exit和_exit的區別:

exit是c語言提供的,_exit是系統提供的
進程如果exit退出的時候,exit()會進行緩沖區的刷新
進程如果exit退出的時候,_exit不會進行緩沖區的刷新

庫函數和系統調用是上下層的關系,庫函數沒有進程終止能力,只能調用系統調用,操作系統給它提供的進程終止的接口它才能終止進程,所以exit的底層封裝了_exit,所以我們之前談論的緩沖區一定不在操作系統的內部,而是庫緩沖區(c語言提供的緩沖區)

異常退出

  • ctrl + c,信號終止
2.2.4 退出碼

退出碼在Linux中通常用來表示命令執行后的結果,0表示成功,非0表示不同的錯誤類型
Linux Shell 中的主要退出碼:

退出碼說明
0成功(命令正常執行)
1一般性錯誤(如參數錯誤、文件未找到、權限不足等)
2Shell 內置命令誤用(如語法錯誤、未找到命令等)
126權限問題(命令不可執行,如缺少執行權限)
127命令未找到(Shell 找不到指定命令)
130進程被 Ctrl+C 終止(SIGINT 信號)
141進程被 SIGHUP 信號終止(如終端關閉)

3. 進程等待

3.1 進程等待必要性

  • 之前講過,子進程退出,父進程如果不管不顧,就可能造成僵尸進程的問題,進而造成內存泄漏。
  • 進程一旦變成僵尸狀態,那就刀槍不入,“殺人不眨眼”的kill-9也無能為力,因為誰也沒有辦法殺死一個已經死去的進程。
  • 最后,父進程派給子進程的任務完成的如何,我們需要知道。如,子進程運行完成,結果對還是不對,或者是否正常退出。
  • 父進程通過進程等待的方式,回收子進程資源,獲取子進程退出信息

3.2 進程等待的方法

3.2.1 wait方法

如果等待子進程,子進程沒有退出,父進程就會阻塞在wait調用處(相當于scanf)
在這里插入圖片描述

在這里插入圖片描述

#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:成功返回被等待進程pid,失敗返回-1。
參數:輸出型參數,獲取子進程退出狀態,不關?則可以設置成為NULL
3.2.2 waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:當正常返回的時候waitpid返回收集到的子進程的進程ID;如果設置了選項WNOHANG,?調?中waitpid發現沒有已退出的子進程可收集,則返回0;如果調?中出錯,則返回-1,這時errno會被設置成相應的值以指?錯誤所在;
參數:pid:Pid = -1,等待任?個子進程。與wait等效。Pid > 0.等待其進程ID與pid相等的?進程。status: 輸出型參數WIFEXITED(status): 若為正常終?子進程返回的狀態,則為真。(查看進程是否是正常退出)WEXITSTATUS(status)((status>>8)&0xFF): 若WIFEXITED?零,提取子進程退出碼。(查看進程的退出碼)options:默認為0,表?阻塞等待WNOHANG: 若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等
待。若正常結束,則返回該子進程的ID。
  • 如果子進程已經退出,調用wait/waitpid時,wait/waitpid會立即返回,并且釋放資源,獲得子進程退出信息。
  • 如果在任意時刻調用wait/waitpid,子進程存在且正常運行,則進程可能阻塞。
  • 如果不存在該子進程,則立即出錯返回。
    在這里插入圖片描述
3.2.3 獲取子進程status
  • waitwaitpid,都有一個status參數,該參數是一個輸出型參數,由操作系統填充。
  • 如果傳遞NULL,表示不關心子進程的退出狀態信息。
  • 否則,操作系統會根據該參數,將子進程的退出信息反饋給父進程。
  • status不能簡單的當作整形來看待,可以當作位圖來看待,具體細節如下圖(只研究status低16比特位):
    在這里插入圖片描述
3.2.4 阻塞與非阻塞等待
  • 進程的阻塞等待方式:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(void)
{pid_t pid;if ((pid = fork()) == -1)perror("fork"), exit(1);if (pid == 0) {sleep(20);exit(10);}else {int st;int ret = wait(&st);if (ret > 0 && (st & 0X7F) == 0) { // 正常退出 printf("child exit code:%d\n", (st >> 8) & 0XFF);}else if (ret > 0) { // 異常退出 printf("sig code : %d\n", st & 0X7F);}}
}
測試結果:
# ./a.out #等20秒退出
child exit code : 10
# ./a.out #在其他終端kill掉
sig code : 9
  • 進程的非阻塞等待方式:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
typedef void (*handler_t)(); // 函數指針類型 
std::vector<handler_t> handlers; // 函數指針數組 
void fun_one() {printf("這是?個臨時任務1\n");
}
void fun_two() {printf("這是?個臨時任務2\n");
}
void Load() {handlers.push_back(fun_one);handlers.push_back(fun_two);
}
void handler() {if (handlers.empty())Load();for (auto iter : handlers)iter();
}
int main() {pid_t pid;pid = fork();if (pid < 0) {printf("%s fork error\n", __FUNCTION__);return 1;}else if (pid == 0) { // childprintf("child is run, pid is : %d\n", getpid());sleep(5);exit(1);}else {int status = 0;pid_t ret = 0;do {ret = waitpid(-1, &status, WNOHANG); // ?阻塞式等待 if (ret == 0) {printf("child is running\n");}handler();} while (ret == 0);if (WIFEXITED(status) && ret == pid) {printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));}else {printf("wait child failed, return.\n");return 1;}}return 0;
}

4. 進程替換

我們先來看一段代碼:
在這里插入圖片描述
在這里插入圖片描述
上面這種現象就叫做程序替換,也就是我自己的程序把系統當中的指令跑起來了。在程序替換的時候,并沒有創建新的進程,只是把當前進程的代碼和數據用新的進程的代碼和數據覆蓋式的進行替換。

  1. 問題一:“為什么我的程序運行結束了”,這段話沒有在顯示器上打印出來?
    答案是一旦程序替換成功就去執行新代碼了,原始代碼的后半部分已經不存在了
  • 有沒有辦法讓后面的代碼能繼續執行?
    答案是有的,創建一個子進程,讓子進程去做替換工作,讓父進程繼續執行后面的代碼。
    在這里插入圖片描述
    效果演示:
    在這里插入圖片描述
    📌Tips:程序替換也能替換我們自己寫的程序,就相當于一種加載器,可以加載各種程序,包括編譯型的解釋型的,程序替換本質上不會創建新的進程
    為什么不會影響父進程呢?
    a.進程具有獨立性 b.數據和代碼發生寫時拷貝
  1. execl的返回值
    在這里插入圖片描述
    execl函數只有失敗返回值,沒有成功返回值
    💦結論exec*系列的函數,不用做返回值判定,只要返回,就是失敗

4.1 替換原理

在這里插入圖片描述

4.2 替換函數

  1. int execl(const char *path,const char *arg,...),第一個參數表示程序+路徑名,第二個參數有個口訣:命令行怎么寫,我們就怎么傳(當然傳-al也是可以的),而把參數一個一個的傳進來我們稱之為list,類似于以鏈表的形式傳給它,所以execl這個l就是llist的意思,execl函數的最后一個參數必須以NULL結尾,表明參數傳遞完成在這里插入圖片描述

  2. int execlp(const char *file,cont char *arg,...),execlp當中的p表示PATH,所以第一個參數只需傳要執行的程序名就行了,因為execlp會自動的在環境變量(PATH)里查找對應的命令,所以execlp一般執行系統級的命令。后面參數的傳遞和上面的相同
    在這里插入圖片描述

  3. int execv(const char *path,char *const argv[]),首先它沒有帶p所以它的參數是path,所以同上上,這里的v就相當于vector,所以第二個參數就以數組的形式呈現了,所以就必須提供一個命令行參數表,就是指針數組,就是把ls -a -l整體放在數組里,一次性傳遞,這個表也必須以NULL結尾。所以我們以前執行的所有命令行參數都是父進程通過execv傳給子進程的
    在這里插入圖片描述

  4. int execvp(const char *path,char *const argv[]),有p所以不用帶路徑
    在這里插入圖片描述

  5. int execvpe(const *file,char *const argv[],char *const envp[])這里的v表示以數組的方式傳進來,p表示不用帶路徑,e表示環境變量,如果非要傳遞環境變量列表,要求:被替換的子進程使用全新的Env列表(自己寫的)
    在這里插入圖片描述
    若要以新增的方式傳遞環境列表呢?
    ?putenv表示哪個進程調用它,就在誰的環境變量表里新增一個環境變量(B是A的子進程,C是B的子進程,如果B在它的環境列表里導入了一個新的環境變量,A的環境列表里看不到,而C的環境列表里能看到)
    ?如果我們就行用execvpe的方式呢?environ
    在這里插入圖片描述
    表示把新增的環境變量添加到環境變量表里面去,然后把環境變量表的起始地址傳給execvpe

  6. int execle(const *path,const *arg,...,char * const envp[])
    在這里插入圖片描述

總結:
這些函數原型看起來很容易混,但只要掌握了規律就很好記。

  • l(list):表示參數采用列表
  • v(vector):參數用數組
  • p(path):有p自動搜索環境變量PATH
  • e(env):表示自己維護環境變量
    在這里插入圖片描述

上面這些函數都是對系統調用進行了語言型的封裝,最后都要調用系統調用execve,為什么要做語言封裝呢?因為程序替換時要面對各種各樣上層替換的場景。所以execve在man手冊第2節
在這里插入圖片描述
下圖exec函數簇一個完整的例子:
在這里插入圖片描述


👍 如果對你有幫助,歡迎:

  • 點贊 ??
  • 收藏 📌
  • 關注 🔔

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/73949.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/73949.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/73949.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

HyperAD:學習弱監督音視頻暴力檢測在雙曲空間中的方法

文章目錄 速覽摘要1. 引言2. 相關工作弱監督暴力檢測雙曲空間中的神經網絡 3. 預備知識雙曲幾何切空間&#xff08;Tangent Space&#xff09;指數映射與對數映射&#xff08;Exponential and Logarithmic Maps&#xff09;3.1 雙曲圖卷積網絡&#xff08;Hyperbolic Graph Con…

動態規劃(6.不同路徑II)

題目鏈接&#xff1a;63. 不同路徑 II - 力扣&#xff08;LeetCode&#xff09; 解法&#xff1a; 本題為不同路徑的變型&#xff0c;只不過有些地方有「障礙物」&#xff0c;只要在「狀態轉移」上稍加修改就可解決。 狀態表示&#xff1a; 對于這種Γ路徑類」的問題&#xf…

深度洞察:DeepSeek 驅動金融行業智能化轉型變革

該文章為軟件測評&#xff0c;不是廣告&#xff01;&#xff01;&#xff01;&#xff01; 目錄 一.金融行業的智能化轉型浪潮? 二.DeepSeek的核心技術剖析 1.DeepSeek 模型的金融智慧? 2.實時聯網搜索&#xff1a;把握金融市場脈搏? 3.RAG 能力&#xff1a;鑄就精準金…

藍橋杯備考----》暴力枚舉---金盞花

這道題&#xff0c;一共12位&#xff0c;給了后六位&#xff0c;我們只要枚舉前六位就行了&#xff0c;當然如果是10的12次方的話&#xff0c;必須要開long long才可以存下&#xff0c;這點我們不要忘了 然后題目中又告訴了沒有前導0&#xff0c;我們可以從100000開始枚舉&…

RAG各類方法python源碼解讀與實踐:利用Jupyter對RAG技術綜合評測【3萬字長文】

檢索增強生成&#xff08;RAG &#xff09;是一種結合信息檢索與生成模型的混合方法。它通過引入外部知識來提升語言模型的性能&#xff0c;從而提高回答的準確性和事實正確性。為了簡單易學&#xff0c;不使用LangChain框架或FAISS向量數據庫&#xff0c;而是利用Jupyter Note…

Python列表2

print("—————————— 列表的相關操作 ————————————")lst.append(x)在列表lst最后增加一個元素 lst.insert(index,x)在列表中第index位置增加一個元素 lst.clear()清除列表lst中所有元素 lst.pop(index)將列表lst中第index位置的元素取出&#xf…

華為OD機試-IPv4地址轉換成整數(Java 2024 B卷 100分)

題目描述 存在一種虛擬 IPv4 地址 Q,由 4 小節組成,每節的范圍為 0~255,以 # 號間隔。虛擬 IPv4 地址可以轉換為一個 32 位的整數。例如: 128#0#255#255 轉換為 32 位整數的結果為 2147549183(0x8000FFFF)1#0#0#0 轉換為 32 位整數的結果為 16777216(0x01000000)現以字…

C語言復習筆記--數組

今天繼續來淺淺推進一下C語言的復習,這次是數組的復習,話不多說,正文開始. 數組的概念 數組是?組相同類型元素的集合,一種自定義類型.數組中元素個數不能為0.數組分為?維數組和多維數組&#xff0c;多維數組?般?較多?的是?維數組. 下面從一維數組說起. 一維數組的創建和…

Canal 解析與 Spring Boot 整合實戰

一、Canal 簡介 1.1 Canal 是什么&#xff1f; Canal 是阿里巴巴開源的一款基于 MySQL 數據庫增量日志解析&#xff08;Binlog&#xff09;中間件&#xff0c;它模擬 MySQL 的從機&#xff08;Slave&#xff09;行為&#xff0c;監聽 MySQL 主機的二進制日志&#xff08;Binl…

《論語別裁》第01章 學而(31) 詩的人生

不過這句話研究起來有一個問題&#xff0c;是詩的問題。我們知道中國文化&#xff0c;在文學的境界上&#xff0c;有一個演變發展的程序&#xff0c;大體的情形&#xff0c;是所謂漢文、唐詩、宋詞、元曲、明小說&#xff0c;到了清朝&#xff0c;我認為是對聯&#xff0c;尤其…

筆記本運行邊緣計算

筆記本電腦可以用來運行PCDN&#xff08;Peer-to-Peer Content Delivery Network&#xff09;服務。實際上&#xff0c;如果你有閑置的筆記本電腦&#xff0c;并且它具備一定的硬件條件和網絡環境&#xff0c;那么它可以成為一個不錯的PCDN節點。 運行PCDN的基本要求 硬件需求…

暗光增強技術研究進展與產品落地綜合分析(2023-2025)

一、引言 暗光增強技術作為計算機視覺與移動影像領域的核心研究方向之一,近年來在算法創新、硬件適配及產品落地方面取得了顯著進展。本文從技術研究與產業應用兩個維度,系統梳理近三年(2023-2025)該領域的關鍵突破,并對比分析主流手機廠商的影像技術優劣勢。 二、暗光增…

多維array和多維視圖std::mdspan

多維數組 這個特性用于訪問多維數組&#xff0c;之前C operator[] 只支持訪問單個下標&#xff0c;無法訪問多維數組。 因此要訪問多維數組&#xff0c;以前的方式是&#xff1a; 重載operator()&#xff0c;于是能夠以m(1, 2) 來訪問第1 行第2 個元素。但這種方式容易和函數…

Python標準庫之os模塊常用方法

一、os模塊簡介 os模塊是Python標準庫中與操作系統交互的一個重要模塊。它提供了非常豐富的方法來處理文件、目錄以及與操作系統相關的操作&#xff0c;讓我們可以編寫跨平臺的代碼&#xff0c;無論是在Windows、Linux還是macOS系統上都能運行。 二、文件和目錄操作 獲取當前…

利用AI讓數據可視化

1. 從問卷星上下載一份答題結果。 序號用戶ID提交答卷時間所用時間來源來源詳情來自IP總分1、《中華人民共和國電子商務法》正式實施的時間是&#xff08;&#xff09;。2、&#xff08;&#xff09;可以判斷企業在行業中所處的地位。3、&#xff08;&#xff09;是指店鋪內有…

K8S學習之基礎三十五:k8s之Prometheus部署模式

Prometheus 有多種部署模式&#xff0c;適用于不同的場景和需求。以下是幾種常見的部署模式&#xff1a; 1. 單節點部署 這是最簡單的部署模式&#xff0c;適用于小型環境或測試環境。 特點&#xff1a; 單個 Prometheus 實例負責所有的數據采集、存儲和查詢。配置簡單&…

【第14節】windows sdk編程:進程與線程介紹

目錄 一、進程與線程概述 1.1 進程查看 1.2 何為進程 1.3 進程的創建 1.4 進程創建實例 1.5 線程查看 1.6 何為線程 1.7 線程的創建 1.8 線程函數 1.9 線程實例 二、內核對象 2.1 何為內核對象 2.2 內核對象的公共特點 2.3 內核對象句柄 2.4 內核對象的跨進程訪…

Python簡單爬蟲實踐案例

學習目標 能夠知道Web開發流程 能夠掌握FastAPI實現訪問多個指定網頁 知道通過requests模塊爬取圖片 知道通過requests模塊爬取GDP數據 能夠用pyecharts實現餅圖 能夠知道logging日志的使用 一、基于FastAPI之Web站點開發 1、基于FastAPI搭建Web服務器 # 導入FastAPI模…

uniapp工程中解析markdown文件

在uniapp中如何導入markdown文件&#xff0c;同時在頁面中解析成html&#xff0c;請參考以下配置&#xff1a; 1. 安裝以下3個依賴包 npm install marked highlight.js vite-plugin-markdown 2. 創建vite.config.js配置文件 // vite.config.js import { defineConfig } fro…

sass介紹

1、Sass簡介 Sass 是一種 CSS 的預編譯語言。它提供了 變量&#xff08;variables&#xff09;、嵌套&#xff08;nested rules&#xff09;、 混合&#xff08;mixins&#xff09;、 函數&#xff08;functions&#xff09;等功能&#xff0c;并且完全兼容 CSS 語法。Sass 能…