Linux信號的處理

目錄

一、信號處理概述:為什么需要“信號”?

二、用戶空間與內核空間:進程的“雙重人格”

三、內核態與用戶態:權限的“安全鎖”

四、信號捕捉的內核級實現:層層“安檢”

五、sigaction函數:精細控制信號行為

1. 函數原型

2. 關鍵結構體

3. 示例代碼:動態修改信號處理

六、可重入函數

1. 問題場景

2. 問題分析

3. 原因解釋

4. 不可重入函數與可重入函數

5. 如何避免重入問題

七、volatile

1. 問題引入

2. 未使用volatile的情況

3. 使用volatile解決問題

4. volatile的作用總結


一、信號處理概述:為什么需要“信號”?

????????想象你在辦公室工作時,突然有人敲門提醒你快遞到了。這里的“敲門”就像操作系統發給進程的信號。信號是操作系統通知進程某個事件發生的機制,例如:

  • Ctrl+C?發送?SIGINT?信號終止進程

  • 程序崩潰時內核發送?SIGSEGV?信號

  • 用戶自定義信號處理邏輯(如保存日志)

????????但進程不會立即處理信號,而是在“合適的時候”——比如從內核態切換回用戶態時。這背后隱藏著操作系統的核心設計邏輯。


二、用戶空間與內核空間:進程的“雙重人格”

每個進程的地址空間分為兩部分:

用戶空間內核空間
存儲進程私有代碼和數據存儲操作系統全局代碼和數據
通過用戶級頁表映射物理內存通過內核級頁表映射物理內存
每個進程看到的內容不同所有進程看到的內容相同

// 示例:用戶空間的變量
int user_data = 100; // 內核空間的代碼(進程無權直接訪問)
void kernel_code() 
{// 管理硬件資源 
}

關鍵點

  • 用戶態代碼無法直接訪問內核空間(權限不足)

  • 執行系統調用(如printf)時,進程會陷入內核,切換到內核態


三、內核態與用戶態:權限的“安全鎖”

用戶態內核態
權限等級低(普通用戶代碼)高(操作系統代碼)
操作限制無法直接訪問硬件可執行任何指令
觸發場景執行普通代碼系統調用、中斷、異常

狀態切換示例

printf("Hello");  // 用戶態 -> 內核態(執行write系統調用) -> 用戶態

具體步驟

1、用戶態:調用printf

  • printf是C標準庫函數,負責格式化字符串(如將"Hello"轉換為字符流)。
  • 若輸出到終端(如屏幕),最終會調用**系統調用write**將數據寫入文件描述符(如標準輸出stdout)。

2、觸發系統調用write

系統調用是用戶程序請求操作系統服務的唯一入口。

write的函數簽名:

ssize_t write(int fd, const void *buf, size_t count);

其中fd=1表示標準輸出,buf指向數據緩沖區,count為數據長度。

3、從用戶態陷入內核態

  • CPU執行特殊的陷入指令(如syscallint 0x80),觸發軟中斷。
  • 硬件自動切換特權級:用戶態(ring 3)→ 內核態(ring 0)。
  • 跳轉到內核中預定義的系統調用處理函數(如sys_write)。

4、內核態:執行sys_write

  • 操作系統驗證參數合法性(如fd是否有效)。
  • 將用戶空間的數據("Hello")從緩沖區復制到內核空間(防止用戶篡改)。
  • 調用設備驅動,將數據發送到終端(如控制臺、SSH會話)。
  • 記錄返回結果(成功寫入的字節數或錯誤碼)。

5、返回用戶態

  • 內核恢復用戶程序的寄存器狀態和堆棧。
  • CPU特權級切換回用戶態(ring 3)。
  • 用戶程序繼續執行printf之后的代碼。

