《Linux系統程式設計訓練營》9_Linux 程式層次分析

發表於2023-09-27

詳解 Linux 程式組

  • 每個程式都有一個程式組號 (PGID)

    • 程式組:一個或多個程式的集合(集合中的程式並不孤立)
    • 程式組中的程式通常存在父子關係,兄弟關係,或 功能相近

  • 程式組可方便程式管理(如:同時殺死多個程式,傳送一個訊號給多個程式)

    • 每個程式必定屬於一個程式組,也只能屬於一個程式組
    • 程式除了有 PID 外,還有 PGID (唯一,可變,即某一個程式可以切換程式組)
    • 每個程式組有一個程式組長,程式組長的 PID 和 PGID 相同
> ps -o pgid 19843
PGID 977

> kill -- -977

  • pid_t getpgrp(void); // 獲取當前程式的組標識
  • pid_t getpgid(pid_t pid); // 獲取指定程式的組標識
  • int setpgid(pid_t pid, pid_t pgid); // 設定程式的組標識

    • pid == pgid, 將 pid 指定的程式設為組長
    • pid == 0, 設定當前程式的組標識為 pgid
    • pid == 0,將 pid 設定為組標識 (即將 pid 所代表的程式設定為程式組長)

程式組示例程式

預設情況下,子程式與父程式屬於同一程式組, 是 fork 工作機制的產物(子程式複製當前程式本身)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int pid = 0;
    int i = 0;

    printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());

    while (i < 5) {
        if ((pid = fork()) > 0) {
            printf("new : %d\n", pid);
        }
        else if (pid == 0) {
            sleep(1);
            printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());
            sleep(60);
            printf("last == pgid = %d\n", getpgrp());
            break;
        }
        else {
            printf("fork error...\n");
        }
        ++i;
    }

    if (pid) {
        sleep(60);
    }

    return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out &
[1] 3022
parent = 3022, ppid = 2125, pgid = 3022
new : 3024
new : 3025
new : 3026
new : 3027
new : 3028
tiansong@tiansong:~/Desktop/linux$ child = 3025, ppid = 3022, pgid = 3022
child = 3026, ppid = 3022, pgid = 3022
child = 3027, ppid = 3022, pgid = 3022
child = 3024, ppid = 3022, pgid = 3022
child = 3028, ppid = 3022, pgid = 3022

tiansong@tiansong:~/Desktop/linux$ ps
    PID TTY          TIME CMD
   2125 pts/3    00:00:00 bash
   3022 pts/3    00:00:00 a.out
   3024 pts/3    00:00:00 a.out
   3025 pts/3    00:00:00 a.out
   3026 pts/3    00:00:00 a.out
   3027 pts/3    00:00:00 a.out
   3028 pts/3    00:00:00 a.out
   3061 pts/3    00:00:00 ps
tiansong@tiansong:~/Desktop/linux$ kill 3022  // kill 程式組長
[1]+  Terminated              ./a.out
tiansong@tiansong:~/Desktop/linux$ ps
    PID TTY          TIME CMD
   2125 pts/3    00:00:00 bash
   3024 pts/3    00:00:00 a.out
   3025 pts/3    00:00:00 a.out
   3026 pts/3    00:00:00 a.out
   3027 pts/3    00:00:00 a.out
   3028 pts/3    00:00:00 a.out
   3149 pts/3    00:00:00 ps
tiansong@tiansong:~/Desktop/linux$ kill -- -3022  // kill 程式組
tiansong@tiansong:~/Desktop/linux$ ps
    PID TTY          TIME CMD
   2125 pts/3    00:00:00 bash
   3163 pts/3    00:00:00 ps

程式組深度剖析

深入理解程式組

  • 程式組長終止,程式組依然存在(程式組長僅用於建立新程式組)
  • 父程式建立子程式後立即透過 setpgid() 改變其組標識(PGID)【當需要將子程式設定到其它程式組時】
  • 同時,子程式也需要透過 setpgid() 改變自身組標識(PGID)【當需要將子程式設定到其它程式組時】
  • 當子程式呼叫 exec()

    • 父程式無法透過 setpgid() 改變子程式組標識(PGID)
    • 只能自身透過 setpgid() 改變其組標識 (PGID)

程式組標識設定技巧

實驗1:設定子程式為程式組長 ? 為例
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int pid = 0;

    printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());

    if( (pid = fork()) > 0) {
        int r = setpgid(pid, pid);    // ① 子程式設定新的程式組
        printf("new: %d, r = %d\n", pid, r);
    }
    else if (pid == 0) {
        setpgid(pid, pid);   // ② -> setpgid(0,0) -> setpgid(子程式pid, 用子程式id作為程式組id)
        sleep(1);
        printf("child = %d, ppid = %d, pgid =%d\n", getpid(), getppid(), getpgrp());
    }
    else {
        printf("fork error ...\n");
    }

    return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out 
