linux程序(不含程序間通訊)掃盲

WangChangAn發表於2024-06-11

參考清華大學<<Linux程式設計>>的筆記,如有錯誤請幫忙指出

5.1 邏輯控制流與併發

邏輯控制流:一個核心抽象為一個PC,PC隨著時間流逝而指向不同的指令的過程就是邏輯控制流.
併發:同一時間段內有多個流執行
並行:同一時間段內有多個流執行在多個核心上

5.2 程序的基本概念

宏觀上看,程序就是進行中的程式的過程

5.2.1 程序概念'結構'描述

Linux將程序抽象為PCB,其管理程序的程式程式碼資料集,及許多屬性,屬性可分為:

  1. 程序描述資訊
  2. 程序控制資訊
  3. 程序資源資訊
  4. CPU現場資訊

5.2.2 程序的基本狀態與狀態轉換

5種基本狀態:

  1. 建立狀態:準備必要的非CPU資源
  2. 就緒狀態:已經準備好所有必要的非CPU資源
  3. 執行狀態:具備就緒狀態的所有條件,且已獲得CPU
  4. 阻塞狀態:等待額外的非CPU資源,這類資源成為了必要的非CPU資源的子集,此時執行緒主動放棄CPU
  5. 終止狀態:執行緒終止後進入終止狀態,不會立即清理

5.2.5 操作程序的工具

  1. ps
    1. ps -ef 檢視所有程序資訊,可配合| grep過濾
    2. ps l 檢視當前使用者的程序資訊
    3. ps -u 檢視當前使用者的程序資源消耗
  2. kill
    1. 建議cli:kill -l 檢視可用的訊號
    2. kill -9 強制關閉程序
    3. kill -15 請求程式正常終止
    4. kill -SIGSTOP pid 掛起程序
    5. kill -SIGCONT pid 恢復程序

5.2.6 程式設計讀取程序屬性

概念:

  • pid:程序id
  • ppid:parent pid,父程序id
  • UID:使用者id,分為
    • 建立者UID
    • 授權啟動者EUID
  • GID:組id,分為
    • 建立者所屬的組的GID
    • 授權啟動者所屬的組的EGID
      示例:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);

uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
uid_t getegid(void);

5.3 程序控制

5.3.1 建立程序

linux系統啟動後,建立第一個程序init,其後的所有程序都是不斷fork產生的

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(){
    int x = 1;
    pid_t pid;

    pid = fork();
    if(pid==0);

}