🌴 為什么需要切換特權態?

  • 用戶態的限制
    用戶程序無法直接訪問硬件(如磁盤、網卡)或修改關鍵數據結構(如進程表)。
    例:若允許用戶程序直接寫磁盤,惡意程序可能覆蓋系統文件。

  • 內核態的權限
    操作系統代碼擁有最高權限,可安全管理硬件和資源。
    通過系統調用“代理”用戶程序的請求,確保所有操作受控。


四、信號捕捉的內核級實現:層層“安檢”

????????在計算機系統里,程序運行時可能會遇到一些特殊情況,比如用戶按下某些按鍵或者系統出現了問題,這時候就需要程序能夠及時做出反應。這種反應機制在Linux系統中是通過“信號”來實現的。信號就像是一個信使,負責把發生的事件告訴程序。

????????現在,假設一個程序正在運行它的主函數(main函數),就好比一個人正在按照計劃做一件大事。突然,某個特定的事件發生了,比如用戶按下了一個特殊的按鍵組合(這會觸發SIGQUIT信號)。這時候,系統會暫時中斷這個人的工作,切換到一個專門處理這種情況的模式,也就是“內核態”,由操作系統來處理這個事件。

????????操作系統在處理完這個事件后,準備回到原來的程序繼續工作之前,會檢查有沒有需要特別處理的信號。如果發現有SIGQUIT信號,而且這個程序之前已經告訴過操作系統,當這個信號出現時要按照它自己定義的方式來處理(也就是注冊了一個信號處理函數sighandler),那么操作系統就會安排一個特殊的操作。

????????這個操作就是:不是直接回到原來的主函數繼續做之前的事情,而是先去執行那個專門定義的處理函數sighandler。這就好比在你做一件大事的時候,突然有緊急情況需要你先去處理一下,處理完了再回來繼續做原來的事。

????????需要注意的是,這個處理函數sighandler和原來的主函數(main函數)是兩個完全獨立的任務,它們就像兩條平行的路,沒有直接的調用關系。sighandler有自己的工作空間(不同的堆棧空間)來完成它的任務。

????????當處理函數sighandler完成自己的任務后,它會觸發一個特殊的指令(sigreturn系統調用),再次回到操作系統那里。操作系統會檢查是否還有其他的緊急情況需要處理。如果沒有,就會回到原來的主函數,恢復之前的狀態,繼續完成未做完的事情。

當進程從內核態返回用戶態時,會檢查未決信號集(pending)

  1. 檢查信號狀態

    • 若信號未被阻塞(block),且處理動作為默認忽略
      → 立即處理(如終止進程)并清除pending標志

    • 若處理動作為自定義
      → 先返回用戶態執行處理函數,再通過sigreturn回到內核

  2. 執行自定義處理函數的關鍵步驟

    • 內核不信任用戶代碼:必須返回用戶態執行處理函數

    • 處理函數與主流程獨立(不同堆棧,無調用關系)


五、sigaction函數:精細控制信號行為

1. 函數原型

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);

參數說明:

  • signo?:指定信號的編號(如SIGINT)。

  • act :新的處理動作

  • oldact :保存舊的處理動作

2. 關鍵結構體

結構體 sigaction 的定義如下:

struct sigaction 
{void     (*sa_handler)(int);          // 信號處理函數sigset_t   sa_mask;                   // 額外屏蔽的信號int        sa_flags;                  // 控制選項(通常設為0)// 其他字段(如sa_sigaction)暫不討論
};

3. 示例代碼:動態修改信號處理

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>struct sigaction act, oact;// 自定義信號處理函數
void handler(int signo)
{printf("捕獲信號: %d\n", signo);// 恢復為默認處理方式(僅第一次捕獲時自定義)sigaction(SIGINT, &oact, NULL);
}int main()
{memset(&act, 0, sizeof(act));memset(&oact, 0, sizeof(oact));act.sa_handler = handler;  // 設置自定義處理函數act.sa_flags = 0;          // 無特殊標志sigemptyset(&act.sa_mask); // 不額外屏蔽其他信號// 注冊SIGINT信號(Ctrl+C觸發)sigaction(SIGINT, &act, &oact);while (1){printf("程序運行中...\n");sleep(1);}return 0;
}

