程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> linux服務器開發二(系統編程)--進程相關,linux--

linux服務器開發二(系統編程)--進程相關,linux--

編輯:關於C語言

linux服務器開發二(系統編程)--進程相關,linux--


進程相關的概念

程序與進程

  • 程序,是指編譯好的二進制文件,在磁盤上,不占用系統資源(CPU、內存、打開的文件、設備、鎖等等)。

  • 進程,是一個抽象的概念,與操作系統原理聯系緊密。進程是活躍的程序,占用系統資源。在內存中執行(程序運行起來,產生一個進程)。

  • 程序 --> 劇本(紙),進程 -->戲(舞台、演員、燈光、道具等等)。同一個劇本可以在多個舞台同時上演。同樣,同個程序也可以加載為不同的進程(彼此之間互不影響)。如:同時開兩個終端。各自都有一個bash,但彼此ID不同。

並發

  • 並發,在操作系統中,一個時間段中有多個進程都處於已啟動運行到運行完畢之間的狀態。但任一個時刻點上仍只有一個進程在運行。

  • 例如,當下,我們使用計算機時可以邊聽音樂邊聊天上網。若籠統的將他們均看做一個進程的話,為什麼可以同時運行呢?因為並發。

  • 分時復用CPU

分時復用CPU

單道程序設計

  • 所有進程一個一個排隊執行。若A阻塞,B只能等待,即使CPU處於空閒狀態。而在人機交互時阻塞的出現是必然的。所有這種模型在系統資源利用上及其不合理,在計算機發展史上存在不久,大部分已被淘汰了。

多道程序設計

  • 在計算機內存中同時存放幾道相互獨立的程序,它們在管理程序控制之下,相互穿插的運行。多道程序設計必須有硬件基礎作為保證。

  • 時鐘中斷即為多道程序設計模型的理論基礎。並發時,任意進程在執行期間都不希望放棄CPU。因此系統需要一種強制讓進程讓出CPU資源的手段。時鐘中斷有硬件基礎作為保障,對進程而言不可抗拒。操作系統中的中斷處理函數,來負責調度程序執行。

  • 在多道程序設計模型中,多個進程輪流使用CPU(分時復用CPU資源)。而當下常見CPU為納米級,1秒可以執行大約10億條指令。由於人眼的反應速度是毫秒級,所以看似同時在運行。

    1s = 1000ms
    1ms = 1000us
    1us = 1000ms
    
  • 實質上,並發是宏觀並行,微觀串行! -- 推動了計算機蓬勃發展,將人類引入了多媒體時代。

CPU與MMU

CPU和MMU

  • 內存管理單元MMU

內存管理單元MMU

進程控制塊PCB

  • 我們知道,每個進程在內核中都有一個進程控制塊(PCB)來維護進程相關的信息,Linux內核的進程控制塊是task_struct結構體。

  • /usr/src/linux-headers-3.16.0-30/include/linux/sched.h文件中可以查看struct task_struct結構體定義。其內部成員有很多,我們重點掌握以下部分即可:

    • 進程ID。系統中每個進程唯一的ID,在C語言中用pid_t類型表示,其實就是一個非負整數。
    • 進程的狀態,有就緒、運行、掛起、停止等狀態。
    • 進程切換時需要保存和恢復的一些CPU寄存器。
    • 描述虛擬地址空間的信息。
    • 描述控制終端的信息。
    • 當前工作目錄(Current Working Directory)
    • umask掩碼。
    • 文件描述符,包含很多指向file結構體的指針。
    • 和信號相關的信息。
    • 用戶id和組id。
    • 會話(Session)和進程組。
    • 進程可以使用的資源上限(Resource Limit)。

進程狀態

  • 進程基本的狀態有5種。分別為初始態,就緒態,運行態,掛起態與終止態。其中初始態為進程准備階段,常與就緒態結合來看。

進程狀態

環境變量

  • 環境變量,是指在操作系統中用來指定操作系統運行環境的一些參數。通常具備以下特征:

    • 1、字符串(本質)。
    • 2、有統一的格式:名=值[:值]。
    • 3、值用來描述進程環境信息。
    • 存儲形式:與命令行參數類似。char*[]數組,數組名environ,內部存儲字符串,NULL作為哨兵結尾。
    • 使用形式:與命令行參數類似。
    • 加載位置:與命令行參數類似。位於用戶區,高於stack的起始位置。
    • 引入環境變量表:須聲明環境變量。extern char **environ;
    • 練習:打印當前進程的所有環境變量。

      #include <stdio.h>
      
      extern char **environ;
      
      int main(int argc, char *argv[])
      {
          int i;
          for(i = 0; environ[i]; i++)
          {
              printf("%s\n", environ[i]);
          }
          return 0;
      }
      

常見環境變量

  • 按照慣例,環境變量字符串都是name=value這樣的形式,大多數name由大寫字母加下劃線組成,一般把name的部分叫做環境變量,value的部分則是環境變量的值。環境變量定義了進程的運行環境,一些比較重要的環境變量的含義如下:

    • PATH

      • 可執行文件的搜索路徑。ls命令也是一個程序,執行它不需要提供完整的路徑名/bin/ls,然而通常我們執行當前目錄下的程序a.out卻需要提供完整的路徑名./a.out,這是因為PATH環境變量的值裡面包含了ls命令所在的目錄/bin,卻不包含a.out所在的目錄。PATH環境變量的值可以包含多個目錄,用:號隔開。在shell中用echo命令可以查看這個環境變量的值:

        echo $PATH
        
    • SHELL

      • 當前shell,它的值通常是/bin/bash。
    • TERM

      • 當前終端類型,在圖形界面終端下它的值通常是xterm,終端類型決定了一些程序的輸出顯示方式,比如圖形界面終端可以顯示漢字,而字符終端一般不行。
    • LANG

      • 語言和locale, 決定了字符編碼以及時間、貨幣等信息的顯示格式。
    • HOME

      • 當前用戶主目錄的路徑,很多程序需要在主目錄下保存配置文件,使得每個用戶在運行該程序時都有自己的一套配置。

getenv函數

  • 獲取環境變量

  • char *getenv(const char *name);

    • 成功:返回環境變量的值;
    • 失敗:NULL(name)不存在。
  • 練習:編程實現getenv函數。

setenv函數

  • 設置環境變量的值

  • int setenv(const char *name, const char * value, int overwrite);

    • 成功:0;
    • 失敗:-1
    • 參數overwrite取值:
      • 1:覆蓋原環境變量
      • 0:不覆蓋。(該參數常用於設置新環境變量,如:ABC=day-night)

unsetenv函數

  • 刪除環境變量name的定義

  • int unsetenv(const char *name);

    • 成功:0;
    • 失敗:-1
    • 注意事項:name不存在仍返回0(成功), 當name命名為"ABC="時則會出錯。
  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main(int argc, char * argv[])
    {
        char * val;
        const char * name = "ABD";
    
        val = getenv(name);
        printf("1, %s = %s\n", name, val);//ABD = NULL
    
        setenv(name, "efg", 1);
    
        val = getenv(name);
        printf("2, %s = %s\n", name, val);//ABD = efg
    
        int ret = unsetenv(name);
        printf("ret = %d \n", ret);//0
    
        val = getenv(name);
        printf("3, %s = %s \n", name, val);//ABD = NULL
    
        return 0;
    }
    

進程控制

fork函數

  • 創建一個子進程。
  • pid_t fork(void);
    • 失敗返回-1;
    • 成功返回:1、父進程返回子進程的ID(非負);2、子進程返回0。
  • pid_t類型表示進程ID,但為了表示-1, 它是有符號整形。(0不是有效進程ID,init最小為1)。
  • 注意返回值,不是fork函數能返回兩個值,而是fork後,fork函數變為兩個,父子需各自返回一個。
  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
        printf("father process exec begin...");
    
        pid_t pid = fork();
        if(pid == -1)
        {
            perror("fork error");
            exit(1);
        }
        else if(pid == 0)
        {
            printf("I'm child, pid = %u, ppid = %u \n", getpid(), getppid());
        }
        else
        {
            printf("I'm father, pid = %u, ppid = %u \n", getpid(), getppid());
            sleep(1);
        }
    
        printf("father process exec end...");
        return 0;
    }
    
  • 循環創建n個子進程

    • 一次fork函數調用可以創建一個子進程。那麼創建n個子進程應該怎麼實現呢?
    • 簡單想,for(i = 0; i< n; i++){ fork() }即可。但這樣創建的是N個子進程嗎?

循環創建n個子進程

  • 錯誤示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
        printf("father process exec begin...");
    
        pid_t pid;
        int i;
        for(i = 0; i < 5; i++)
        {
            pid_t pid = fork();
            if(pid == -1)
            {
                perror("fork error");
                exit(1);
            }
            else if(pid == 0)
            {
                printf("I'm %dth child, pid = %u, ppid = %u \n", i+1, getpid(), getppid());
            }
            else
            {
                printf("I'm father, pid = %u, ppid = %u \n", getpid(), getppid());
                sleep(1);
            }        
        }
    
        printf("father process exec end...");
        return 0;
    }
    
  • 正確的調用方式

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[])
    {
        printf("father process exec begin...");
    
        pid_t pid;
        int i;
        for(i = 0; i < 5; i++)
        {
            pid_t pid = fork();
            if(pid == -1)
            {
                perror("fork error");
                exit(1);
            }
            else if(pid == 0)
            {
                //不讓子進程現創建孫子進程
                break;
            }       
        }
    
        if(i<5)
        {
            sleep(i);
            printf("I'm %dth child, pid = %u, ppid = %u \n", i+1, getpid(), getppid());
        }
        else
        {
            sleep(i);
            printf("I'm father");
        }
    
        return 0;
    }
    
  • getpid函數

    • 獲取進程ID。pid_t getpid(void);
  • getppid函數

    • 獲取父進程ID。pid_t getppid(void);
  • getuid函數

    • 獲取當前進程實際用戶ID。uid_t getuid(void);
    • 獲取當前進程有效用戶ID。uid_t geteuid(void);
  • getgid函數

    • 獲取當前進程使用用戶組ID。gid_t getgid(void);
    • 獲取當前進程有效用戶組ID。gid_t getegid(void);
  • 進程共享

    • 父子進程之間在fork後,有哪些相同,那些想異之處呢?
      • 父子相同處:全局變量、.data、.text、棧、堆、環境變量、用戶ID、宿主目錄、進程工作目錄、信號處理方式……
      • 父子不同處:進程ID、fork返回值、父進程ID、進程運行時間、鬧鐘(定時器)、未決定信號集。
      • 似乎,子進程復制了父進程0-3G用戶空間內容,以及父進程的PCB,但pid不同。真的每fork一個子進程都要將父進程的0-3G地址空間完全拷貝一份,然後在映射至物理內存嗎?
      • 當然不是,父子進程間遵循讀時共享寫時復制的原則。這樣設計,無論子進程執行父進程的邏輯還是執行自己的邏輯都能節省內存開銷。
      • 練習:編寫程序測試,父子進程是否共享全局變量。
      • 重點注意!躲避父子進程共享全局變量的知識誤區!
    • 重點:父子進程共享:1、文件描述符(打開文件的結構體)。2、mmap建立的映射區(進程間通信詳解)。
    • 特別的,fork之後的父進程先執行還是子進程先執行不確定。取決於內核所使用的調度算法。
  • gdb調試

    • 使用gdb調試的時候,gdb只能跟蹤一個進程。可以在fork函數調用之前,通過指令設置gdb調試工具跟蹤父進程或者是跟蹤子進程。默認跟蹤父進程。
    • set follow-fork-mode child 命令設置gdb在fork之後跟蹤子進程。
    • set follow-fork-mode parent 設置跟蹤父進程。
    • 注意:一定要在fork函數調用之前設置才有效。