parent = 3434, ppid = 2125, pgid = 3434
new: 3435, r = 0
child = 3435, ppid = 1, pgid =3435
問:為什麼在父子程式都需要呼叫 setpgid 呢?
答:為了雙保險。

fork 完成之後,無法確認是父程式先執行還是子程式限制性(現代作業系統一般子程式先執行)。
為了確保不讓子程式與父程式在“短暫的時間內”仍出現在相同的程式組中,需要在子程式建立出來之後立即對 “子程式” 進行程式組設定。


實驗2:當子程式呼叫 exec() 後,父程式無法透過 setpgid() 改變子程式組標識(PGID)
main.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int pid = 0;

    printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());

    if( (pid = fork()) > 0) {
        int r = setpgid(pid, pid);
        printf("new: %d, r = %d\n", pid, r);
    }
    else if (pid == 0) {
        char *out = "./helloword.out";
        char *const ps_argv[] = {out, NULL};
        char *const ps_envp[] = {"PATH=/bin:/usr/bin", NULL};
        execve(out, ps_argv, ps_envp);
    }
    else {
        printf("fork error ...\n");
    }

    sleep(60);

    return 0;
}
helloword.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());

    printf("hello world\n");

    sleep(30);

    return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out &
[1] 5660
parent = 5660, ppid = 2125, pgid = 5660
new: 5662, r = 0  // r 等於 0, 表示父程式 setpgid 呼叫成功
child = 5662, ppid = 5660, pgid = 5662
hello world
修改 main.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int pid = 0;

    printf("parent = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());

    if( (pid = fork()) > 0) {
        int r = 0;
        sleep(1);  // 確保子程式先執行並呼叫了 execve
        r = setpgid(pid, pid);
        printf("new: %d, r = %d\n", pid, r);
    }
    else if (pid == 0) {
        char *out = "./helloword.out";
        char *const ps_argv[] = {out, NULL};
        char *const ps_envp[] = {"PATH=/bin:/usr/bin", NULL};
        execve(out, ps_argv, ps_envp);
    }
    else {
        printf("fork error ...\n");
    }

    sleep(60);

    return 0;
}
修改 helloword.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
    sleep(5);  // 等待父程式 setpgid 後再進行後續列印

    printf("child = %d, ppid = %d, pgid = %d\n", getpid(), getppid(), getpgrp());

    printf("hello world\n");

    sleep(30);

    return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out &
parent = 5917, ppid = 2125, pgid = 5917
tiansong@tiansong:~/Desktop/linux$ new: 5919, r = -1  // r 等於 -1, 表示 setpgid 執行失敗
child = 5919, ppid = 5917, pgid = 5917  // pgid 也未發生變化
hello world

會話與終端的關係