結合上述程式碼剖析子程序建立過程`

  1. 父程序在執行fork前,準備好了資料集(父)
  2. 執行fork時,將本執行緒狀態壓入棧
  3. 執行fork中,系統複製PCB(父)作為子程序PCB並分配給子程序獨立的PID,然後複製資料集(父)於子程序記憶體空間,然後建立資料集(子)
  4. 在fork執行後,因為PCB(子)完全複製了PCB(父),且PCB中有各種程式現場資訊(包括壓入棧的資訊,如PC),所以下一步彈出棧到暫存器後,子程序的下一條指令與父程序相同,都是將fork的返回值進行返回,此時,linux規定:子程序的PID對自己來說是0,對父程序來說是上一步中獨立的PID.然後繼續執行程式碼...

編寫多程序併發程式

  1. 畫出程式框架
  2. 透過pid==0判斷是否是子程序,進而執行對應task

5.3.2 多程序併發特徵與執行流程分析

為控制多個程序中語句的執行順序為非隨機,需要使用linux作業系統提供的機制

5.3.3 程序終止與程序回收

1.程序終止
正常終止vs異常終止
exit(code)是正常終止
abort(code)是程式檢出錯誤後主動異常終止
檢出子程序的返回碼:父程序執行waitpid()得到子程序終止狀態,下面會詳細說明如何使用
2.程序殭屍問題
殭屍程序:已經執行完畢(釋放了大多數資源)但是仍有自己的PCB
清理工作:waitpid函式會讀取子程序的PCB中的資訊(退出狀態,其他資訊等),然後徹底清理子程序
3.回收程序(清理工作)
對於擁有子程序的父程序,使用waitpid函式來管理回收子程序

#include<sys/type.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);

pid_t wait(int* status);

waitpid(pid_t pid, int *status, int options)
返回值:正常情況是某個子程序pid,錯誤時返回-1(因無子程序而錯誤,會設定errno[一個宏]=ECHILD,因訊號中斷而錯誤,會設定errno=EINTR).
引數:
int* options;

  • 0:預設值,表示waitpid掛起呼叫程序的執行
    int* pid:
  • pid>0 : 等待一個指定的pid
  • pid == -1 : 等待所有的子程序
    • 此處有一個等待集合的概念,指的是要等待的程序有哪些,目的是方便管理子程序,假設A建立了A1,A2;A2又建立了B1,B2{此時,Ax構成了程序組,Bx構成了另一個程序組,還有一個由所有Ax和Bx共同構成的大程序組},那麼A程序中waitpid的形參pid == -1就可以等待所有Ax和Bx,也能透過設定程序組的方式,傳參給pid形參,來管理指定程序組

int* status:
status儲存了子程序的返回的狀態資訊,使用特定的宏可以查詢status
WIFEXITED(status):查詢正常終止(exit或return)則返回真
WEXITSTATUS(status):獲取正常終止時的返回的狀態資訊
WIFSIGALED(status):查詢訊號終止則返回真
WTERMSIG(status):獲取導致子程序終止的訊號編號

5.3.4 程序休眠

#include<unistd.h>
//返回值是0(正常返回)或剩餘的秒數(被訊號中斷之後就會這樣)
unsigned int sleep(unsigned int sec);
//微秒級別,秒,分秒,毫秒,微秒,納秒
void usleep(unsigned int usec);
//讓程序休眠until接到訊號
int pause(void);

5.3.5 載入並執行程式

exec函式族執行過程:將系統分配的資源和資料集和程式的程式碼返還,但是保留PCB和pid,然後再為指定的程式分配資源,然後載入程式程式碼和資料集等

#include<unistd.h>
int execve(const char *fname, const char *argv[], const char *envp[]);
int execvp(const char *fname, const char *argv[]);
int execlp(const char *fle, const char *arg,...);

envp是環境變數的二級指標,一級指標指向一個含有真實字串的陣列,可在c程式中使用以下介面set,get環境變數

#include <stdlib.h>
char* getenv(const char* name);

int setenv(const char* name, const char* newvalue, int overwrite);

void unsetenv(const char *name);

5.3.7 fork和exec和dup

...

非本地跳轉

常規的函式呼叫棧結構,使得我們在深層函式中出現錯誤需要呼叫最外層的處理機制時,要一層層解開呼叫棧,這顯然複雜.非本地跳轉的核心思想是使用setjmp函式在env 緩衝區(在 C 語言中,env 緩衝區通常指的是用於儲存環境變數的記憶體區域。)中儲存當前呼叫環境(包括PC,棧指標,通用目的暫存器),然後在需要的地方使用longjmp函式將最近一次setjmp儲存的'呼叫環境'還原.
函式簽名

#include <setjmp.h>
int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);

int longjmp(jmp_buf env, int retval);
int siglongjmp(sigjmp_buf env, int savesigs);
  • setjmp的執行會儲存呼叫環境到env,然後返回0;
    1. longjmp的執行會將env還原到呼叫環境
    1. 並會觸發最後初始化同一env的setjmp函式的函式呼叫返回部分,這個呼叫返回會攜帶longjmp中設定的retval.
  • 上述第2部分的呼叫是必須的,僅僅還原現場資訊只能調整pc,但是我們還需要把retval返回給介面的使用者,不然讓使用者自己在記憶體中找retval的位置可太危險了.

5.4 訊號機制

向程序傳入訊號,kill指令就可以.
在CLI中鍵入 man 7 signal可以檢視linux系統支援的訊號
程序受到訊號後就需要處理之,預設的處理方式一般有2種:忽略和終止
名詞解釋:

  • 捕獲訊號:程序接收到訊號後,如果該訊號在程序中註冊了對應編號的訊號,則會捕獲之,然後呼叫程式設計師自己安裝在程式中的對應了該訊號的處理函式
  • 忽略訊號:程序不予處理
  • 終止訊號:進入終止態(還記得嗎,程序終止後仍有自己的pcb,資源被沒有被100%回收)

5.4.2 訊號的有關術語

傳送一個訊號到目的程序有兩個步驟組成:

  • 傳送訊號: 核心update程序裡的某個狀態,傳送訊號的原因有2個
      1. 核心內部檢測到了系統事件(事件是一種由核心記錄並由核心傳播的資料結構)
      1. 程序之間透過kill函式傳遞訊號
  • 接收訊號: 即響應訊號.程序接收到訊號之後可以表現為以下三種行為:捕獲並處理,忽略和終止.
  • 待處理訊號: 發出但是沒被接收的訊號.每一種型別訊號只存在一個訊號實體,多餘的訊號實體會被丟棄.

5.4.3 傳送訊號的方法

程序組的概念中實現了程序發訊號的相關機制

1.程序組:
程序組是n個程序的集合,通常處理一組作業.一個程序只屬於一個程序組,這不難理解,因為一個程序組繫結一組作業,本程序組的程序一般無權干涉其他作業組,只需要處理自己的作業組.
介面:

#include<unistd.h>
pid_t getpgrp(void);//當前程序的程序組id
int setpgid(pid_t pid, pid_t pgid);

setpgid是設定pgid的介面將pid程序的程序組設為pgid.邏輯上是先有程序再有程序組.當pid0時,指的是呼叫setpgid的(當前)執行緒;當pgid0時,指的是以pid所指代的程序為父程序的同時新開一個程序組.
2./bin/killCLI命令傳送訊號
3.鍵盤傳送訊號(如ctrl+c)
4.kill函式和raise函式發訊號
kill向其他執行緒(組)發訊號.raise向程序自己發訊號

#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int sig);
int raise(int sig);

上述,若pid>0,則是向pid程序發訊號,若pid<0,則是向abs(pid)程序組發訊號
5.alarm函式發訊號
unsigned int alarm(unsigned int seconds);
向核心設定核心在sec秒後,向呼叫程序傳送SIGALRM訊號.相當於時鐘定時.這個虛擬的鬧鐘每次至多設定一次有效定時,因為設定之後都會取消之前的定時.
若sec為0,則清除任何待處理的時鐘,返回先前設定的鬧鐘的剩餘時間.

5.4.4 接收訊號的過程

當從核心態返回到程序的使用者態時,核心會檢查程序的待處理訊號集合,通常會選擇其中最小的訊號號碼,然後將該訊號傳輸給該程序,進而該程序會做出某種行為A(如進入訊號處理函式)(,如果在訊號處理函式中再次陷入核心態,那麼從核心態返回到使用者態時不再執行傳輸待處理訊號給程序的行為,因為此時程序的狀態屬於訊號尚未處理完成的狀態,如果貿然再次傳遞訊號,就可能導致原先的訊號處理過程丟失這是不允許的),A行為完成後,執行程序的下一條指令.
我們知道,程序在接收到訊號後有2個預設行為:忽略或終止,還有其他訊號,能夠讓程序執行不同行為,一般預設行為都能更改,除了SIGSTOP和SIGKILL.
透過以下介面能設定自定義處理的訊號和對應的處理器控制代碼

#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signal函式中會使用回撥函式用法,為signum呼叫形參handler,同時:

  • 若handler是SIG_IGN,則忽略之
  • 若handler是SIG_DFL,則重置之
    序號 訊號名稱 訊號說明 預設處理方式
    1 SIGHUP 終端控制程序終止或者控制終端關閉 終止程序
    2 SIGINT interrupt 訊號,通常由 Ctrl+C 產生 終止程序
    3 SIGQUIT 退出訊號,通常由 Ctrl+\ 產生 生成核心轉儲並終止程序
    4 SIGILL 非法指令訊號,程序執行了非法的指令 生成核心轉儲並終止程序
    5 SIGTRAP 跟蹤/斷點異常訊號 生成核心轉儲並終止程序
    6 SIGABRT 程序呼叫 abort() 函式產生的訊號 生成核心轉儲並終止程序
    7 SIGEMT 協處理器使用異常訊號 終止程序
    8 SIGFPE 算術異常訊號,如被零除或溢位 生成核心轉儲並終止程序
    9 SIGKILL 終止程序訊號,此訊號不能被忽略或阻塞 終止程序
    10 SIGBUS 匯流排錯誤訊號,如訪問未對齊的記憶體地址 生成核心轉儲並終止程序
    11 SIGSEGV 段錯誤訊號,程序訪問非法記憶體地址 生成核心轉儲並終止程序
    12 SIGSYS 無效的系統呼叫 生成核心轉儲並終止程序
    13 SIGPIPE 寫一個沒有讀端的管道或socket 終止程序
    14 SIGALRM 定時器超時訊號 終止程序
    15 SIGTERM 程序終止訊號 終止程序
    16 SIGURG 緊急條件訊號,通常用於套接字 忽略
    17 SIGSTOP 停止程序訊號,此訊號不能被忽略或阻塞 停止程序
    18 SIGTSTP 停止或暫停程序訊號,通常由使用者輸入Ctrl+Z產生 停止程序
    19 SIGCONT 繼續執行停止的程序訊號 忽略
    20 SIGCHLD 子程序狀態改變訊號 忽略
    21 SIGTTIN 後臺程序讀終端訊號 停止程序
    22 SIGTTOU 後臺程序寫終端訊號 停止程序
    23 SIGPOLL 輪詢式 I/O 訊號 終止程序
    24 SIGXCPU 超過CPU時間限制訊號 生成核心轉儲並終止程序
    25 SIGXFSZ 超過檔案大小限制訊號 終止程序
    26 SIGVTALRM 虛擬時鐘訊號 終止程序
    27 SIGPROF Profileing 時鐘訊號 終止程序
    28 SIGWINCH 視窗大小改變訊號 忽略
    29 SIGINFO status request from keyboard 忽略
    30 SIGUSR1 使用者自定義訊號1 終止程序
    31 SIGUSR2 使用者自定義訊號2 終止程序
    32 SIGSYS 無效的系統呼叫 生成核心轉儲並終止程序

5.4.6 可移植訊號處理

linux系統會重啟被中斷的系統呼叫,所謂系統呼叫就是核心態才能執行的指令
不同系統對於同一個訊號的處理有不同語義,一個較好的方式是使用posix定義的統一的訊號處理介面

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

但是該介面不常用,<<Linux程式設計>>中包裝了該介面為以下介面,它滿足

  • 只有當前被處理的訊號型別被阻塞
  • 訊號不會排隊等待
  • 儘量重啟被中斷的系統呼叫
handler_t* Signal(int signum,handler_t* handler)
  struct sigaction action,old_action;

  action.sa_handler = handler;
  sigemptyset(&action.sa_mask);
  action.sa_flags=SA_RESTART;

  if(sigaction(signum,&action,&old_action)<0)
    perror("Signal error");

  return(old_action.sa_handler);

5.4.7 訊號處理引起的競爭問題

這屬於併發問題,通常是邏輯執行順序與指令時間執行順序不一致導致的衝突,這會發生在訊號上,畢竟訊號是突發的.一種解決策略是使用訊號掩碼技術在需要邏輯控制的地方阻塞相關訊號,這種策略在組合語言中也經常使用.
程序概念中有一個blocked位向量,表示阻塞訊號集合,sigmask的介面操作這個阻塞訊號集合.
以下是sigmask的介面介紹

#include<signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t* oldset);

how包括以下選項

  • SIG_BLOCK:將set+到block向量中
  • SIG_UNBLOCK:將set從block向量中移除
  • SIG_SETMASK:將block設定為mask
    oldset是儲存以前的block位向量的值
    還有其他函式,功能如其名
int sigemptyset(sigset_t* set);
int sigfillset(sigset_t* set);
int sigaddset(sigset_t* set, int signum);
int sigdelset(sigset_t* set, int signum);
int sigismember(sigset_t* set, int signum);

5.5 守護程序

一些基礎功能的提供者,不過這些功能是特供給本程序組的.類似於一個駐場角色.
建立守護程序的步驟如下:

  1. fork建立新程序
  2. 脫離terminal,session,程序組
  3. 關閉開啟的檔案描述符
  4. 改變當前工作目錄
  5. 處理檔案描述符012
#include

5.6 程序,系統呼叫與核心

程序是為了實現任務而分配的資源,其對系統資源的訪問是受限的,在程序當中的操作被成為使用者態
系統呼叫是核心為程序使用核心許可權而抽象的介面
核心管理計算機的所有資源,擁有最高的許可權,在核心當中的操作被稱為使用者態

待續...

相關文章