exec函數族

  • fork創建子進程後執行的是和父進程相同的程序(但有可能執行不同的代碼分支), 子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec並不創建新進程,所以調用exec前後該進程的id並未改變。

  • 將當前進程的.text、.data替換為所要加載的程序的.text、.data,然後讓進程從新的.text第一條指令開始執行,但進程ID不變,換核不換殼。

  • 其實有六種以exec開頭的函數,統稱exec函數:

    • int execl(const char *path, const char *arg, ...);
    • int execlp(const char *file, const char *arg, ...);
    • int execle(const char *path, const char *arg, ..., char * const envp[]);
    • int execv(const char *path, char *const argv[]);
    • int execvp(const char *file, char *const argv[]);
    • int execve(const *path, char * const argv[], char *const envp[]);

execlp函數

  • 加載一個進程,借助PATH環境變量

  • int execlp(const char *file, const char *arg, ...); 成功:無返回;失敗:-1。

  • 參數1:要加載的程序的名字。該函數需要配合PATH環境變量來使用,當PAHT中所有目錄搜索後沒有參數1則出錯返回。

  • 該函數通常用來調用系統程序。如:ls、date、cp、cat等命令。

  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(int argc, char * argv[])
    {
        pid_t pid;
        pid = fork();
        if(pid == -1)
        {
            perror("fork error");
            exit(1);
        }
        else if (pid > 0)
        {
            sleep(1);
            printf("parent");
        }
        else
        {
            execlp("ls", "ls", "-l", "-a", NULL);
        }
    
        return 0;
    }
    

execl函數

  • 加載一個進程,通過路徑+程序名來加載。

  • int execl(const char *path, const char *arg, ...);成功:無返回;失敗:-1

  • 對比execlp, 如加載“ls”命令帶有-l,-F參數

    execlp("ls", "ls", "-l", "-F", NULL);           使用程序名在PATH中搜索
    execl("/bin/ls", "ls", "-l", "-F", NULL);       使用參數1給出的絕對路徑搜索
    

execvp函數

  • 加載一個進程,使用自定義環境變量env。

  • int execvp(const char *file, const char *argv[]);

  • 變參形式:1、... 2、argv[] (main 函數也是變參函數,形式上等同於int main(int argc, char *argv0, ...))

  • 變參終止條件:1、NULL結尾;2、固參指定。

  • execvp與execlp參數形式不同,原理一致。

    char *argv[] = {"ls", "-l", "-a", NULL};
    execvp("ls", argv);
    execv("/bin/ls", argv);
    
  • 練習:將當前系統中的進程信息,打印到文件中。

    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char *argv[])
    {
        int fd;
        fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if(fd < 0)
        {
            perror("open ps.out error");
            exit(1);
        }
    
        dup2(fd, STDOUT_FILENO);
    
        execlp("ps", "ps", "aux", NULL);//執行成功,後面的語句不會執行
        perror("execlp error");
        exit(1);
    
        return 0;
    }
    

exec函數族一般規律

  • exec函數一旦調用成功即執行新的程序,不返回。只有失敗才返回,錯誤值-1。所以通常我們直接在exec函數調用後直接調用perror和exit(),無需if判斷。

    l(list)             命令行參數列表。
    p(path)             搜索file時使用path變量
    v(vector)           使用命令行參數數組
    e(environment)      使用環境變量數組,不使用進程原有的環境變量,設置新加載程序運行的環境變量。
    
  • 事實上,只有execve是真正的系統調用,其它五個函數最終都是調用execve,所以execve在man手冊第2節,其它函數在man手冊第3節。這些函數之間的關系如下圖所示。

exec函數族

回收子進程

孤兒進程

  • 孤兒進程:父進程先於子進程結束,則子進程成為孤兒進程,子進程的父進程成為init進程,稱為init進程領養孤兒進程。

  • 示例,產生一個孤兒進程:

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(int argc, char * argv[])
    {
        pid_t pid;
        pid = fork();
    
        if(pid == 0)
        {
            while(1)
            {
                printf("I am child, my parent pid is %d\n", getppid());
                sleep(1);
            }
        }
        else if(pid >0)
        {
            printf("I am parent, my pid is %d \n", getpid());
            sleep(9);
            printf("----------parent going to die---------\n");
        }
        else
        {
            perror("fork");
            return 1;
        }
    
        return 0;
    }
    

僵屍進程

  • 僵屍進程:進程終止,父進程尚未回收,子進程殘留資源(PCB)存放於內核中,變成僵屍(Zombie)進程。

  • 特別注意:僵屍進程是不能使用kill命令清除掉的。因為kill命令只是用來終止進程的,而僵屍進程已經終止。

  • 思考,用什麼辦法可清除僵屍進程呢?

  • 示例,產生一個僵屍進程:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[])
    {
        pid_t pid;
        pid = fork();
    
        if(pid == 0)
        {
            printf("---child, my parent=%d, going to sleep 10s \n", getppid());
            sleep(10);
            printf("-------------child die--------------\n");
        }
        else if(pid > 0)
        {
            while(1)
            {
                printf("I am parent, pid = %d, myson = %d \n", getpid(), pid);
            }
        }
        else {
            perror("fork error");
            exit(1);
        }
        return 0;
    }
    

wait函數

  • 一個進程在終止時會關閉所有文件描述符,釋放在用戶空間分配的內存,但它的PCB還保留著,內核在其中保存了一些信息:如果是正常終止則保存著退出狀態,如果是異常終止則保存著導致該進程終止的信號是哪個。這個進程的父進程可以調用wait或waitpid獲取這些信息,然後徹底清除掉這個進程。我們知道一個進程的退出狀態可以在shell中用特殊變量$?查看,因為Shell是它的父進程,當它終止時Shell調用wait或waitpid得到它的退出狀態,同時徹底清除掉這個進程。

  • 父進程調用wait函數可以回收子進程終止信息。該函數有三個功能:

    • 1、阻塞等待子進程退出。
    • 2、回收子進程殘留資源。
    • 3、獲取子進程結束狀態(退出原因)。
    • pid_t wait(int *status);
      • 成功:清理掉的子進程ID;
      • 失敗:-1(沒有子進程)。
  • 當進程終止時,操作系統的隱式回收機制會:

    • 1、關閉所有文件描述符。
    • 2、釋放用戶空間分配的內存。內核的PCB仍存在。其中保存該進程的退出狀態。(正常終止-->退出值班;異常終止-->終止信號)。
  • 可使用wait函數傳出參數status來保存進程的退出狀態。借助宏函數來進一步判斷進程終止的具體原因。宏函數可以分為如下三組:

    1、WIFEXITED(status)         為非0 --> 進程正常結束
       WEXITSTATUS(status)      如上宏為真,使用此宏 --> 獲取進程退出狀態(exit的參數)
    2、WIFSIGNALED(status)       為非0 --> 進程異常結束
       WTERMSIG(status)         如上宏為真,使用此宏 --> 取得使進程終止的那個信號的編號。
    3、WIFSTOPPED(status)        為非0 --> 進程處於暫停狀態
       WSTOPSIG(status)         如上宏為真,使用此宏 --> 取得使進程暫停的那個信號的編號。
       WIFCONTINUED(status)     為真 --> 進程暫停後已經繼續運行。
    
  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[])
    {
        pid_t pid, wpid;
        int status;
        pid = fork();
    
        if(pid == 0)
        {
            printf("---child, my parent=%d, going to sleep 10s \n", getppid());
            sleep(30);
            printf("-------------child die--------------\n");
            //exit(100);
            return 100;
        }
        else if(pid > 0)
        {
            wpid = wait(&status);
            if(wpid == -1)
            {
                perror("wait error");
                exit(1);
            }
            if(WIFEXITED(status))
            {
                printf("child exit with %d \n", WEXITSTATUS(status));
            }
            if(WIFSIGNALED(status))
            {
                printf("child killed by %d \n", WTERMSIG(status));
            }
    
            while(1)
            {
                printf("I am parent, pid = %d, myson = %d \n", getpid(), pid);
            }
        }
        else {
            perror("fork error");
            exit(1);
        }
        return 0;
    }
    

waitpid函數

  • 作用同wait,但可指定pid進程清理,可以不阻塞。

  • pid_t waitpid(pid_t pid, int *status, int options);成功:返回清理掉的子進程ID;失敗:-1(無子進程)。

  • 特殊參數和返回情況:

    參數pid:
            >0      回收指定ID的子進程
            -1      回收任意子進程(相當於wait)
            0       回收和當前調用waitpid一個組的所有子進程
            <-1     回收指定進程組內的任意子進程
    參數status
    參數options:
            0           (wait)阻塞回收
            WNOHANG     非阻塞回收(輪詢)
    返回:
            成功          pid
            失敗          -1
            0           參數3傳WNOHANG,並且子進程尚未結束
    
  • 注意:一次wait或waitpid調用只能清理一個子進程,清理多個子進程應使用循環。

  • 作業:父進程fork 3個子進程,三個子進程一個調用ps命令,一個調用自定義程序1(正常),一個調用自定義程序2(會出現錯誤)。父進程使用waitpid對其子進程進行回收。

  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[])
    {
        int n = 5, i;//默認創建五個子進程
        pid_t p, q;
        pid_t wpid;
    
        if(argc == 2)
        {
            n = atoi(argv[1]);
        }
    
        for(i = 0; i < n; i++)
        {//出口1,父進程專用出口
            p = fork();
            if(p == 0)
            {
                break;//出口2,子進程出口,i不自增
            }
            else if (i == 3)
            {
                q = p;
            }
        }
    
        if(n == i)
        {
            sleep(n);
            printf("I am parent, pid = %d\n", getpid(), getgid());
            //waitpid(q, NULL, 0);  //1、回收第三個子進程
            //while(waitpid(-1, NULL, 0));  //2、等價於wait(NULl),阻塞回收任意子進程
            do
            {
                //3、非阻塞回收任意子進程
                //如果wpid == 0 說明子進程正在運行
                wpid = waitpid(-1, NULL, WNOHANG);
                if(wpid > 0)
                {
                    n--;
                }
                sleep(1);
            }
            while(n > 0)
    
            printf("wait finish\n");
        }
        else
        {
            sleep(i);
            printf("I'm %dth child, pid = %d, gid = %d \n", i+1, getpid(), getgid());
        }
    
        return 0;
    }
    

IPC(InterProcess Communication)進程間通信

  • linux環境下,進程地址空間相互獨立,每個進程各自有不同的用戶地址空間。任何一個進程的全局變量在另一個進程中都看不到,所以進程和進程之間不能相互訪問,要交換數據必須通過內核,在內核中開辟一塊緩沖區,進程1把數據從用戶空間拷到內核緩沖沖區,進程2再從內核緩沖區把數據讀走,內核提供的這種機制稱為進程間通信(IPC)。

進程間通信IPC

  • 在進程間完成數據傳遞需要借助操作系統提供特殊的方法,如:文件、管道、信號、共享內存、消息隊列、套接字、命名管道等。隨著計算機的蓬勃發展,一些方法由於自身設計缺陷被淘汰或者棄用。常用的進程間通信方式有:
    • 管道(使用最簡單)
    • 信號(開銷最小)
    • 共享映射區(無血緣關系)
    • 本地套接字(最穩定)

管道