運行效果

  1. 第一次按下Ctrl+C?→ 打印“捕獲信號: 2”

  2. 再次按下Ctrl+C?→ 進程終止(已恢復默認行為)


六、可重入函數

1. 問題場景

????????假設我們有一個簡單的鏈表結構,定義了兩個節點 node1 和 node2,以及一個頭指針 head。在 main 函數中,我們調用 insert 函數將 node1 插入到鏈表中。insert 函數的實現分為兩步:首先將新節點的 next 指針指向當前頭節點,然后更新頭指針為新節點。

node_t node1, node2, *head;void insert(node_t *p) {p->next = head; // 第一步:將新節點的 next 指針指向當前頭節點head = p;       // 第二步:更新頭指針為新節點
}int main() {// ... 其他代碼 ...insert(&node1); // 在 main 函數中插入 node1// ... 其他代碼 ...
}

????????在插入 node1 的過程中,假設剛執行完第一步(p->next = head),此時發生了硬件中斷,導致進程切換到內核態。在內核態處理完中斷后,檢查到有信號待處理,于是切換到信號處理函數 sighandler。sighandler 同樣調用 insert 函數,試圖將 node2 插入到同一個鏈表中。

void sighandler(int signo) {// ... 其他代碼 ...insert(&node2); // 在信號處理函數中插入 node2// ... 其他代碼 ...
}

????????當 sighandler 完成插入 node2 的操作并返回內核態后,再次回到用戶態,繼續執行 main 函數中被中斷的 insert 函數的第二步(head = p)。

2. 問題分析

????????理想情況下,我們希望 main 函數和 sighandler 分別將 node1 和 node2 插入到鏈表中,最終鏈表包含兩個節點。然而,實際情況卻并非如此。

  1. main 函數插入 node1 的第一步 :將 node1 的 next 指針指向當前頭節點(初始時 head 為 NULL),此時 node1->next = NULL。

  2. 中斷發生,切換到內核態 :main 函數的 insert 操作被中斷,此時 head 還未更新為 node1。

  3. sighandler 插入 node2 :在信號處理函數中,執行 insert(&node2)。此時 head 仍為 NULL,所以 node2->next = NULL,然后 head 被更新為 node2。

  4. 返回 main 函數繼續執行 :執行 insert 函數的第二步,將 head 更新為 node1。

????????最終,鏈表的頭指針 head 指向 node1,而 node1 的 next 指針為 NULL。node2 被插入后又被覆蓋,實際上沒有真正加入鏈表。

3. 原因解釋

????????這個問題的根源在于 insert 函數被不同的控制流程(main 函數和 sighandler)調用,且在第一次調用還未完成時就再次進入該函數。這種現象稱為“重入”(Reentrant)。insert 函數訪問了一個全局鏈表 head,由于全局變量在多個控制流程之間共享,導致數據不一致。

4. 不可重入函數與可重入函數

  1. 不可重入函數 :如果一個函數在被調用過程中,其內部操作依賴于全局變量或共享資源,并且在函數執行過程中這些資源可能被其他調用者修改,那么這個函數就是不可重入的。像上面的 insert 函數,因為它操作了全局鏈表 head,所以在重入情況下容易出錯。

  2. 可重入函數 :如果一個函數只訪問自己的局部變量或參數,不依賴于全局變量或共享資源,那么它就是可重入的。可重入函數在不同控制流程中被調用時,不會相互干擾。

5. 如何避免重入問題

  1. 避免使用全局變量 :盡量使用局部變量,或者通過參數傳遞必要的數據。

  2. 使用互斥機制 :在多線程或信號處理場景中,使用互斥鎖(如 mutex)來保護共享資源的訪問。

  3. 設計可重入函數 :確保函數只依賴于參數和局部變量,不依賴于外部環境。


七、volatile

