詳解 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
, 設定當前程式的組標識為 pgidpid == 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 相等
- 終端登陸後的第一個程式成為會話首程式,通常是一個
通廠情況下,會話與一個終端(控制終端)相關聯用於執行輸入輸出操作
- 會話首程式建立與控制終端的連線(會話首程式又叫做控制程式)
會話中的程式組可分為:
- 前臺程式組:可接收控制終端中的輸入,也可輸出資料到控制終端
- 後臺程式組:所有程式後臺執行,無法接收終端中的輸入,但可輸出資料到終端
其中 getty 用於關聯終端
會話與前後臺程式組
會話中的前臺程式組
問題
在終端中輸入命令後,發生了什麼?
- 當命令列(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
問題:在上述測試中,子程式建立新會話,與當前終端斷開,與任何終端都不關聯,那麼為什麼會在當前終端有輸出呢?
儘管 setsid 導致子程式再無相關聯的終端,但因為 fork 的關係,子程式的 stdout 仍標記的當前所操作的終端上,因此子程式的列印會在當前終端輸出
總結:標準輸入輸出與終端是“無關”的。只不過在預設情況下,標準輸入輸出和終端掛接到了一起(可以透過重定向使其斷連)。(新會話可以沒有控制終端,但還是可以有標準輸入輸出)