管道的概念

  • 管道是一種最基本的IPC機制,作用於有血緣關系的進程之間,完成數據傳遞。調用pipe系統函數即可創建一個管道。有如下特質:

    • 1、其本質是一個偽文件(實為內核緩沖區)
    • 2、由兩個文件描述符引用,一個表示讀端,一個表示寫端。
    • 3、規定數據從管道的寫端流入管道,從讀端流出。
  • 管理的原理:管道實為內核使用環形隊列機制,借助內核緩沖區(4k)實現。

  • 管道的局限性:

    • 1、數據自己讀不能自己寫。
    • 2、數據一旦被讀走,便不在管道中存在,不可反復讀取。
    • 3、由於管道采用半雙工通信方式。因此,數據只能在一個方向上流動。
    • 4、只能在有公共祖先的進程間使用管道。

pipe函數

  • int pipe(int pipefd[2]);

  • 參數

    • fd[2] (傳出參數)
  • 返回值

    • 成功:0
    • 失敗:-1,設置errno

管道的讀寫行為

  • 讀管道

    • 管道中有數據
      • read返回實際讀到的字節數
    • 管道中無數據
      • 寫端全關閉:read返回0
      • 仍有寫端打開:阻塞等待
  • 寫管道

    • 讀端全關閉
      • 進程異常終止(SIGPIPE信號)
    • 有讀端打開
      • 管道未滿:寫數據,返回寫入字節數
      • 管道已滿:阻塞(少見)
  • 父子進程間通信ls | wc -l

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(void)
    {
        pid_t pid;
        int fd[2];
    
        pipe(fd);
        pid = fork();
    
        //子進程
        if(pid == 0){
            //子進程從管道中讀數據,關閉寫端
            close(fd[1]);
            //讓wc從管道中讀取數據
            dup2(fd[0], STDIN_FILENO);
            //wc命令默認從標准讀入取數據
            execlp("wc", "wc", "-l", NULL);
        }else {
            //父進程向管道中寫數據,關閉讀端
            close(fd[0]);
            //將ls的結果寫入管道中
            dup2(fd[1], STDOUT_FILENO);
            //ls輸出結果默認對應屏幕
            execlp("ls", "ls", NULL);
        }
    
        return 0;
    }
    
  • 兄弟進程間通信

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/wait.h>
    
    int main(void)
    {
        pid_t pid;
        int fd[2], i;
    
        pipe(fd);
    
        for(i = 0; i < 2; i++){
            if((pid = fork()) == 0){
                break;
            }
        }
    
        if(i == 0){        //兄
            close(fd[0]);           //寫,關閉讀端
            dup2(fd[1], STDOUT_FILENO);
            execlp("ls", "ls", NULL);
        }else if(i == 1){  //弟
            close(fd[1]);           //讀,關閉寫端
            dup2(fd[0], STDIN_FILENO);
            execlp("wc", "wc", "-l", NULL);
        }else {
            close(fd[0]);
            close(fd[1]);
            for(i = 0; i < 2; i++){ //兩個兒子wait兩次
                wait(NULL);
            }
        }
    
        return 0;
    }
    

管道緩沖區的大小

  • 命令:ulimit -a

  • 函數:fpathconf, 參數2:__PC_PIPE_BUF

管道的優劣

  • 優點:

    • 實現手段簡單
  • 缺點:

    • 單向通信
    • 只能有血緣關系進程間使用

FIFO

  • 命名管道(Linux基礎文件類型)

  • 創建

    • 命令:mkfifo
    • 函數:int mkfifo(const char *pathname, mode_t mode);
      • 參數:
        • name
        • mode:8進制
      • 返回值:
        • 成功:0
        • 失敗:-1,設置errno
  • 無血緣關系進程間通信

    • 使用同一FIFO
    • 可多讀端,多寫端

共享存儲映射

文件進程間通信

  • 使用文件也可以完成IPC,理論依據是,fork後,父子進程共享文件描述符。也就共享打開的文件。

  • 練習:編程測試,父子進程共享打開的文件。借助文件進行進程間通信。

  • 思考:無血緣關系的進程可以打開同一個文件進行通信嗎?為什麼?

  • 示例

    /**
    *父子進程共享打開的文件描述符------使用文件完成進程間共享
    */
    
    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <fcntl.h>
    #include <sys/wait.h>
    
    int main(int argc, char *argv[])
    {
        int fd1, fd2;
        pid_t pid;
        char * str = "----test for shared fd in parent child process----\n";
    
        pid = fork();
        if(pid < 0)
        {
            perror("fork error");
            exit(1);
        }
        else if(pid == 0)
        {
            fd1 = open("test.txt", O_RDWR);
            if(fd1 < 0)
            {
                perror("open error");
                exit(1);
            }
    
            //子進程寫入數據
            write(fd1, str, strlen(str));
            printf("child wrote over...\n");
        }
        else
        {
            fd2 = open("test.txt", O_RDWR);
            if(fd2 < 0)
            {
                perror("open error");
                exit(1);
            }
            sleep(1);   //保證子進程寫入數據
    
            //父進程讀取數據
            int len = read(fd2, buf, sizeof(buf));
            write(STDOUT_FILENO, buf, len);
    
            wait(NULL);
        }
    
        return 0;
    }
    

存儲映射I/O

  • 存儲映射I/O(Memory-mmapped I/O)使一個磁盤文件與存儲空間中一個緩沖區相映射。於是當從緩沖區取數據,就相當於讀文件中的相應字節。於此類似,將數據存入緩沖區,則相應的字節就自動寫入文件。這樣,就可在不適用read和write函數的情況下,使用地址(指針)完成I/O操作。

  • 使用這種方法,首先應通知內核,將一個指定文件映射到存儲區域中。這個映射工作可以通過mmap函數來實現。

mmap

  • mmap函數

    • void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

    • 返回:

      • 成功:返回創建的映射區的首地址。
      • 失敗:MAP_FAILED宏。
    • 參數:

      • addr:建立映射區的首地址,由Linux內核指定。使用時,直接傳遞NULL。
      • length:欲創建映射區的大小。
      • prot:映射區權限PROT_READ、PROT_WRITE、PROT_READ | PROT_WRITE。
      • flags:標志位參數(常用於設定更新物理區域、設置共享、創建匿名映射區)
        • MAP_SHARED:會將映射區所做的操作反映到物理設備(磁盤)上。
        • MAP_PRIVATE:映射區所做的修改不會反映到物理設備。
      • fd:用來建立映射區的文件描述符。
      • offset:映射文件的偏移(4k的整數倍)
    • 示例

      #include <stdio.h>
      #include <stdlib.h>
      #include <string.h>
      #include <unistd.h>
      #include <fcntl.h>
      #include <sys/mmap.h>
      
      void sys_err(char *str)
      {
          perror(str);
          exit(1);
      }
      
      int main(int argc, char *argv[])
      {
          char *p = NULL;
          int fd = open("test.txt", O_CREAT|O_REWR, 0644);
          if(fd < 0)
          {
              sys_err("open error");
          }
      
          int len = ftruncate(fd, 4);
          if(len == -1)
          {
              sys_err("ftruncate error");
          }
      
          p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
          if(p == MAP_FAILED)
          {
              sys_err("mmap error");
          }
      
          strcpy(p, "abc");  //寫數據
      
          int ret = munmap(p, 4);
          if(ret == -1)
          {
              sys_err("munmap error");
          }
          close(fd);
      
          return 0;
      }
      
  • munmap函數

    • 同malloc函數申請內存空間類似的,mmap建立的映射區在使用結束後也應調用類似free的函數來釋放。

    • int munmap(void *addr, size_t length);

      • 成功:0;
      • 失敗:-1。
    • 借鑒malloc和free函數原型,嘗試封裝自定義smalloc,sfree來完成映射區的建立和釋放。思考函數應如何設計?

  • mmap注意事項

    • 思考:

      • 1、可以open的時候O_CREAT一個新文件來創建映射區嗎?(可以)
      • 2、如果open時O_RDONLY,mmap時PROT參數指定PROT_READ|PROT_WRITE會怎樣?(權限不足)
      • 3、文件描述符先關閉,對mmap映射有沒有影響?(沒有影響)
      • 4、如果偏移量為1000會怎樣?(不行,必須是4k的整數倍)
      • 5、對mem越界操作會怎樣?(不能越界)
      • 6、如果mem++,mmap可否成功?(不行)
      • 7、mmap什麼情況下會調用失敗?(每個參數都有影響)
      • 8、如果不檢測mmap的返回值,會怎樣?(會死得很難看)
    • 總結:使用mmap時務必注意以下事項:

      • 1、創建映射區的過程中,隱含著一次對映射文件的讀操作。
      • 2、當MAP_SHARED時,要求:映射區的權限<=文件打開的權限(出於對映射區的保護)。而MAP_PRIVATE則無所謂,因為mmap中的權限是對內存的限制。
      • 3、映射區的釋放與文件關閉無關。只要映射建立成功,文件可以立即關閉。
      • 4、特別注意,當映射文件大小為0時,不能創建映射區。所以:用於映射的文件必須要有實際大小!!mmap使用時常常會出現總線錯誤,通常是由於共享文件存儲空間大小引起的。
      • 5、munmap傳入的地址一定是mmap的返回地址。堅決杜絕指針++、--操作。
      • 6、如果文件偏移量必須為4k的整數倍。
      • 7、mmap創建映射區出錯概率非常高,一定要檢查返回值,確保映射區建立成功再進行後續操作。

mmap父子進程通信

  • 父子等有血緣關系的進程之間也可以通過mmap建立的映射區來完成數據通信。但相應的要在創建映射區的時候指定對應的標志位參數flags:

    • MAP_PRIVATE:(私有映射)父子進程各自獨占映射區。
    • MAP_SHARED:(共享映射)父子進程共享映射區。
  • 練習:父進程創建映射區,然後fork子進程,子進程修改映射區內容,然後,父進程讀取映射區內容,查驗是否共享。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/mman.h>
    #include <sys/wait.h>
    
    int var = 100;
    
    int main(int argc, char *argv[])
    {
        int *p;
        pid_t pid;
    
        int fd;
        fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
        if(fd < 0)
        {
            perror("open error");
            exit(1);
        }
    
        unlink("temp"); //刪除臨時文件目錄項,使之具備被釋放條件
        ftruncate(fd, 4);
    
        p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
        //p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
        if(p == MAP_FAILED) //注意:不是p == NULL
        {
            perror("mmap error");
            exit(1)
        }
        close(fd); //映射區建立完畢,即可關閉文件
    
        pid = fork();//創建子進程
        if(pid == 0)
        {
            *p = 2000;
            var = 1000;
            printf("child, *p = %d, var = %d\n", *p, var);
        }
        else
        {
            sleep(1);
            printf("parent, *p = %d, var = %d\n", *p, var);
            wait(NULL);    
        }
    
        int ret = mnumap(p, 4);//釋放映射區
        if(ret == -1)
        {
            perror("mnumap error");
            exit(1);
        }
    
        return 0;
    }
    
  • 結論:父子進程共享:

    • 1、打開的文件
    • 2、mmap建立的映射區(但必須要使用MAP_SHARED)