????????在C語言中,volatile 是一個經常被提及但又容易被誤解的關鍵字。今天,我們通過一個具體的信號處理例子,來深入理解 volatile 的作用。

1. 問題引入

考慮以下代碼:

#include <stdio.h>
#include <signal.h>int flag = 0;void handler(int sig) {printf("change flag 0 to 1\n");flag = 1;
}int main() {signal(2, handler);while (!flag);printf("process quit normal\n");return 0;
}

????????該程序的功能是:在接收到 SIGINT 信號(如用戶按下 Ctrl+C)時,執行自定義信號處理函數 handler,將全局變量 flag 設置為 1,從而退出 while 循環,程序正常結束。

2. 未使用volatile的情況

????????在未使用 volatile 修飾 flag 的情況下,編譯器可能會對代碼進行優化。例如,當使用 -O2(大寫字母O) 優化選項編譯時,編譯器可能會認為 flag 的值在 while 循環中不會被改變(因為從代碼的靜態分析來看,沒有明顯的修改操作),于是將 flag 的值緩存到 CPU 寄存器中,而不是每次都從內存中讀取。

????????這就會導致一個問題:當信號處理函數 handler 修改了 flag 的值時,while 循環中的條件判斷仍然使用寄存器中的舊值,無法及時檢測到 flag 的變化,程序無法正常退出。這種現象被稱為“數據不一致性”或“內存可見性”問題。

3. 使用volatile解決問題

為了解決上述問題,我們需要使用 volatile 關鍵字修飾 flag 變量:

#include <stdio.h>
#include <signal.h>volatile int flag = 0;void handler(int sig) {printf("change flag 0 to 1\n");flag = 1;
}int main() {signal(2, handler);while (!flag);printf("process quit normal\n");return 0;
}

? ?volatile 告訴編譯器,該變量的值可能會被程序之外的其他因素(如信號處理函數、硬件中斷等)改變,因此編譯器在優化時不會假設該變量的值不變。每次訪問 volatile 修飾的變量時,編譯器都會生成代碼從內存中重新讀取該變量的值,而不是使用寄存器中的緩存值。

????????這樣,在信號處理函數修改了 flag 的值后,while 循環中的條件判斷能夠及時檢測到變化,程序可以正常退出。

4. volatile的作用總結

volatile 的主要作用是保持內存的可見性,確保程序能夠正確地讀取和寫入變量的最新值。在以下場景中,使用 volatile 是必要的:

  1. 信號處理 :當變量可能被信號處理函數修改時,需要使用 volatile 修飾,以確保主程序能夠及時檢測到變量的變化。

  2. 多線程編程 :在多線程環境中,當變量可能被其他線程修改時,volatile 可以防止編譯器優化導致的內存可見性問題。不過,需要注意的是,volatile 并不能完全替代互斥鎖等同步機制,因為它不能保證操作的原子性。

  3. 硬件寄存器訪問 :當程序需要直接訪問硬件寄存器時,這些寄存器的值可能會被硬件異步修改,因此需要使用 volatile 修飾相關的指針或變量。

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

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

相關文章

IntelliJ IDEA 2023.3.1安裝指南從下載到配置的完整教程(附資源下載)

安裝 IntelliJ IDEA 2023.3.1 非常簡單&#xff0c;以下是詳細的安裝步驟&#xff0c;適用于 Windows、macOS 和 Linux 系統。 1. 下載 IntelliJ IDEA IntelliJ IDEA下載鏈接&#xff1a;https://pan.quark.cn/s/3ad975664934 選擇適合你的操作系統的版本&#xff1a; Ultimat…

【HarmonyOS Next】鴻蒙中App、HAP、HAR、HSP概念詳解

【HarmonyOS Next】鴻蒙中App、HAP、HAR、HSP概念詳解 &#xff08;圖1-1&#xff09; 一、鴻蒙中App、HAP、HAR、HSP是什么&#xff1f; &#xff08;1&#xff09;App Pack&#xff08;Application Package&#xff09; 是應用發布的形態&#xff0c;上架應用市場是以App Pa…