Linux 會話(session)

  • 使用者透過終端登入系統後產生一個會話
  • 會話是一個或多個程式組的集合
  • 每個會話有一個會話標識 (SID

    • 終端登陸後的第一個程式成為會話首程式,通常是一個 shell/pash
    • 對於會話首程式 (session leader), 其 PID 與 SID 相等

image.png

image.png

  • 通廠情況下,會話與一個終端(控制終端)相關聯用於執行輸入輸出操作

    • 會話首程式建立與控制終端的連線(會話首程式又叫做控制程式)
    • 會話中的程式組可分為:

      • 前臺程式組:可接收控制終端中的輸入,也可輸出資料到控制終端
      • 後臺程式組:所有程式後臺執行,無法接收終端中的輸入,但可輸出資料到終端

image.png

其中 getty 用於關聯終端

會話與前後臺程式組

會話中的前臺程式組

image.png

問題

在終端中輸入命令後,發生了什麼?
  • 當命令列(shell)執行命令後建立一個新的程式組
  • 如果執行的命令中有多個子命令則建立多個程式(新建立的程式處於新建的程式組中)
  • 命令不帶 &

    • shell 將新見的程式組設定為前臺程式組,並將自己暫時設定為後臺程式組
  • 命令帶 shell

    • shell 將新見的程式組設定為後臺程式組,自己依舊是前臺程式組

什麼是終端程式組標識(TPGID)

  • 標識程式是否處於一個和終端相關的程式組中
  • 前臺程式:TPGID == PGID (由於前臺程式組可能改變,TPGID 用於標識當前的前臺程式組)
  • 後臺程式:TPGID != PGID
  • 若程式和任何終端無關:TPGID == -1
透過比較 TPGID 與 PGID 可判斷:一個程式屬於前臺程式組,還是後臺程式組
如果程式組和終端相關聯,那麼當終端關閉斷開連線,程式組的程式將全部結束
tiansong@tiansong:~/Desktop/linux$ ps -ajx | grep TPGID
   PPID     PID    PGID     SID TTY        TPGID STAT   UID   TIME COMMAND
   2125    6933    6932    2125 pts/3       6932 S+    1000   0:00 grep --color=auto TPGID

會話程式設計深度剖析

Linux 會話介面

  • #include <unistd.h>
  • pid_t getsid(pd_t pid); // 獲取指定程式的 SID, (pid == 0) → 當前程式
  • pid_t setpid(void); // 用於建立新會話,其中呼叫程式不能是程式組長,執行了如下動作:

    • 建立新會話, SID == PID,呼叫程式會成為會話首程式(在建立的會話中是唯一程式)
    • 建立新程式組, PGID == PID, 呼叫程式成為程式組長 (在建立的程式組中是唯一程式)
    • 呼叫程式沒有控制終端,若呼叫前關聯了控制終端,呼叫後與控制終端斷聯
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int pid = 0;

    if( (pid = fork()) > 0) {
        printf("parent = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
        printf("new: %d\n", pid);
    }
    else if (pid == 0) {
        setsid();  // 子程式脫離當前會話,建立新的程式會話,新的程式組
        sleep(180);
        printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
    }
    else {
        printf("fork error ...\n");
    }

    sleep(240);

    return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out   // 前臺執行
parent = 7291, ppid = 2125, pgid = 7291, sid = 2125
new: 7292
^C                                          //  ctrl + c 終止前臺程式組中的前臺程式
tiansong@tiansong:~/Desktop/linux$ ps       // 檢視當前終端視窗中執行的基本資訊,發現沒有 a.out
    PID TTY          TIME CMD
   2125 pts/3    00:00:00 bash
   7361 pts/3    00:00:00 ps
tiansong@tiansong:~/Desktop/linux$ ps -ajx | grep a.out  // 顯示沒有控制終端的程式(-x), 搜尋 a.out
      1    7292    7292    7292 ?             -1 Ss    1000   0:00 ./a.out  // 7292可知為建立的子程式。? 表明與任何一個終端都不關聯,同時 TPGID 為 -1
   2125    7383    7382    2125 pts/3       7382 S+    1000   0:00 grep --color=auto a.out

tiansong@tiansong:~/Desktop/linux$ pstree -p -s -A 7292
systemd(1)---a.out(7292)                    // 父程式終止執行,被初始化程式接管

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
    int pid = 0;

    if( (pid = fork()) > 0) {
        printf("parent = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
        printf("new: %d\n", pid);
    }
    else if (pid == 0) {
        setsid();
        sleep(3);   // 修改此處,方便觀察列印
        printf("child = %d, ppid = %d, pgid = %d, sid = %d\n", getpid(), getppid(), getpgrp(), getsid(getpid()));
    }
    else {
        printf("fork error ...\n");
    }

    sleep(240);

    return 0;
}
tiansong@tiansong:~/Desktop/linux$ ./a.out 
parent = 7947, ppid = 2125, pgid = 7947, sid = 2125
new: 7948
child = 7948, ppid = 7947, pgid = 7948, sid = 7948    // pgid(程式組長), sid(會話首程式)為當前程式,符合預期
^C // ctrl + c 終止前臺程式組中的前臺程式

tiansong@tiansong:~/Desktop/linux$ ps -ajx | grep 7948
      1    7948    7948    7948 ?             -1 Ss    1000   0:00 ./a.out  // ? 表明與任何一個終端都不關聯,同時 TPGID 為 -1
   2125    8028    8027    2125 pts/3       8027 S+    1000   0:00 grep --color=auto 7948
問題:在上述測試中,子程式建立新會話,與當前終端斷開,與任何終端都不關聯,那麼為什麼會在當前終端有輸出呢?

image.png

儘管 setsid 導致子程式再無相關聯的終端,但因為 fork 的關係,子程式的 stdout 仍標記的當前所操作的終端上,因此子程式的列印會在當前終端輸出

總結:標準輸入輸出與終端是“無關”的。只不過在預設情況下,標準輸入輸出和終端掛接到了一起(可以透過重定向使其斷連)。(新會話可以沒有控制終端,但還是可以有標準輸入輸出)