匿名映射

  • 通過使用我們發現,使用映射區來完成文件讀寫操作十分方便,父子進程間通信也較容易。但缺陷是,每次創建映射區一定要依賴一個文件才能實現。通常為了建立映射區要open一個temp文件,創建好了再unlink、close掉,比較麻煩。可以直接使用匿名精映射來代替。其實Linux系統給我們提供了創建匿名映射區的方法,無需依賴一個文件即可創建映射區。同樣需要借助標志位參數flags來指定。
  • 使用MAP_ANONYMOUS(或MAP_ANON),如:

    int *p = mmap(NUll, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
    "4"隨意舉例,該位置大小,可依實際需要填寫。
    
  • 需要注意的是,MAP_ANONYMOUS和MAP_ANON這兩個宏是Linux操作系統特有的宏。在類Unix系統中如無該宏定義,可使用如下兩步來完成匿名映射區的建立。

    1、fd = open("/dev/zero", O_RDWR);
    2、p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
    
  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/mman.h>
    #include <sys/wait.h>
    
    int var = 100;
    
    int main(int argc, char *argv[])
    {
        int *p;
        pid_t pid;
    
        p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
        if(p == MAP_FAILED) //注意:不是p == NULL
        {
            perror("mmap error");
            exit(1)
        }
    
        pid = fork();//創建子進程
        if(pid == 0)
        {
            *p = 2000;
            var = 1000;
            printf("child, *p = %d, var = %d\n", *p, var);
        }
        else
        {
            sleep(1);
            printf("parent, *p = %d, var = %d\n", *p, var);
            wait(NULL);    
        }
    
        int ret = mnumap(p, 4);//釋放映射區
        if(ret == -1)
        {
            perror("mnumap error");
            exit(1);
        }
    
        return 0;
    }
    

mmap無血緣關系進程間通信

  • 實質上mmap是內核借助文件幫我們創建了一個映射區,多個進程之間利用該映射區完成數據傳遞。由於內核空間多進程共享,因此無血緣關系的進程間也可以使用mmap來完成通信。只要設置相應的標志位參數flags即可。若想實現共享,當然應該使用MAP_SHARED了。

  • 示例

    • 讀端

      #include <stdio.h>
      #include <sys/stat.h>
      #include <fcntl.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <sys/mman.h>
      #include <string.h>
      
      struct STU
      {
          int id;
          char name[20];
          char sex;
      }
      
      void sys_err(char *str)
      {
          perror(str);
          exit(-1);
      }
      
      int main(int argc, char *argv[])
      {
          int fd;
          struct STU student;
          struct STU *mm;
      
          if(argc < 2)
          {
              printf("./a.out file_shared\n");
              exit(-1);
          }
      
          fd = open(argv[1], O_RDONLY);
          if(fd == -1)
          {
              sys_err("open error");
          }
      
          mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
          if(mm == MAP_FAILED)
          {
              sys_err("mmap error");
          }
      
          close(fd);
      
          while(1)
          {
              printf("id=%d\t name=%s\t %c\n", mm->id, mm->name, mm->sex);
              sleep(2);
          }
      
          munmap(mm, sizeof(student));
      
          return 0;
      }
      
    • 寫端

      #include <stdio.h>
      #include <sys/stat.h>
      #include <sys/types.h>
      #include <fcntl.h>
      #include <unistd.h>
      #include <stdlib.h>
      #include <sys/mman.h>
      #include <string.h>
      
      struct STU
      {
          int id;
          char name[20];
          char sex;
      }
      
      void sys_err(char *str)
      {
          perror(str);
          exit(-1);
      }
      
      int main(int argc, char *argv[])
      {
          int fd;
          struct STU student = {10, "xiaoming", 'm'};
          char *mm;
      
          if(argc < 2)
          {
              printf("./a.out file_shared\n");
              exit(-1);
          }
      
          fd = open(argv[1], O_RDWR | O_CREAT, 0664);
          ftruncate(fd, sizeof(student));
      
          mm = mmap(NULL, sizeof(student), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
          if(mm == MAP_FAILED)
          {
              sys_err("mmap error");
          }
      
          close(fd);
      
          while(1)
          {
              memcpy(mm, &student, sizeof(student));
              student.id++;
              sleep(1);
          }
      
          munmap(mm, sizeof(student));
      
          return 0;
      }
      

階段綜合練習一

  • 實現文件多進程拷貝。
  • 假設有一個超大文件,需對其完成拷貝工作。為提高效率,可采用多進程並行拷貝的方法實現。假設文件大小為len,共有n個進程對該文件進行拷貝。那每個進程拷貝的字節數應為len/n。但未必一定能整除,我們可以選擇最後一個進程負責剩余部分拷貝工作。可使用len%(len/n)將剩余部分大小求出。
  • 為降低實現復雜度,可選用mmap來實現源、目標文件的映射,通過指針操作內存地址,設置每個進程拷貝的起始、結束位置。借助MAP_SHARED選項將內存中所做的修改反映到物理磁盤上。 多進程拷貝
  • 實現步驟:
    • 1、指定創建子進程的個數
    • 2、打開源文件
    • 3、打開目的文件,不存在則創建
    • 4、獲取文件大小
    • 5、根據文件大小拓展目標文件
    • 6、為源文件創建映射
    • 7、為目標文件創建映射
    • 8、求出每個子進程該拷貝的字節數
    • 9、創建N個子進程
    • 10、子進程完成分塊拷貝(注意最後一個子進程拷貝起始位置)
    • 11、釋放映射區

階段綜合練習二

  • 實現簡單的交互式的Shell。
  • 使用已學習的各種C函數實現一個簡單的交互式的Shell, 要求:

    • 1、給出提示符,讓用戶輸入一行命令,識別程序名和參數並調用適當的exe函數執行程序,待執行完成後再次給出提示符。
    • 2、該程序可識別和處理以下符號:
      • 1)簡單的標准輸入輸出重定向:仿照例“父子進程ls | wc -l”,先dup2然後exec。
      • 2)管理(|):Shell進程先調用pipe創建管道,然後fork出兩個子進程。一個子進程關閉讀端,調用dup2將寫端賦給標准輸出,另一個子進程關閉寫端,調用dup2把讀端賦給標准輸入,兩個子進程分別調用exec執行程序,而Shell進程把管道的兩端都關閉,調用wait等待兩個子進程終止。類似於“兄弟進程間ls | wc -l”練習的實現。
    • 你的程序應該可以處理以下命令:

      ls -l -R > file1
      cat < file1 | wc -c > file1
      
    • 實現步驟:

      • 1、接收用戶輸入命令字符串,拆分命令及參數存儲。(自行設計數據存儲結構)
      • 2、實現普通命令加載功能。
      • 3、實現輸入、輸出重定向的功能。
      • 4、實現管道。
      • 5、支持多重管道。

階段綜合練習三

  • 簡易本地聊天室
  • 借助IPC完成一個簡易的本地聊天功能。設有服務器端和客戶端兩方。服務啟動監聽客戶端請求,並負責記錄處理客戶端登錄、聊天、退出等相關數據。客戶端完成登錄、發起聊天等操作。可以借助服務器轉發向某個指定客戶端完成數據包發送(聊天)。
  • 客戶端向服務器發送數據包,可采用如下協議格式來存儲客戶端數據,使用”協議號“區分客戶端請求和各種狀況。服務器依據包號處理客戶端對應請求。 簡易聊天室

信號的概念

  • 信號在我們的生活中隨處可見,如:古代戰爭中摔杯為號;現代戰爭中的信號彈;體育比賽中使用的信號槍......
  • 他們都有共性:
    • 1、簡單
    • 2、不能攜帶大量信息
    • 3、滿足某個特設條件才發送
  • 信號是信息的載體,Linux/Unix環境下,古老、經典的通信方式,現下依然是主要的通信手段。
  • Unix早期版本就提供了信號機制,但不可靠,信號可能丟失。Berkeley和AT&T都對信號模型做了更改,增加了可靠信號機制。但彼此不兼容。POSIX.1對可靠信號例程進行了標准化。

信號的機制

  • A給B發送信號,B收到信號之前執行自己的代碼,收到信號後,不管執行到程序的什麼位置,都要暫停運行,去處理信號,處理完畢再繼續執行。與硬件中斷類似--異步模式。但信號是軟件層面上實現的中斷,早期常被稱為”軟中斷“。
  • 信號的特質:由於信號是通過軟件方法實現,其實現手段導致信號很強的延時性。但對於用戶來說,這個延遲時間非常短,不易察覺。
  • 每個進程收到的所有信號,都是由內核負責發送的,內核處理。

與信號相關的事件和狀態

  • 產生信號
    • 1、按鍵產生,如:ctrl+c、ctrl+z、ctrl+\
    • 2、系統調用產生,如:kill、raise、abort
    • 3、軟件條件產生,如:定時器alarm
    • 4、硬件異常產生,如:非法訪問內存(段錯誤)、除0(浮點數例外)、內存對齊出錯(總線錯誤)
    • 5、命令產生,如:kill命令
  • 遞達:遞達並且到達進程。
  • 未決:產生和遞達之間的狀態。主要由於阻塞(屏蔽)導致該狀態。
  • 信號的處理方式:
    • 1、執行默認動作
    • 2、忽略(丟棄)
    • 3、捕捉(調用戶處理函數)
  • Linux內核的進程控制塊PCB是一個結構體,task_struct,除了包含進程id,狀態,工作目錄,用戶id,組id,文件描述符表,包含了信號相關的信息,主要指阻塞信號集和未決信號集。
  • 阻塞信號集(信號屏蔽字):將某些信號加入集合,對他們設置屏蔽,當屏蔽x信號後,再收到該信號,該信號的處理將推後(解除屏蔽後)
  • 未決信號集
    • 1、信號產生,未決信號集中描述該信號的位立刻翻轉為1,表信號處於未決狀態。當信號被處理對應位翻轉回為0。這一時刻往往非常短暫。
    • 2、信號產生後由於某些原因(主要是阻塞)不能抵達。這類信號的集合稱之為未決信號集。在屏蔽解除前,信號一直處於未決狀態。

阻塞信號集與未決信號集

信號的編號

  • 可以使用kill -l命令來查看當前系統可使用的信號有哪些。

     1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
     6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
    11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
    16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
    21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
    26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
    31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
    38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
    43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
    48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
    53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
    58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
    63) SIGRTMAX-1  64) SIGRTMAX
    
  • 不存在編號為0的信號。其中1-31號信號稱之為常規信號(也叫普通信號或標准信號),34-64稱之為實時信號,驅動編程與硬件相關。名字上區別不大。而前32個名字各不相同。

信號4要素

  • 與變量三要素類似的,每個信號也有其必備4要素,分別是:
    • 1、編號
    • 2、名稱
    • 3、事件
    • 4、默認處理動作
  • 可通過man 7 signal查看幫助文檔獲取。也可查看/usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h

    Signal     Value     Action   Comment
    ──────────────────────────────────────────────────────────────────────
    SIGHUP        1       Term    Hangup detected on controlling terminal or death of controlling process
    SIGINT        2       Term    Interrupt from keyboard
    SIGQUIT       3       Core    Quit from keyboard
    SIGILL        4       Core    Illegal Instruction
    SIGABRT       6       Core    Abort signal from abort(3)
    SIGFPE        8       Core    Floating point exception
    SIGKILL       9       Term    Kill signal
    SIGSEGV      11       Core    Invalid memory reference
    SIGPIPE      13       Term    Broken pipe: write to pipe with no readers
    SIGALRM      14       Term    Timer signal from alarm(2)
    SIGTERM      15       Term    Termination signal
    SIGUSR1   30,10,16    Term    User-defined signal 1
    SIGUSR2   31,12,17    Term    User-defined signal 2
    SIGCHLD   20,17,18    Ign     Child stopped or terminated
    SIGCONT   19,18,25    Cont    Continue if stopped
    SIGSTOP   17,19,23    Stop    Stop process
    SIGTSTP   18,20,24    Stop    Stop typed at terminal
    SIGTTIN   21,21,26    Stop    Terminal input for background process
    SIGTTOU   22,22,27    Stop    Terminal output for background process
    
    The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
    
  • 在標准信號中,有一些信號是有三個“Value”,第一個值通常對alpha和sparc架構有效,中間值針對x86、arm和其他架構,最後一個應用於mips架構。一個'-'表示在對應架構中尚未定義該信號。

  • 不同的操作系統定義了不同的系統信號。因此有些信號出現在Unix系統內,也出現在Linux中,而有的信號出現在FreeBSD或Mac OS中,卻沒有出現在Linux中。這裡我們只研究Linux系統中的信號。
  • 默認動作:
    • Term:終止進程
    • Ign:忽略信號(默認即時對該種信號忽略操作)
    • Core:終止進程,生成Core文件。(查驗進程死亡原因,用於gdb調試)
    • Stop:停止(暫停)進程
    • Cont:繼續運行進程
  • 注意man 7 signal幫助文檔中可看到:The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.這裡特別強調了9) SIGKILL19) SIGSTOP信號,不允許忽略和捕捉,只能執行默認動作。甚至不能將其設置為阻塞。
  • 另外需清楚,只有每個信號所對應的事件發生了,該信號才會被遞達(但不一定遞達),不應亂發信號!!