配置阿里云yum源

配置阿里云yum源 修改默認的yum倉庫&#xff0c;把原有的移動到創建的目錄里&#xff08;踢出國外的yum源&#xff09; # 切換到/ect/yum.repos.d/目錄下 cd /etc/yum.repos.d/ # 新建repo目錄 mkdir repo # 把原有的移動到創建的目錄里 mv ./*.repo ./repo/配置yum源 # 找到…

在C#的MVC框架framework項目的使用ajax,及源碼下載

在C# MVC框架中使用AJAX實現異步請求,有助于提高應用程序的性能和用戶體驗。 在MVC框架framework項目中&#xff0c;ajax使用方法如下 1.在Controller類中&#xff0c;創建一個新的方法&#xff08;例如&#xff1a;GetRes&#xff09;&#xff0c;該方法處理AJAX請求并返回J…

Linux部署DHCP服務腳本

#!/bin/bash #部署DHCP服務 #userli 20250319#檢查是否為root用戶 if[ "$USER" ! "root" ] thenecho "錯誤&#xff1a;非root用戶&#xff0c;權限不足&#xff01;"exit 0 fi#配置網絡環境 read -ep "請給本機配置一個IP地址(不…

vulhub Matrix-Breakout

1.下載靶機&#xff0c;打開靶機和kali虛擬機 2.查詢kali和靶機ip 3.瀏覽器訪問 訪問81端口有登陸界面 4.掃描敏感目錄 kali dirb 掃描 一一訪問 robot.txt提示我們繼續找找&#xff0c;可能是因為我們的字典太小了&#xff0c;我們換個掃描器換個字典試下,利用kali自帶的最大…

科普類——雙目立體視覺與 RGBD 相機的簡單對比

雙目立體視覺與 RGBD 相機生成的深度圖在原理、性能和應用場景上有顯著差異。以下是兩者的詳細對比和分析&#xff1a; 1. 原理差異 (1) 雙目立體視覺 (Stereo Vision) 原理&#xff1a; 通過兩個攝像頭模擬人眼視差&#xff0c;計算匹配像素點的水平位移&#xff08;視差&…

深度學習項目--基于DenseNet網絡的“乳腺癌圖像識別”,準確率90%+,pytorch復現

&#x1f368; 本文為&#x1f517;365天深度學習訓練營 中的學習記錄博客&#x1f356; 原作者&#xff1a;K同學啊 前言 如果說最經典的神經網絡&#xff0c;ResNet肯定是一個&#xff0c;從ResNet發布后&#xff0c;很多人做了修改&#xff0c;denseNet網絡無疑是最成功的…

面試八股 —— Redis篇

重點&#xff1a;緩存 和 分布式鎖 緩存&#xff08;穿透&#xff0c;擊穿&#xff0c;雪崩&#xff09; 降級可作為系統的保底策略&#xff0c;適用于穿透&#xff0c;擊穿&#xff0c;雪崩 1.緩存穿透 2.緩存擊穿 3.緩存雪崩 緩存——雙寫一致性 1.強一致性業務&#xff08…

mapbox-gl源碼中解析style加載地圖過程詳解

我將結合 Mapbox GL JS 的源碼示例&#xff0c;一步一步講解 style 的解析和地圖加載過程&#xff0c;幫助大家深入理解其內部機制。 Mapbox GL JS 是一個強大的 Web 地圖庫&#xff0c;利用 WebGL 技術渲染交互式地圖。其核心功能之一是通過樣式&#xff08;style&#xff09…

瑞薩RA系列使用JLink RTT Viewer輸出調試信息

引言 還在用UART調試程序么?試試JLINK的RTT Viewer吧!不需占用UART端口、低資源暫用、實時性高延時微秒級,這么好的工具還有什么理由不用了! 目錄 一、JLink RTT Viewer 簡介 二、軟件安裝 三、工程應用 3.1 SEGGER_RTT驅動包 3.2 手搓宏定義APP_PRINT 3.3 使用APP_…