Linux常規信號一覽表

  • 1)SIGHUP:當用戶退出Shell時,由該Shell啟動的所有進程將收到這個信號,默認動作為終止進程。
  • 2)SIGINT:當用戶按下了ctrl+c組合鍵時,用戶終端向正在運行中的由該終端啟動的程序發出此信號。默認動作為終止進程。
  • 3)SIGQUIT:當用戶按下ctrl+\組合鍵時產生該信號,用戶終端向正在運行中的由該終端啟動的程序發出此信號。默認動作為終止進程。
  • 4)SIGILL:CPU檢測到某進程執行了非法指令。默認動作為終止進程並產生core文件。
  • 5)SIGTRAP:該信號由斷點指令或其他trap指令產生。默認動作為終止進程並產生core文件。
  • 6)SIGABRT:調用abort函數時產生該信號。默認動作為終止進程並產生core文件。
  • 7)SIGBUS:非法訪問內存地址,包括內存對齊出錯,默認動作為終止進程並產生core文件。
  • 8)SIGFPE:在發生致命的運算錯誤時發出。不僅包括浮點運算錯誤,還包括溢出及除數為0等所有的算法錯誤。默認動作為終止進程並產生core文件。
  • 9)SIGKILL:無條件終止進程。本信號不能被忽略,處理和阻塞。默認動作為終止進程。它向系統管理員提供了可以殺死任何進程的方法。
  • 10)SIGUSR1:用戶定義的信號。即程序員可以在程序中定義並使用該信號。默認動作為終止進程。
  • 11)SIGSEGV:指示進程進行了無效內存訪問。默認動作為終止進程並產生core文件。
  • 12)SIGUSR2:另外一個用戶自定義信號,程序員可以在程序中定義並使用該信號。默認動作為終止進程。
  • 13)SIGPIPE:Broken pipe向一個沒有讀端的管道寫數據。默認動作為終止進程。
  • 14)SIGALRM:定時器超時,超時的時間由系統調用alarm設置。默認動作為終止進程。
  • 15)SIGTERM:程序結束信號,與SIGKILL不同的是,該信號可以被阻塞和終止。通常用來表示程序正常退出。執行Shell命令kill時,缺省產生這個信號。默認動作為終止進程。
  • 16)SIGSTKFLT:Linux早期版本出現的信號,現仍保留向後兼容。默認動作為終止進程。
  • 17)SIGCHLD:子進程結束時,父進程會收到這個信號。默認動作為忽略這個信號。
  • 18)SIGCONT:如果進程已停止,則使其繼續運行。默認動作為繼續或忽略。
  • 19)SIGSTOP:停止進程的執行。信號不能被忽略、處理和阻塞。默認動作為暫停進程。
  • 20)SIGTSTP:停止終端交互進程的運行。按下ctrl+z組合鍵時發出這個信號。默認動作為暫停進程。
  • 21)SIGTTIN:後台進程讀終端控制台。默認動作為暫停進程。
  • 22)SIGTTOU:該信號類似於SIGTTIN,在後台進程要向終端輸出數據時發生。默認動作為暫停進程。
  • 23)SIGURG :套接字上有緊急數據時,向當前正在運行的進程發出信號,報告有緊急數據到達。如網絡帶外數據到達,默認動作為忽略該信號。
  • 24)SIGXCPU:進程執行時間超過了分配給該進程的CPU時間,系統產生該信號並發送給該進程。默認動作為終止進程。
  • 25)SIGXFSZ:超過文件的最大長度設置。默認動作為終止進程。
  • 26)SIGVTALRM:虛擬時鐘超時時產生該信號。類似於SIGALRM,但是該信號只計算該進程占用CPU的使用時間。默認動作為終止進程。
  • 27)SIGPROF:類似於SIGVTALRM,它不包括該進程占用CPU時間還包括執行系統調用時間。默認動作為終止進程。
  • 28)SIGWINCH:窗口變化大小時發出。默認動作為忽略該信號。
  • 29)SIGIO:此信號向進程指示發出了一個異步IO事件。默認動作為忽略。
  • 30)SIGPWR:關機。默認動作為終止進程。
  • 31)SIGSYS:無效的系統調用。默認動作為終止進程並產生core文件。
  • 34)SIGRTMIN ~ 64)SIGRTMAX:Linux的實時信號,它們沒有固定的含義(可以由用戶自定義)。所有的實時信號的默認動作都為終止進程。

信號的產生

終端按鍵產生信號

    ctrl+c      2) SIGINT(終止/中斷)        "INT" -- Interrupt
    ctrl+z      20)SIGTSTP(暫停/停止)       "T" -- Terminal終端
    ctrl+\      3) SIGQUIT(退出)

硬件異常產生信號

    除0操作            8) SIGFPE(浮點數例外)        "F" -- float 浮點數
    非法訪問內存      11)SIGSEGV(段錯誤)
    總線錯誤            7) SIGBUS

kill函數/命令產生信號

  • kill命令產生信號:kill -SIGKILL pid
  • kill函數:給指定進程發送指定信號(不一定殺死)
    • int kill(pid_t pid, int sig);
      • 返回值
        • 成功:0
        • 失敗:-1(ID非法,信號非法,普通用戶殺init進程等權級問題),設置errno。
      • 參數
        • sig:不推薦直接使用數字,應使用宏名,因為不同操作系統信號編號可能不同,但名稱一致。
        • pid>0:發送信號給指定的進程。
        • pid=0:發送信號與調用kill函數進程屬於同一進程組的所有進程。
        • pid<0:取|pid|發給對應進程組。
        • pid=-1:發送給進程有權限發送的系統中所有進程。
  • 進程組:每個進程都屬於一個進程組,進程組是一個或多個進程集合,他們相互關聯,共同完成一個實體任務,每個進程組都有一個進程組長,默認進程組ID與進程組長ID相同。
  • 權限保護:super用戶(root)可以發送信號給任意用戶,普通用戶是不能向系統用戶發送信號的。kill -9(root用戶的pid)是不可以的。同樣,普通用戶也不能向其他普通用戶發送信號,終止其進程。只能自己創建的進程發送信號。普通用戶基本規則是:發送者實際或有效用戶ID==接收者實際或有效用戶ID。
  • 練習:循環創建5個子進程,任一子進程用kill函數終止其父進程。
  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <signal.h>
    
    #define N 5
    
    int main(int argc, char *argv[])
    {
        int i;
        pid_t pid;
    
        for(i = 0; i < N; i++)
        {
            pid = fork();
            if(pid == 0)
                break;
            if(i == 2)
                q = pid;
        }
        if(i < N)
        {
            while(1)
            {
                printf("I am child %d, getpid() = %u \n", i+1, getpid());
                sleep(1);
            }
        }
        else
        {
            sleep(1);
            kill(q, SIGKILL);
            while(1);
        }
        // int ret = kill(getpid(), SIGKILL);
        // if(ret == -1)
        //     exit(1);
    
        return 0;
    }
    

raise和abort函數

  • raise函數
    • 給當前進程發送指定信號(自己給自己發) raise(signo) == kill(getpid(), signo)
    • int raise(int sig);
      • 成功:0
      • 失敗:非0值
  • abort函數
    • 給自己發送異常終止信號 6)SIGABRT 信號,終止並產生core文件。
    • void abort(void); 該函數無返回

軟件條件產生信號

  • alarm函數

    • 設置定時器(鬧鐘)。在指定seconds後,內核會給當前進程發送 14)SIGALRM 信號。進程收到該信號,默認動作終止。
    • 每個進程都有且只有一個定時器。
    • unsigned int alarm(unsigned int seconds);
      • 返回0或剩余的秒數,無失敗。
    • 常用:取消定時器alarm(0),返回舊鬧鐘余下秒數。
    • 例:alarm(5) --> 3sec --> alarm(4) --> 5sec --> alarm(5) --> alarm(0)
    • 定時,與進程狀態無關(自然定時法)!就緒、運行、掛起(阻塞、暫停)、終止、僵屍……無論進程處於何種狀態,alarm都計時。
    • 練習:編寫程序,測試你使用的計算機1秒鐘能數多少個數。

      #include <stdio.h>
      #include <unistd.h>
      
      int main(void)
      {
          int i;
          alarm(1);
          for(i = 0; ; i++)
          {
              printf("%d\n", i);
          }
          return 0;
      }
      
    • 使用time命令查看程序執行的時間。程序運行的瓶頸在於IO,優化程序,首選優化IO。

      time ./alarm > out
      
    • 實際執行時間 = 系統時間 + 用戶時間 + 等待時間

  • setitimer函數

    • 設置定時器(鬧鐘)。可代替alarm函數。精度微秒us,可以實現周期定時。
    • int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
      • 返回值
        • 成功:0
        • 失敗:-1,設置errno
      • 參數which:指定定時方式
        • 1、自然定時:ITIMER_REAL --> 14)SIGLARM,計算自然時間。
        • 2、虛擬空間計時(用戶空間):ITIMER_VIRTUAL --> 26)SIGVTALRM,只計算進程占用CPU時間。
        • 3、運行時計時(用戶+內核):ITIMER_PROF --> 27)SIGPROF,計算占用CPU及執行系統調用的時間。
    • 練習:使用setitimer函數實現alarm函數,重復計算機1秒數數程序。

      #include <stdio.h>
      #include <stdlib.h>
      #include <sys/time.h>
      #include <unistd.h>
      
      // struct itimerval {
      //     struct timeval it_interval; /* Interval for periodic timer */
      //     struct timeval it_value;    /* Time until next expiration */
      // };
      
      // struct timeval {
      //     time_t      tv_sec;         /* seconds */
      //     suseconds_t tv_usec;        /* microseconds */
      // };
      
      unsigned int my_alarm(unsigned int sec)
      {
          struct itimerval it, oldit;
          int ret;
      
          it.it_value.tv_sec = sec;
          it.it_value.tv_usec = 0;
          it.it_interval.tv_sec = 0;
          it.it_interval.tv_usec = 0;
      
          ret = setitimer(ITIMER_REAL, &it, &oldit);
          if(ret == -1)
          {
              perror("setitimer");
              exit(1);
          }
          return oldit.it_value.tv_sec;
      }
      
      int main(void)
      {
          int i;
          my_alarm(1);
          for(i = 0; ; i++)
          {
              printf("%d\n", i);
          }
          return 0;
      }
      
    • 拓展練習,結合man page編寫程序,測試it_interval、it_value這兩個參數的作用。

      • 提示:
        • it_interval:用來設定兩次定時任務之間間隔的時間。
        • it_value:定時的時長。
        • 兩個參數都設置為0,即清0操作。
    • 示例

      #include <stdio.h>
      #include <sys/time.h>
      #include <signal.h>
      
      void myfunc(int signo)
      {
          printf("hello\n");
      }
      
      int main(void)
      {
          struct itimerval it, oldit;
          signal(SIGALRM, myfunc); //注冊SIGALRM信號的捕捉處理函數
      
          it.it_value.tv_sec = 5;
          it.it_value.tv_usec = 0;
      
          it.it_interval.tv_sec = 3;
          it.it_interval.tv_usec = 0;
      
          if(setitimer(ITIMER_REAL, &it, &oldit) == -1)
          {
              perror("setitimer error");
              return -1;
          }
      
          while(1);
          return 0;
      }
      

信號集的操作函數

  • 內核通過讀取未決信號集來判斷信號是否應被處理。信號屏蔽字mask可以影響未決信號集。而我們可以在應用程序中自定義set來改變mask。已達到屏蔽指定信號的目的。

信號集設定

    sigset_t set;  //typedef unsigned long sigset_t;
    int sigemptyset(sigset_t *set);                     將某個信號集清0                成功:0;失敗:-1
    int sigfillset(sigset_t *set);                      將某個信號集置1                成功:0;失敗:-1
    int sigaddset(sigset_t *set, int signum);           將某個信號加入信號集          成功:0;失敗:-1
    int sigdelset(sigset_t *set, int signum);           交某個信號清出信號集          成功:0;失敗:-1
    int sigismember(const sigset_t *set, int signum);   判斷某個信號是否在信號集中       不在:0;在:1;出錯:-1

    sigset_t類型的本質是位圖。但不應該直接使用位操作,而應用使用上述函數,保證跨系統操作有效。
    對比認知select函數。

sigprocmask函數

  • 用來屏蔽信號、解除屏蔽也使用該函數。其本質,讀取與修改進程的信號屏蔽字(PCB中)。
  • 嚴格注意,屏蔽信號:只是將信號處理延後執行(延至解除屏蔽);而忽略表示將信號丟棄處理。
  • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    • 返回值
      • 成功:0
      • 失敗:-1,設置errno
    • 參數
      • set:傳入參數,是一個位圖,set中哪位置1,就表示當前進程屏蔽哪個信號。
      • oldset:傳出參數,保存舊的信號屏蔽集。
      • how參數取值:假設當前的信號屏蔽字為mask
        • SIG_BLOCK:當how設置為此值,set表示需要屏蔽的信號。相當於mask = mask | set
        • SIG_UNBLOCK:當how設置為此值,set表示需要解除屏蔽的信號。相當於mask = mask & ~set
        • SIG_SETMASK:當how設置為此值,set表示用於替代原始屏蔽集的新屏蔽集。相當於mask = set,若調用sigprocmask解除了對當前若干信號的阻塞,則在sigprocmask返回前,至少將其中一個信號遞達。

sigpending函數

  • 讀取當前進程的未決信號集。
  • int sigpending(sigset_t *set);
    • 參數set為傳出參數。
    • 返回值
      • 成功:0
      • 失敗:-1,設置errno
  • 練習:編寫程序。把所有常規信號的未決狀態打印至屏幕。

    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>
    
    void printped(sigset_t *ped)
    {
        int i;
        for(i = 1; i < 32; i++) 
        {
            if(sigismember(ped, i) == 1)
            {
                putchar('1');
            }
            else
            {
                putchar('0');
            }
        }
        printf("\n");
    }
    
    int main(void)
    {
        sigset_t myset, oldset, ped;
    
        sigemptyset($myset);
    
        sigaddset(&myset, SIGQUIT);
        sigaddset(&myset, SIGINT);
        sigaddset(&myset, SIGTSTP);
        sigaddset(&myset, SIGSEGV);
        sigaddset(&myset, SIGKILL); //9,19不能屏蔽,加入也沒用
    
        sigprocmask(SIG_BLOCK, &myset, &oldset);
    
        while(1)
        {
            sigpending(&ped);
            printped(&ped);
            sleep(1);
        }
    
        return 0;
    }
    

信號的捕捉

signal函數

  • 注冊一個信號捕捉函數:
  • typedef void (*sighandler_t)(int);
  • sighandler_t signal(int signum, sighandler_t handler);
  • 該函數由ANSI定義,由於歷史原因在不同版本的Unix和不同版本的Linux中可能有不同的行為。因此應該盡量避免使用它,取而代之使用sigaction函數。
  • void (*signal(int signum, void (*sighandler_t)(int)))(int);
  • 能看出這個函數代表什麼意思嗎?注意多在復雜結構中使用typedef。
  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <unistd.h>
    
    typedef void (*sighandler_t)(int);
    
    void catchsigiint(int signo)
    {
        printf("-----SIGINIT-----\N");    
    }
    
    int main(void)
    {
        sighandler_t handler;
    
        handler = signal(SIGINT, catchsigiint);
        if(handler == SIG_ERR)
        {
            perror("signal error");
            exit(1);
        }
    
        while(1);
        return 0;
    }
    

sigaction函數

  • 修改信號處理動作(通常在Linux用其來注冊一個信號的捕捉函數)
  • int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    • 返回值
      • 成功:0
      • 失敗:-1,設置errno
    • 參數:
      • act:傳入參數,新的處理方式。
      • oldact:傳出參數,舊的處理方式。
  • struct sigaction

    struct sigaction {
       void     (*sa_handler)(int);
       void     (*sa_sigaction)(int, siginfo_t *, void *);
       sigset_t   sa_mask;
       int        sa_flags;
       void     (*sa_restorer)(void);
    };
    
    sa_restorer:該元素是過時的,不應該使用,POSIX.1標准將不指定該元素。(棄用)
    sa_sigaction:當sa_flags被指定為SA_SIGINFO標志時,使用該信號處理程序。(很少使用)
    
    重點掌握:
    1、sa_handler:指定信號捕捉後和處理函數名(即注冊函數)。也可賦值為SIG_IGN表忽略或SIG_DFL表執行默認動作。
    2、sa_mask:調用信號處理函數時,所要屏蔽的信號集(信號屏蔽字)。注意:僅在修理函數被調用期間屏蔽生效,是臨時性設置。
    3、sa_flags:通常設置為0,表使用默認屬性。
    
  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <siganl.h>
    #include <unistd.h>
    
    void docatch(int signo)
    {
        printf("%d signal is catched\n", signo);
    }
    
    int main(void)
    {
        int ret;
        struct sigaction act;
    
        act.sa_handler = docatch;
        sigemptyset(&act.sa_mask);
        sigaddset(&act.sa_mask, SIGQUIT);
        act.sa_flags = 0; //默認屬性。信號捕捉函數執行期間,自動屏蔽本信號
    
        ret = sigaction(SIGINT, &act, NULL);
        if(ret < 0)
        {
            perror("sigaction error");
            exit(1);
        }
    
        while(1);
        return 0;
    }
    
  • 信號捕捉特性

    • 進程正常運行時,默認PCB中有一個信號屏蔽字,假定為☆,它決定了進程自動屏蔽哪些信號。當注冊了某個信號捕捉函數,捕捉到該信號以後,要調用函數。而該函數有可能執行很長時間,在這期間所屏蔽的信號不由☆來指定。而是用sa_mask來指定。調用完信號處理函數,再恢復為☆。
    • xxx信號捕捉函數執行期間,xxx信號自動被屏蔽。
    • 阻塞的常規信號不支持排隊,產生多次只記錄一次。(後32個實時信號支持排隊)
    • 練習1:為某個信號設置捕捉函數
    • 練習2:驗證在信號處理函數執行期間,該信號多次遞達,那麼只在處理函數之行結束後,處理一次。
    • 練習3:驗證sa_mask在捕捉函數執行期間的屏蔽作用。

內核實現信號捕捉過程

內核實現信號捕捉過程

競態條件(時序競態)

pause函數

  • 調用該函數可以造成進程主動掛起,等待信號喚醒。調用該系統調用的進程將處於阻塞狀態(主動放棄CPU)直到有信號遞達將其喚醒。
  • int pause(void);,返回值:-1,並設置errno為EINTR。
  • 返回值
    • ① 如果信號的默認處理是終止進程,則進程終止,pause函數沒有機會返回。
    • ② 如果信號的默認處理動作是忽略,進程繼續處於掛起狀態,pause函數不返回。
    • ③ 如果信號的處理動作是捕捉,則【調用完信號處理函數之後,pause返回-1】,errno設置為EINTR,表示“被信號中斷”。想想我們還有哪個函數只有出錯返回值。
    • ④ pause收到的信號不能被屏蔽,如果被屏蔽,那麼pause就不能被喚醒。
  • 練習,使用pause和alarm來實現sleep函數。

    • 注意,unslept = alarm(0)的用法。
    • 例如:睡覺,alarm(10)鬧鈴。
    • 正常:10後鬧鈴將我喚醒,這時額外設置alarm(0)取消鬧鈴,不會出錯。
    • 異常:5分鐘,被其他事物喚醒,alarm(0)取消鬧鈴防止打擾。
    • 實現示例:

      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <signal.h>
      #include <errno.h>
      
      void catch_signalrm(int signo)
      {
          printf("%d signal is catched.", signo);
      }
      
      unsigned int mysleep(unsigned int seconds)
      {
          int ret;f
          struct sigaction act, oldact;
          act.sa_handler = catch_signalrm;
          sigemptyset(&act.sa_mask);
          act.sa_flags = 0;
      
          ret = sigaction(SIGALRLM, &act, &oldact);
          if(ret == -1)
          {
              perror("sigaction error");
              exit(1);
          }
      
          alarm(seconds);
          ret = pause(); // 主動掛起,等信號
          if(ret == -1 && errno == EINTR)
          {
              printf("pause sucess\n");
          }
      
          ret = alarm(0); //鬧鈴清零
          sigaction(SIGALRM, &oldact, NULL); //恢復SIGALRM信號舊有的處理方式。
          return ret;
      }
      
      int main(void)
      {
          while(1)
          {
              mysleep(3);
              printf("-------------------\n");
          }
          return 0;
      }
      