MySQL 入門大全:查詢語言分類

&#x1f9d1; 博主簡介&#xff1a;CSDN博客專家&#xff0c;歷代文學網&#xff08;PC端可以訪問&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移動端可微信小程序搜索“歷代文學”&#xff09;總架構師&#xff0c;15年工作經驗&#xff0c;…

1.Windows+vscode+cline+MCP配置

文章目錄 1.簡介與資源2.在windows中安裝vscode及Cline插件1. 安裝vscode2. 安裝Cline插件3. 配置大語言模型3. 配置MCP步驟(windows) 1.簡介與資源 MCP官方開源倉庫 MCP合集網站 參考視頻 2.在windows中安裝vscode及Cline插件 1. 安裝vscode 2. 安裝Cline插件 Cline插件…

性能測試過程實時監控分析

性能監控 前言一、查看性能測試結果的3大方式1、GUI界面報告插件2、命令行運行 html報告3、后端監聽器接入儀表盤 二、influxDB grafana jmeter測試監控大屏1、原理&#xff1a;2、linux環境中influxDB 安裝和配置3、jmerer后端監聽器連接influxDB4、linux環境總grafana環境搭…

【Linux我做主】淺談Shell及其原理

淺談Linux中的Shell及其原理 Linux中Shell的運行原理github地址前言一、Linux內核與Shell的關系1.1 操作系統核心1.2 用戶與內核的隔離 二、Shell的演進與核心機制2.1 發展歷程2.2 核心功能解析2.3 shell的工作流程1. 用戶輸入命令2. 解析器拆分指令3. 擴展器處理動態內容變量替…

可視化圖解算法:鏈表中倒數(最后)k個結點

1. 題目 描述 輸入一個長度為 n 的鏈表&#xff0c;設鏈表中的元素的值為ai &#xff0c;返回該鏈表中倒數第k個節點。 如果該鏈表長度小于k&#xff0c;請返回一個長度為 0 的鏈表。 數據范圍&#xff1a;0≤n≤105&#xff0c;0 ≤ai≤109&#xff0c;0 ≤k≤109 要求&am…

在線教育網站項目第四步:deepseek騙我, WSL2不能創建兩個獨立的Ubuntu,但我們能實現實例互訪及外部訪問

一、說明 上一章折騰了半天&#xff0c;搞出不少問題&#xff0c;今天我們在deepseek的幫助下&#xff0c;完成多個獨立ubuntu24.04實例的安裝&#xff0c;并完成固定ip&#xff0c;實踐證明&#xff0c;deepseek不靠譜&#xff0c;浪費我2個小時時間&#xff0c;我們將在下面實…

CMake 保姆級教程

CMake 是一個跨平臺的構建工具&#xff0c;用于生成適合不同平臺和編譯器的構建系統文件&#xff08;如 Makefile 或 Visual Studio 項目文件&#xff09;。 在 Windows 下使用 CMake 構建項目時&#xff0c;CMake 會根據 CMakeLists.txt 文件生成適合 Windows 的構建系統文件&…

zabbix數據庫溯源

0x00 背景 zabbix數據庫如果密碼泄露被登錄并新增管理員如何快速發現&#xff1f;并進行溯源&#xff1f; 本文介紹數據庫本身未開啟access log的情況。 0x01 實踐 Mysql 數據庫查insert SELECT * FROM sys.host_summary_by_statement_type where statement like %insert% 查…

Spring Boot集成PageHelper:輕松實現數據庫分頁功能

Spring Boot集成PageHelper&#xff1a;輕松實現數據庫分頁功能 1. 為什么需要分頁&#xff1f; 分頁是處理大數據量查詢的核心技術&#xff0c;其重要性體現在&#xff1a; 性能優化&#xff1a;避免單次查詢返回過多數據導致內存溢出或響應延遲。用戶體驗&#xff1a;前端展…