時序競態

  • 前導例

    • 設想如下場景:
      • 欲睡覺,定鬧鐘10分鐘,希望10分鐘後鬧鈴將自己喚醒。
      • 正常:定時,睡覺,10分鐘後被鬧鐘喚醒。
      • 異常:鬧鐘定好後,被喚走,外出勞動,20分鐘後勞動結束。回來繼續睡覺計劃,但勞動期間鬧鐘已經響過,不會再將我喚醒。
  • 時序問題分析

    • 回顧,借助pause和alarm實現的mysleep函數。設想如下時序:
    • 1、注冊SIGALRM信號處理函數(sigaction...)
    • 2、調用alarm(1)函數設定鬧鐘1秒。
    • 3、函數調用剛結束,開始倒計時1秒。當前進程失去CPU,內核調度優先級高的進程(有多個)取代當前進程。當前進程無法獲得CPU,進入就緒等待CPU。
    • 4、1秒後,鬧鐘超時,內核向當前進程發送SIGALRM信號(自然定時法,與進程狀態無關),高優先級進程尚未執行完,當前進程仍處於就緒態,信號無法處理(未決)。
    • 5、優先級高的進程執行完,當前進程獲得CPU資源,內核調度回當前進程執行。SIGALRM信號遞達,信號設置捕捉,執行處理函數sig_alarm。
    • 6、信號處理函數執行結束,返回當前進程主控流程,pause()被調用掛起等待。(欲等待alarm函數發送的SIGALRM信號將自己喚醒)。
    • 7、SIGALRM信號已經處理完畢,pause不會等到。
  • 解決時序問題

    • 可以通過設置屏蔽SIGALRM的方法來控制程序執行邏輯,但無論如何設置,程序都有可能在“解除信號屏蔽”與”掛起等待信號“這兩個操作間隙失去CPU資源。除非將這兩步驟合並成一個”原子操作“。sigsuspend函數具備這個功能。在對時序要求嚴格的場合下都應該使用sigsuspend替換pause。
    • int sigsuspend(const sigset_t *mask);,掛起等待信號。
    • sigsuspend函數調用期間,進程信號屏蔽字由其參數mask指定。
    • 可將某個信號(如SIGALRM)從臨時信號屏蔽字mask中刪除,這樣在調用sigsuspend時將解除對該信號的屏蔽,然後掛起等待,當sigsuspend返回時,進程的信號屏蔽字恢復為原來的值。如果原來對該信號是屏蔽態,sigsuspend函數返回後仍然屏蔽該信號。
    • 改進版mysleep

      #include <stdio.h>
      #include <stdlib.h>
      #include <unistd.h>
      #include <signal.h>
      #include <errno.h>
      
      void sig_alrm(int signo)
      {
          printf("%d signal is catched.", signo);
      }
      
      unsigned int mysleep(unsigned int seconds)
      {
          int ret;f
          struct sigaction newact, oldact;
          sigset_t newmask, oldmask, suspmask;
      
          //1、為SIGALRM設置捕捉函數,一個空函數
          newact.sa_handler = sig_alrm;
          sigemptyset(&newact.sa_mask);
          newact.sa_flags = 0;
          sigaction(SIGALRM, &newact, &oldact);
      
          //2、設置阻塞信號集,阻塞SIGALRM信號
          sigemptyset(&newmask);
          sigaddset(&newmask, SIGALRM);
          sigprocmask(SIG_BLOCK, &newmask, &oldmask);
      
          //3、定時n秒,到時後可以產生SIGALRM信號
          alarm(seconds);
      
          //4、構造一個調用sigsuspend臨時有效的阻塞信號集,在臨時阻塞信號集裡解除SIGALRM的阻塞
          suspmask = oldmask;
          sigdelset(&suspmask, SIGALRM);
      
          //5、sigsuspend調用期間,采用臨時阻塞信號集suspmask替換原有阻塞信號集
          //這個信號集中不包含SIGALR信號,同時掛起等待
          //當sigsuspend被信號喚醒返回時,恢復原有的阻塞信號集
          sigsuspend(&suspmasks);
      
          unslept = alarm(0);
          //6、恢復SIGALRM原有的處理動作
          sigaction(SIGALRM, &oldact, NULL);
          //7、解除對SIGALRM的阻塞
          sigprocmask(SIG_SETMASK, &oldmask, NULL);
          return (unslept);
      }
      
      int main(void)
      {
          while(1)
          {
              mysleep(3);
              printf("-------------------\n");
          }
          return 0;
      }
      
  • 總結

    • 競態條件,跟系統負載有很緊密的關系,體現出信號的不可靠性。系統負載越嚴重,信號不可靠性越強。
    • 不可靠由其實現原理所致。信號是通過軟件方式實現(跟內核調度高度依賴,延時性強),每次系統調用結束後,或中斷處理結束後,需通過掃描PCB中的未決信號集,來判斷是否應處理某個信號。當系統負載過重時,會出現時序混亂。
    • 這種意外情況只能在編寫程序過程中,提早預見,主動規避,而無法通過gdb程序調試等其他手段彌補。且由於該錯誤不具規律性,後期捕捉和重現十分困難。

全局變量異步I/O

  • 分析如下父子進程交替的數數程序。當捕捉函數裡面的sleep取消,程序即會出現問題。請分析原因。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
    
    int n = 0; flag = 0;
    
    void sys_err(char *str)
    {
        perror(str);
        exit(1);
    }
    
    void do_sig_child(int num)
    {
        printf("I am child %d\t%d\n", getpid(), n);
        n+=2;
        flag = 1;
        //sleep(1);
    }
    
    void do_sig_parent(int num) 
    {
        printf("I am parent %d\t%d\n", getpid(), n);
        n+=2;
        flag = 1;//數數完成
        //sleep(1);
    }
    
    int main(void)
    {
        pid_t pid;
        struct sigaction act;
    
        if((pid = fork()) < 0)
        {
            sys_err("fork");
        }
        else if(pid > 0 )
        {
            n = 1;
            sleep(1);
            act.sa_handler = do_sig_parent;
            sigemptyset(&act.sa_mask);
            act.sa_flags = 0;
            sigaction(SIGUSR2, &act, NULL); //注冊自己的信號捕捉函數
    
            do_sig_parent(0);
    
            while(1)
            {
                // wait for signal
                if(flag == 1)//父進程數數完成
                {
                    kill(pid, SIGUSR1);
                    flag = 0;//標志已經給子進程發送完信號
                }
            }
        }
        else if(pid == 0)
        {
            n = 2;
            act.sa_handler = do_sig_child;
            sigemptyset(&act.sa_mask);
            act.sa_flags = 0;
            sigaction(SIGUSR1, &act, NULL);//父進程數數完成發送SIGUSR1給子進程。
    
            while(1)
            {
                // wait for signal
                if(flag == 1)
                {
                    kill(getppid(), SIGUSR2);
                    flag = 0;
                }
            }
        }
    }
    
  • 示例中,通過flag變量標記程序實行進度。flag置1表示數數完成,flag置0表示給對方發送信號完成。

  • 問題出現的位置,在父子進程kill函數之後需要緊接著調用flag,將其置0,標記信號已經發送。但在這期間很有可能被kernel調度,失去執行權利,而對方獲取了執行時間,通過發送信號回調捕捉函數,從而修改了全局的flag。
  • 如何解決該問題呢?可以使用後續課程講到的“鎖”機制。當操作全局變量的時候,通過加鎖、解鎖來解決該問題。
  • 現階段,我們在編程期間如若使用全局變量,應在主觀上注意全局變量的異步IO可能造成的問題。

可/不可重入函數

  • 一個函數在調用執行期間(尚未調用結束),由於某種時序又被重復調用,稱之為“重入”。根據函數實現的方法可分為“可重入函數”和“不可重入函數”兩種。看如下時序。 不可重入函數
  • 顯示,insert函數是不可重入函數,重入調用,會導致意外結果呈現。究其原因,是該函數內部實現使用了全局變量。

  • 注意事項

    • 1、定義可重入函數,函數內不能含有全局變量及static變量,不能使用malloc、free。
    • 2、信號捕捉函數應設計為可重入函數。
    • 3、信號處理程序可以調用的可重入函數可參閱man 7 signal。
    • 4、沒有包含在上述列表中的函數大多是不可重入的,其原因為:
      • 使用靜態數據結構
      • 調用了malloc和free
      • 是標准I/O函數

SIGCHLD信號

SIGCHLD的產生條件

  • 子進程終止時
  • 子進程接收到SIGSTOP信號停止時
  • 子進程處在停止態,接受到SIGCONT後喚醒時

借助SIGCHLD信號回收子進程

  • 子進程結束運行,其父進程會收到SIGCHLD信號。該信號的默認處理動作是忽略。可以捕捉該信號,在捕捉函數中完成子進程狀態的回收。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <errno.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <signal.h>
    
    void sys_err(str)
    {
        perror(str);
        exit(1);
    }
    
    void do_sig_child(int signo)
    {
        int status;
        pid_t pid;
    
        while((pid = waitpid(0, &status, WNOHANG)) > 0){
            if(WIFEXITED(status))
                printf("-----------child %d exit %d \n", pid, WEXITSTATUS(status));
            else if(WIFSIGNALED(status))
                printf("child %d cancel signal %d \n", pid, WTERMSIG(status));
        }
    }
    
    int main(void)
    {
        pid_t pid;
        int i;
        //阻塞SIGCHLD
        for(i = 0; i < 10; i++){
            if((pid = fork()) == 0)
                break;
            else if(pid < 0)
                sys_err("fork");
        }
    
        if(pid == 0){   //10個子進程
            int n = 1;
            while(n--){
                printf("child ID %d \n", getpid());
                sleep(1);
            }
            return i+1;
        }else if(pid > 0){
            //SIGCHLD阻塞
            struct sigaction act;
    
            act.sa_handler = do_sig_child;
            sigemptyset(&act.sa_mask);
            act.sa_flags = 0;
            sigaction(SIGCHLD, &act, NULL);
            //解除對SIGCHLD的阻塞
    
            while(1){
                printf("Parent ID %d \n", getpid());
                sleep(1);
            }
        }
    
        return 0;
    }
    
  • 分析該例子。結合 17)SIGCHLD信號默認動作,掌握不使用捕捉函數回收子進程的方式。

  • 如果每創建一個子進程後不使用sleep可以嗎?可不可以將程序中捕捉函數內部的while替換為if?為什麼?
  • 思考:信號不支持排隊,當正在執行SIGCHLD捕捉函數時,再過來一個或多個SIGCHLD信號怎麼辦?

子進程結束status處理方式

  • pid_t waitpid(pid_t pid, int *status, int options)
    • options
      • WNOHANG
        • 沒有子進程結束,立即返回。
      • WUNTRACED
        • 子進程已經停止,立即返回(但不是通過調用ptrace函數調用)。即使沒有指定此選項,也將提供已停止的子進程的狀態。
      • WCONTINUED
        • 一個已停止的子進程被SIGCONT信號遞達恢復,立即返回。

SIGCHLD信號注意問題

信號傳參

發送信號傳參

  • sigqueue函數對應kill函數,但可在向指定進程發送信號的同時攜帶參數

    int sigqueue(pid_t pid, int sig, const union sigval value);
            成功:0
            失敗:-1,設置errno
    
    union sigval{
        int sival_int;
        void *sival_ptr;
    }
    
  • 向指定進程發送指定信號的同時,攜帶數據。但,如傳地址,需注意,不同進程之間虛擬地址空間各自獨立,將當前進程地址傳遞給另一進程沒有實際意義。

捕捉信號傳參

    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    struct sigaction {
       void     (*sa_handler)(int);
       void     (*sa_sigaction)(int, siginfo_t *, void *);
       sigset_t   sa_mask;
       int        sa_flags;
       void     (*sa_restorer)(void);
    };
  • 當注冊信號捕捉函數,希望獲取更多信號相關信息,不應使用sa_handler而應該使用sa_sigaction。但此時的sa_flags必須指定為SA_SIGINFO。siginfo_t是一個成員十分豐富的結構體類型,可以攜帶各種與信號相關的數據。

中斷系統調用

  • 系統調用可分為兩類:慢速系統調用和其他系統調用。
    • 1、慢速系統調用:可能會使進程永遠阻塞的一類。如果在阻塞期間收到一個信號,該系統調用就被中斷,不再繼續執行(早期);也可以設定系統調用是否重啟。如:read、write、pause、wait...
    • 2、其他系統調用:getpid、getppid、fork...
  • 結合pause,回顧慢速系統調用:
    • 慢速系統調用被中斷的相關行為,實際上就是pause的行為:如:read
      • 1、想中斷pause,信號不能被屏蔽。
      • 2、信號的處理方式必須是捕捉(默認、忽略都不可以)。
      • 3、中斷後返回-1,設置errno為EINTR(表示“被信號中斷”)。
  • 可修改sa_flags參數來設置被信號中斷後系統調用是否重啟。SA_INTERRURT不重啟。SA_RESTART重啟。
  • 擴展了解:sa_flagsf還有很多可選參數,適用於不同情況。如:捕捉到信號後,在執行捕捉函數期間,不希望自動阻塞該信號,可將sa_flags設置為SA_NODEFER,除非sa_mask中包含該信號。

終端

  • 在Unix系統中,用戶通過終端登錄系統後得到一個Shell進程,這個終端成為Shell進程的控制終端(Controlling Terminal),進程中,控制終端是保存在PCB中的信息,而fork會復制PCB中的信息,因此由Shell進程啟動的其它進程的控制終端也是這個終端。默認情況下(沒有重定向),每個進程的標准輸入、標准輸出和標准錯誤輸出都指向控制終端,進程從標准輸入讀也就是讀用戶的鍵盤輸入,進程往標准輸出或標准錯誤輸出寫也就是輸出到顯示器上。信號中還進過,在控制終端輸入一些特殊的控制鍵可以給前台進程發信號,例如ctrl+c表示SIGINT,ctrl+\表示SIGQUIT。
  • Alt+Ctrl+[F1-F6],字符終端,pts(pseudo terminal slave)指偽終端。
  • Alt+F7,圖形終端。
  • SSH、Telnet...,網絡終端。

終端的啟動流程

  • 文件與I/O中講過,每個進程都可以通過一個特殊的設備文件/dev/tty訪問它的控制終端。事實上每個終端設備都對應一個不同的設備文件,/dev/tty提供了一個通用的接口,一個進程要訪問它的控制終端既可以通過/dev/tty,也可以通過該終端設備所對應的設備文件來訪問。ttyname函數可以由文件夾描述符查出對應的文件名,該文件描述符必須指向一個終端設備而不能是任意文件。
  • 簡單來說,一個Linux系統啟動,大致經歷如下步驟:

    init --> fork --> exec --> getty --> 用戶輸入帳號 --> login --> 輸入密碼 --> exec --> bash
    
  • 硬件驅動程序負責讀寫實際的硬件設備,比如從鍵盤讀入字符和把字符輸出到顯示器,線路規程像一個過濾器,對於某些特殊字符並不是讓它直接通過,而是做特殊處理,比如在鍵盤上按下ctrl+z,對應的字符並不會被用戶程序的read讀到,而是被線程規程截獲,解釋成SIGTSTP信號發給前台進程,通常會使該進程停止。線路規程應該過濾哪些字符和做哪些特殊處理是可以配置的。

終端設備模塊

  • line disciline:線路規程,用來過濾鍵盤輸入的內容。

ttyname函數

  • 由文件描述符查出對應的文件名
  • char *ttyname(int fd);
    • 成功:終端名
    • 失敗:NULL,設置errno
  • 下面我們借助ttyname函數,通過實驗看一下各種不同的終端所對應的設備文件名。

    #include <unistd.h>
    #include <stdio.h>
    
    int main(void)
    {
        printf("fd 0: %s\n", ttyname(0));
        printf("fd 1: %s\n", ttyname(1));
        printf("fd 2: %s\n", ttyname(2));
        return 0;
    }
    

網絡終端

  • 虛擬終端或串口終端的數目是有限的,虛擬終端(字符控制終端)一般就是/dev/tty1~/dev/tty6六個,串口終端的數目也不超過串口的數目。然而網絡終端或圖形窗口的數目卻是不受限制的,這是通過偽終端(Pseudo TTY)實現的。一套偽終端由一個主設備(PTY Master)和一個從設備(PTY Slave)組成。主設備在概念上相當於鍵盤和顯示器,只不過它不是真正的硬件而是一個內核模塊,操作它的也不是用戶而是另一個進程。從設備和上面介紹的/dev/tty1這樣的終端設備模塊類似,只不過它的底層驅動程序不是訪問硬件而是訪問主設備。網絡終端或圖形終端的Shell進程以及它啟動的其它進程,都會認為自己的控制終端是偽終端從設備,例如/dev/pts/0、/dev/pts/1等。下面以telnet為例說明網絡登錄和使用偽終端的過程。

網絡終端

  • TCP/IP協議棧:在數據包上添加報頭。
  • 如果telnet客戶端和服務器之間的網絡延遲較大,我們會觀察到按下一個鍵之後要過幾分鐘才能回顯到屏幕上。這說明我們每按一個鍵telnet客戶端都會立刻把該字符發送給服務器,然後這個字符經過偽終端主設備和從設備之後被Shell進程讀取,同時回顯到偽終端從設備,回顯的字符再經過偽終端主設備、telnet服務器和網絡發回給telnet客戶端,顯示給用戶看。也許你會覺得吃驚,但真的是這樣:每按一個鍵都要在網絡上走個來回。

進程組

概念和特性

  • 進程組,也稱之為作業。BSD於1980年前後向Unix中增加的一個新特性。代表一個或多個進程的集合。每個進程都屬於一個進程組。在waitpid函數和kill函數的參數中都曾使用到。操作系統設計的進程組的概念,是為了簡化對多個進程的管理。
  • 當父進程,創建子進程的時候,默認子進程與父進程屬於同一進程組。進程組ID==第一個進程ID(組長進程)。所以,組長進程標識:其進程組ID==其進程ID。
  • 可以使用kill -SIGKILL -進程組ID(負的)來將整個進程組內的進程全部殺死。
  • 組長進程可以創建一個進程組,創建該進程組中的進程,然後終止。只要進程組中有一個進程存在,進程組就存在,與組長進程是否終止無關。
  • 進程組生存期:進程組創建到最後一個進程離開(終止或轉移到另一個進程組)。
  • 一個進程可以為自己或子進程設置進程組ID。

進程組操作函數

  • getpgrp函數

    • 獲取當前進程的進程組ID
    • pid_t getpgrp(void);
      • 總是返回調用者的進程組ID。
  • getpgid函數

    • 獲取指定進程的進程組ID
    • pid_t getpgid(pid_t pid);
      • 成功:0
      • 失敗:-1,設置errno
    • 如果pid = 0,那麼該函數作用和getpgrp一樣。
    • 練習:查看進程對應的進程組ID
  • setpgid函數

    • 改變進程默認所屬的進程組。通常可用來加入一個現有的進程組或創建一個新進程組。
    • int setpgid(pid_t pid, pid_t pgid);
      • 成功:0
      • 失敗:-1,設置errno
    • 將參1對應的進程,加入參2對應的進程組中。
    • 注意
      • 1、如改變進程為新的組,應fork後,exec前。
      • 2、權級問題。非root進程只能改變自己創建的子進程,或有權限操作的進程。
    • 練習:修改子進程的進程組ID
  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(void)
    {
        pid_t pid;
        if((pid = fork()) < 0){ 
            perror("fork");
            exit(1);
        }else if(pid == 0){ 
            printf("child PID == %d\n", getpid());
            printf("child Group ID == %d\n", getpgid(0)); //返回組ID
            //printf("child Group ID == %d\n", getpgrp()); //返回組ID
            sleep(7);
            printf("----Group ID of child is changed to %d\n", getpgid(0));
            exit(0);
        }else if(pid > 0){ 
            sleep(1);
            setpgid(pid, pid);//讓子進程自立門戶,成為進程組組長,以它的pid為進程組id
    
            sleep(13);
            printf("\n");
            printf("parent PID == %d\n", getpid());
            printf("parent's parent process PID == %d\n", getppid());
            printf("parent Group ID == %d\n", getpgid(0));
    
            sleep(5);
            setpgid(getpid(), getppid());//改變父進程的組ID為父進程的父進程
            printf("\n -----Group ID of parent is changed to %d \n", getpgid(0));
    
            while(1);
        }   
        return 0;
    }
    

會話

創建會話

  • 創建一個會話需要注意以下6點注意事項:
    • 1、調用進程不能是進程組組長,該進程變成新會話首進程(session header)。
    • 2、該進程成為一個新進程組的組長進程。
    • 3、需有root權限(ubuntu不需要)。
    • 4、新會話丟棄原有的控制終端,該會話沒有控制終端。
    • 5、該調用進程是組長進程,則出錯返回。
    • 6、建立新會話時,先調用fork,父進程終止,子進程調用setsid

getsid函數

  • 獲取進程所屬的會話ID。
  • pid_t getsid(pid_t pid);
    • 成功:返回調用進程的會話ID
    • 失敗:-1,設置errno
    • pid為0表示察看當前進程session ID。
  • ps ajx命令查看系統中的進程。參數a表示不僅當前用戶的進程,也列出所有其他用戶的進程,參數x表示不僅列出有控制終端的進程,也列出所有無控制終端的進程,參數j表示列出與作業控制相關的信息。
  • 組長進程不能成為新會話首進程,新會話首進程必定會成為組長進程。

setsid函數

  • 創建一個會話,並以自己的ID設置進程組ID,同時也是新會話的ID。
  • pid_t setsid(void);
    • 成功:返回調用進程的會話ID
    • 失敗:-1,設置errno
  • 練習:fork一個子進程,並使其創建一個新會話。查看進程組ID、會話ID前後變化。

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    int main(void)
    {
        pid_t pid;
    
        if((pid = fork()) < 0){ 
            perror("fork error");
            exit(1);
        } else if(pid == 0){ 
            printf("child process PID is %d\n", getpid());
            printf("Group ID of child is %d\n", getpgid(0));
            printf("Session ID of child is %d\n", getsid(0));
    
            sleep(0);
            setsid();//子進程非組長進程,故其成為新會話首進程,且成為組長進程。該進程組ID即為會話進程
    
            printf("Changed:\n");
    
            printf("child process PID is %d\n", getpid());
            printf("Group ID of child is %d\n", getpgid(0));
            printf("Session ID of child is %d\n", getsid(0));
    
            sleep(20);
    
            exit(0);
        }   
    
        return 0;
    }
    

守護進程

  • Daemon(精靈)進程,是Linux中的後台服務進程,通常獨立於控制終端並且周期性地執行某種任務或等待處理某些發生的事件。一般采用以d結尾的名字。
  • Linux後台的一些系統服務進程,沒有控制終端,不能直接和用戶交互。不受用戶登錄、注銷的影響,一直在運行著,他們都是守護進程。如:預讀入緩輸出機制的實現;ftp服務器;nfs服務器等。
  • 創建守護進程,最關鍵的一步是調用setsid函數創建一個新的Session,並成為Session Leader。

創建守護進程模型

  • 1、創建子進程,父進程退出。所有工作在子進程中進行,形式上脫離了控制終端。
  • 2、在子進程中創建新會話。setsid函數,使子進程完全獨立出來,脫離控制。
  • 3、改變當前目錄為根目錄。chdir函數,防止占用可卸載的文件系統,也可以換成其他路徑。
  • 4、重設文件權限掩碼。umask函數,防止繼承的文件創建屏蔽字拒絕某些權限,增加守護進程靈活性。
  • 5、關閉文件描述符。繼承的打開文件不會用到,浪費系統資源,無法卸載。
  • 6、開始執行守護進程核心工作。
  • 7、守護進程退出處理程序模型。
  • 示例

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    
    int main(void)
    {
        pid_t pid, sid;
        int ret;
    
        pid = fork();
        if(pid > 0)
        {   
            exit(1);
        }   
    
        sid = setsid();
    
        ret = chdir("/home/super/");
        if(ret == -1) 
        {   
            perror("chdir error");
            exit(1);
        }   
    
        umask(0022);
    
        close(STDIN_FILENO);
        open("/dev/null", O_RDWR);
        dup2(0, STDOUT_FILENO);
        dup2(0, STDERR_FILENO);
    
        while(1);
    
        return 0;
    }

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved