Linux 進程與信號的概念和操作 linux process and signals


## 進程

**主要參考: http://www.bogotobogo.com/Linux/linux_process_and_signals.php **
譯者:李秋豪

信號與進程幾乎控制了操作系統的每個任務。

在shell中輸入ps -ef命令,我們將得到如下結果:

(譯者注:-e Select all processes. Identical to -A; -f Do full-format listing. This option can be combined with many other UNIX-style options to add additional columns. It also causes the command arguments to be printed.)

UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0  2010 ?        00:01:48 init 
root     21033     1  0 Apr04 ?        00:00:39 crond
root     24765     1  0 Apr08 ?        00:00:01 /usr/sbin/httpd

ProcessState.png

每一個進程都會被賦予一個特殊的整數,稱為進程標識符 (process identifier PID) ,PID的范圍是2~32768。當一個進程啟動的時候,數字最少會從2開始算,因為1是為init進程保留的——正如上面這個例子可以看到的,init進程會管理其他的進程。

當我們運行一個程序時,保存在硬盤上的可執行指令集就會被加載到內存中的一個區塊中,通常來說,一個linux進程是不能向這個區塊進行寫操作的。(所以說,這個區塊可以被安全地共享)

process

同樣,系統的庫也可以被共享。因此,即使很多程序都用到了printf這個函數,在內存中只要有一份拷貝就夠了。

與能夠共享的庫不同,一個程序或許會有自己的內部變量,這些變量是保存在程序自己獨有的棧空間中的,無法和另外的進程共享。每個進程也有自己管理的獨有的環境變量。另外,每個進程也應該有自己獨有的程序計數器(PC)——用來記錄程序執行到哪里了。(執行線程請參考 linux pthread)

進程表

進程表中保存了當前內存中加載的所有進程,我們可以使用ps命令將其顯示出來。但是,默認情況下ps只會顯示和終端或者偽終端或者串行鏈接(serial line)保持連接的進程。其他不需要和用戶終端交互的進程是由操作系統負責管理共享資源的。為了顯示所有的程序,我可以使用-e-f參數。

(譯者注:To see every process on the system using standard syntax: ps -ef)

系統進程

$ ps -ax
  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     1:48 init [3]
    2 ?        S<     0:03 [migration/0]
    3 ?        SN     0:00 [ksoftirqd/0]
 ....
 2981 ?        S<sl  10:14 auditd
 2983 ?        S<sl   3:43 /sbin/audispd
 ....
 3428 ?        SLs    0:00 ntpd -u ntp:ntp -p /var/run/ntpd.pid -g
 3464 ?        Ss     0:00 rpc.rquotad
 3508 ?        S<     0:00 [nfsd4]
 ....
 3812 tty1     Ss+    0:00 /sbin/mingetty tty1
 3813 tty2     Ss+    0:00 /sbin/mingetty tty2
 3814 tty3     Ss+    0:00 /sbin/mingetty tty3
 3815 tty4     Ss+    0:00 /sbin/mingetty tty4
.....
19874 pts/1    R+     0:00 ps -ax
19875 pts/1    S+     0:00 more
21033 ?        Ss     0:39 crond
24765 ?        Ss     0:01 /usr/sbin/httpd

STAT對應的字符含義如下表所示:

STAT Code Description
R 正在運行或者有能力運行。
D 不間斷的睡眠 (等待中) - 通常是為了等待完成輸入輸出。
S 睡眠中. 通常是在等待一個事件, 例如一個信號或者輸入變成可獲得的。
T 已停止. 通常是被shell的job控制了或者正在被一個調試器進行調試。
Z 死亡/失效的僵屍進程.
N 低優先級, nice(譯者注:nice后面會提到).
W 分頁.
s 這個進程是會話中的首進程.
+ 這個進程在前台工作組中。
l 這個進程是多線程的。
< 高優先級的任務。

觀察下面這個進程:

1 ?        Ss     1:48 init [3]

每一個子進程都是由父進程fork出來的。當linux開始運行時,它只運行了一個進程:init, PID為1。init是系統的進程管理者,並且它是其他所有進程的直接/間接父進程。當init fork出進程后,這些進程又開始fork進程(類似於病毒傳播)。登錄就是一個例子:init會為每個終端通過fork出getty這個進程,通過它我們可以進行登錄操作。如下所示:

 3812 tty1     Ss+    0:00 /sbin/mingetty tty1

getty進程會等待被終端激活,為用戶輸出登錄時候的提示符,然后把控制交給登錄相關的程序,這些程序會建立起用戶的環境然后啟動一個shell。當用戶從這個shell退出的時候,init會啟動另一個getty進程。

啟動新的進程並等待他們結束是一個操作系統的基本任務。我們也可以通過使用系統調用fork(), exec(), wait(), 完成這些工作。

一個系統調用相當於一個可控的和內核交流的入口,通過這些調用,進程可以要求內核提供一些服務和工作。

事實上,一個系統調用會將處理器的用戶狀態轉化為內核狀態,因此cpu可以訪問內存中被保護的內核模塊。內核通過系統調用API為進程提供了非常豐富的服務。

進程調度

讓我看看ps ax本身的STAT:

23603 pts/1    R+     0:00 ps ax

R代表進程23603處於runnable狀態。換句話說,它監測了自己的狀態。指示器只是表明了這個程序處於可運行狀態,並不一定正在運行(參見下面的資料,可能在runqueue中)。R+表示出這個進程是在前台工作組中,所以它不會等待其他的進程完成也不會等待輸入輸出完畢。這也是為什么我們可能在ps的輸出中看到兩個以上R+的進程。

(譯者:這個地方感謝胡堯學長指點,之前有幾句話沒有理解正確)


譯者:參考一下Process State DefinitionRunnable Process Definition :(有時間我會把這兩篇翻譯一下)(更新:已經翻譯了:Linux 進程狀態標識Linux 可運行進程

1.節選Process State Definition中前一部分:

Process state is the state field in the process descriptor.

A process descriptor is a task_struct-type data structure whose fields contain all of the information about a single process. A process, also referred to as a task, is an instance of a program in execution.

A data structure is a way of storing data in a computer so that it can be used efficiently. task_struct is a relatively large data structure (roughly 1.7 kilobytes on a 32-bit machine) that is designed to hold all the information that the kernel (i.e., the core of the operating system) has and needs about a process.

The state field in the process descriptor describes what is currently happening to a process. This field contains one of the following five flags (i.e., values):

TASK_RUNNING: The process is runnable, and it is either currently running or it is on a runqueue waiting to run. This is the only possible state for a process executing in user space (i.e., that portion of system memory in which user processes run); it can also apply to a process in kernel space (i.e., that portion of memory in which the kernel executes and provides its services) that is actively running. A runnable process is a process that is in the TASK_RUNNING process state.

A runqueue is the basic data structure in the scheduler, and it contains the list of runnable processes for the CPU (central processing unit), or for one CPU on a multiprocessor system. The scheduler, also called the process scheduler, is a part of the kernel that allocates the scare CPU time among the various runnable processes on the system.

2.Runnable Process Definition

A runnable process is a process which is in the TASK_RUNNING process state.

A process, also referred to as a task, is an instance of a program in execution. A process state is a field in the process descriptor. This field can accept any of five possible flags (i.e., values), one of which is TASK_RUNNING.

A process descriptor is a task_struct-type data structure whose fields contain all of the information regarding a single process. Its process state field describes what is currently happening to the process. A data structure is a way of storing data in a computer so that it can be used efficiently. A task_struct data structure is a data structure that is used to describe a process on the system.

The TASK_RUNNING state means that the process is runnable, and it is either currently running or on a runqueue waiting to run. This is the only possible state for a process executing in user space (i.e., that portion of system memory in which user processes run); it can also apply to a process in kernel space (i.e., that portion of memory in which the kernel executes and provides its services) that is actively running.

A runqueue is the basic data structure in the scheduler, and it contains the list of runnable processes for the CPU (central processing unit), or for one CPU on a multiprocessor system. The scheduler, also called the process scheduler, is a part of the kernel that allocates the scare CPU time among the various runnable processes on the system.


Linux內核使用一個叫做進程調度器的程序通過進程的優先級判斷哪個進程會獲得下一個cpu時間片

通常情況下,幾個程序會同時競爭計算資源。如果一個程序只占用少量的計算資源並且會停下來等待輸入,我們就說它是“安分守己的”——與此相反,有的進程會不斷的霸占系統的計算資源。術語上我們把“安分守己”的程序稱作美好(nice)程序。同時,這種美好程度(niceness)也是可計量的。

操作系統通過進程的nice值來判斷該進程的優先級。長時間不暫停的程序通常會有更低的優先級(譯者:沒懂,如果這樣的程序是非常不nice的——需要很多計算資源的進程,還是給它很小的優先級嗎?),相反地,暫停的程序會得到“獎賞”——這保證了交互式進程可以很快的相應用戶,當它在等待用戶輸入時,操作系統會提高的它的優先級,這樣當它准備恢復運行的時候就已經是高優先級了。

nice值(niceness)是一個從-20到20的整數,-20代表最高的優先級,19或者20代表最低的優先級。一個子進程的默認優先級是從它的父進程繼承來的,通常是0。我們可以通過nice命令設置nice值,也可以使用renice命令更改nice值。nice命令每次會把進程的nice值提高10,使得它的優先級降低。只有root權限的用戶可以降低進程的nice值(提高優先級)。在Linux上你可以改變 /etc/security/limits.conf來允許別的用戶或者組降低nice值。

我們可以通過ps和參數-l-f查看進程的nice值:

(譯者注:-l Long format. The -y option is often useful with this. -y Do not show flags; show rss in place of addr. This option can only be used with -l.)

F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S   601 12649 12648  0  75   0 -  1135 wait   pts/0    00:00:00 bash
0 S   601 12681 12649  0  76   0 -  1122 wait   pts/0    00:00:00 myTest.sh
0 S   601 12682 12681  0  76   0 -   929 -      pts/0    00:00:00 sleep
0 R   601 12683 12649  0  76   0 -  1054 -      pts/0    00:00:00 ps

:這里我們可以看到 myTest.sh程序是運行在默認nice值0下(譯者注:NI列)。但如果它是這么啟動的:

$ nice ./myTest.sh &

那么它的nice值就會+10。

F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S   601  9835  9834  0  75   0 -  1135 wait   pts/1    00:00:00 bash
0 S   601 12744 12649  0  86  10 -  1122 wait   pts/0    00:00:00 myTest.sh
0 S   601 12745 12744  0  86  10 -   929 -      pts/0    00:00:00 sleep
0 R   601 12746 12649  0  76   0 -  1054 -      pts/0    00:00:00 ps

也可以這樣做:

$ renice 10 12681
12681: old priority 0, new priority 10

有了更高的nice值,這個程序會更少的運行。如下圖所示,STAT列的值多了一個N標記,說明這個進程的nice值和默認不同了。

$ ps x
12649 pts/0    Ss     0:00 -bash
12744 pts/0    SN     0:00 /bin/bash ./myTest.sh
12745 pts/0    SN     0:00 sleep 100
12867 pts/0    R+     0:00 ps x

The PPID field of ps output indicates the parent process ID, the PID of either the process that caused this process to start or, if that process is no longer running, init (PID 1).ps輸出中PPID列表示了該進程父進程的PID,如果那個父進程沒有運行了,就會是init(PID 1)

init進程 / 守護進程

(譯者注:守護來自於daemon這個詞,它有兩個含義:1.(esp in Greek mythology) supernatural being that is half god, half man (尤指希臘神話中的)半人半神的精靈. 2. spirit that inspires sb to action or creativity 守護神.)

當我們啟動系統的時候,內核會創建一個叫做init的進程(來自於/sbin/init),它是所有其他進程的“祖宗”

系統上所有的其他進程都是通過調用fork()init或者它的后代生成的。init進程總是擁有為1的PID和超級用戶的權限。它也不能被終止掉,除非機器關機。inti的主要功能就是相應操作系統生成其他進程並監視管理所有進程。

守護進程是一個有着特殊目的的進程(例如syslogd, httpd等等),它也是由操作系統負責生成並管理的,但它和普通的進程有以下兩個不同:

  1. 長壽命。一個守護程序通常會在系統啟動的時候就開始運行,直到機器關機。
  2. 它是在后台運行的,也就是說沒有一個和它連接的終端可以用來輸入輸出。

創建一個新進程

我們可以在一個程序中啟動另一個程序,system庫函數就是用來創建新進程的。下面這個例子就通過調用system運行了ps.

// mySysCall.c

#include <iostream>

int main()
{
  system("ps ax");
  std::cout << "Done." << std::endl;
  exit(0);
  return 0;
}

如果運行這個程序,輸出如下:

$./mySysCall
  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     1:48 init [3]
....
24447 pts/0    S+     0:00 ./mySysCall
24448 pts/0    R+     0:00 ps ax
Done.

因為system是通過一個shell啟動新的進程的,我們也可以做一個改變:

  system("ps ax &");

運行這個新的版本,輸出如下:

Done.
  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     1:48 init [3]
....
24849 pts/1    Ss+    0:00 -bash
25802 pts/1    R      0:00 ps ax

現在,system在shell命令完成后就立即返回了。因為它要求shell將這個新程序放在后台運行,shell會在ps程序啟動后立即返回。這和我們在shell中輸入相同的命令是一樣的:

$ps ax &

shell返回后,我們的程序就打印出“Done.“並在ps命令有機會完成輸出前退出。這看起來有些難以理解,所以我們也需要完全控制進程的行為。

exec() 系統調用

exec函數會將當前的進程替換為一個新的進程,這個新的進程可以由路徑或者文件參數指定。我們可以使用exec將我們正在執行的程序切換到另一個。

如下圖所示,我們在bash中發起ls命令。在這種情況下,shell作為父進程,通過調用fork()創建出一個子進程,這個子進程隨之調用exec()將之變為ls

exec_ls.png

exec會比system更加有效率,因為調用exec后父進程就不會再運行了。

(譯者注: The exec() family of functions replaces the current process image with a new process image. )

/* Execute PATH with arguments ARGV and environment from `environ'.  */
extern int execv (__const char *__path, char *__const __argv[])
     __THROW __nonnull ((1));

/* Execute PATH with all arguments after PATH until a NULL pointer,
   and the argument after that for environment.  */
extern int execle (__const char *__path, __const char *__arg, ...)
     __THROW __nonnull ((1));

/* Execute PATH with all arguments after PATH until
   a NULL pointer and environment from `environ'.  */
extern int execl (__const char *__path, __const char *__arg, ...)
     __THROW __nonnull ((1));

/* Execute FILE, searching in the `PATH' environment variable if it contains
   no slashes, with arguments ARGV and environment from `environ'.  */
extern int execvp (__const char *__file, char *__const __argv[])
     __THROW __nonnull ((1));

/* Execute FILE, searching in the `PATH' environment variable if
   it contains no slashes, with all arguments after FILE until a
   NULL pointer and environment from `environ'.  */
extern int execlp (__const char *__file, __const char *__arg, ...)
     __THROW __nonnull ((1));

這些函數大多是通過使用execve實現的,以p作為后綴的函數會在環境變量PATH對應的地方搜尋那個要運行的程序,如果沒有找到可運行的那個程序,你必須給這個函數傳入一個文件的絕對路徑作為參數。

全局變量environ可以給新的程序傳遞環境參數。execleexecve有另外的方法:你可以傳入一個字符串數組用來建立新程序的環境。

下面是使用execlp的一個例子:

(譯者注:unistd.h 是 C 和 C++ 程序設計語言中提供對 POSIX 操作系統 API 的訪問功能的頭文件的名稱。該頭文件由 POSIX.1 標准(單一UNIX規范的基礎)提出,故所有遵循該標准的操作系統和編譯器均應提供該頭文件(如 Unix 的所有官方版本,包括 Mac OS XLinux 等)。)

// my_ps.c

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

int main()
{
  printf("ps with execlp\n");
  execlp("ps", "ps", 0);
  printf("Done.\n");
  exit(0);
}

當我們運行這個程序,輸出將會只有ps的標准輸出而沒有"Done", 同樣的,我們在ps的輸出中也找不到my_ps這個進程。

$./my_ps
ps with execlp
  PID TTY          TIME CMD
12377 pts/0    00:00:00 bash
18304 pts/0    00:00:00 ps

這個程序打印出了第一個“ps with execlp”,然后調用了execlp() ——在PATH環境變量對應的地方搜索一個叫做ps的程序。最后它執行ps以代替my_ps ,就像我們在shell中執行以下命令一樣:

$ ps

(譯者注:舉個例子,實際上,bash里面就有一個exec命令,我們平時在bash中執行的命令都是在生成了子進程,並沒有替換當前shell的進程,如果在bash中直接使用exec ps會馬上”退出“bash——輸出你也來不到,如果我們在一個bash中輸入bash,然后輸入exec ps ,就會得到正確的輸出,但是這個時候實際上已經在第一個bash里面了,輸入一個exit就能退出shell了。)

所以,當ps進程完畢時,我們會得到一個shell的提示符而不是返回到my_ps 。因此,第二個printf沒有打印出”Done“這個消息。exec得到的新進程的PID和nice值都是和”父進程“一樣的。

為了讓一個進程可以同時進行多個函數,我們可以使用threads或者完全創建另一個進程,就像init做的,而不是像exec一樣替換現有進程。

其中的一種方法就是調用fork().

fork() 與 execv()

在下面的代碼中,fork先在父進程中穿創建子進程,隨后這個子進程調用exec將父進程的代碼替換為path中指定的值。

void main(char *path, char *argv[]) (譯者:main函數第一個參數還可以指針類型?)
{ 
    pid_t pid = fork(); 
    if (pid == 0) 
    { 
        printf("Child\n"); 
        execv(path, argv); 
    } 
    else 
    { 
        printf("Parent %d\n", pid); 
    } 
    printf("Parent prints this line \n"); 
} 

fork() 系統調用

(譯者注:可以先看一下中文的一個教程linux中fork()函數詳解(原創!!實例講解),覺得講解的不錯,特別是fork的時候流緩沖區的問題和fork返回值的問題很有意思。)

我們可以調用fork()來創建一個新的進程。這個系統調用會”復制“當前的進程,在進程表中產生一個新的入口,新的進程的很多屬性和負進程是相同的。

理解fork()的關鍵點在於當它返回時,會存在兩個進程,並且,在兩個進程中,程序會從fork()返回的地方繼續開始執行。

Linux會復制父進程完整的地址空間並把它賦值給子進程。因此,父進程和子進程擁有完全相同內容的地址空間/代碼。但是這兩個進程是互相獨立的,它們有自己的獨立的環境,數據空間,文件描述符等等。所以,和exec()相結合,fork()就是我門需要用來創建新程序的調用。

另外,要注意的是,fork()被調用一次會返回兩次!(譯者注,這句話本來放在前面,但放在這好像好一些)

對於父進程,fork()會返回新創建的子進程的PID ——這是很有用的,因為父進程可能會創建很多進程並監視它們的狀態(通過wait()函數)(譯者注:wait, waitpid, waitid - wait for process to change state),對於子進程,fork()會返回0。如果必要的話,進程可以通過getpid()獲得本進程的PID ,通過getppid()獲得父進程的PID (譯者注:如果父進程已經死亡,PPID將會是1)。如果fork()調用失敗會返回-1,這要么是因為子進程數量上的限制(CHILD_MAXerrno會被設置成EAGAIN) ,要么是因為進程表中沒有足夠的空間再創建一個入口或者(虛擬)內存不足(errno會被設置成ENOMEM )。

那么,在fork()之后哪一個進程會先運行呢?

是子進程?還是父進程?

事實上,這是未定義的!

fork_diagram.png

上面的參考圖來自於 "The Linux Programming Interface"

下面是個總結:

系統調用fork()不需要參數轉入並且會返回一個PID. 使用fork()的目的在於創建一個新的進程,也就是其父進程的子進程。在新的子進程創建后,父與子都會從fork()調用的下一條指令開始執行。因此,我們必須區分哪一個是父進程,哪一個是子進程,這可以通過fork()的返回值來判斷:

  1. 如果返回一個負值,那么調用失敗。
  2. 如果返回的是0,那么當前處於新創建的子進程。
  3. 如果返回一個正數,那這個數代表新創建的子進程的PID(這個正數是pid_t類型的,聲明在sys/types.h)。正常情況下,這個正數是一個整數。另外,一個進程可以使用getpid()來獲取本進程的PID。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>	
//譯者注:還應該包括unistd.h

#define BUF_SIZE 150

int main()
{
  int pid = fork();
  char buf[BUF_SIZE];
  int print_count;

  switch (pid)
  {
    case -1:
      perror("fork failed");
      exit(1);
    case 0:
      /* When fork() returns 0, we are in the child process. */
      print_count = 10;
      sprintf(buf,"child process: pid = %d", pid);
      break;
    default: /* + */
      /* When fork() returns a positive number, we are in the parent process
       * (the fork return value is the PID of the newly created child process) */
      print_count = 5;
      sprintf(buf,"parent process: pid = %d", pid);
      break;
  }
  for(;print_count > 0; print_count--) {
      puts(buf);
      sleep(1);
  }
  exit(0);
}

Output is:

child process: pid = 0
parent process: pid = 13510
child process: pid = 0
parent process: pid = 13510
child process: pid = 0
parent process: pid = 13510
child process: pid = 0
parent process: pid = 13510
child process: pid = 0
parent process: pid = 13510
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0

正如輸出中所看到的,fork()在父進程中返回了子進程的PID,在子進程中返回了0. 我們使用fork()創建的子進程獨立於父進程運行。但是有些時候,我們想要知道子進程是否運行完了,如果父進程提前於子進程運行完畢,就像上面這個例子,這會很讓人困惑。所以,我們需要通過wait()函數來等待子進程執行完畢。

譯者注:在我的機器(Ubuntu 16.04 gcc 5.4 bash 4.3.48)上運行結果如下:

frank@under:~/tmp$ ./a.out 
parent process: pid = 24238
child process: pid = 0
parent process: pid = 24238
child process: pid = 0
parent process: pid = 24238
child process: pid = 0
parent process: pid = 24238
child process: pid = 0
parent process: pid = 24238
child process: pid = 0
child process: pid = 0
frank@under:~/tmp$ child process: pid = 0 #這里
child process: pid = 0
child process: pid = 0
child process: pid = 0
ls
a.out  hellolinux  hellolinux.c  test.c  test.s
frank@under:~/tmp$ 

這其中標識的那一行很有意思,bash的提示符先於子程序結束前出現了,我猜想是因為bash只是等待父進程執行完畢然后開始接受新的輸入,對於這個父進程產生的子進程它並不關心。於是乎我做了一個小實驗:

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

#define BUF_SIZE 150

int main()
{
  int pid = fork();
  char buf[BUF_SIZE];
  int print_count;

  switch (pid)
  {
    case -1:
      perror("fork failed");
      exit(1);
    case 0:
      /* When fork() returns 0, we are in the child process. */
      print_count = 10;
      sprintf(buf,"child process: pid = %d", pid);
      break;
    default: /* + */
      /* When fork() returns a positive number, we are in the parent process
       * (the fork return value is the PID of the newly created child process) */
      print_count = 5;
      sprintf(buf,"parent process: pid = %d", pid);
      break;
  }
  for(;print_count > 0; print_count--) {
      puts(buf);
      sleep(1);
  }
  if(pid)
  {
  	return 1;//the parent process
  }
  else
  {
  	return 0;//the child process
  }
}

如果bash也監控子進程,那么由於子進程是后來完成的,bash得到的返回值應該是0,否則就是1.結果輸出如下:

frank@under:~/tmp$ ./a.out 
parent process: pid = 26290
child process: pid = 0
parent process: pid = 26290
child process: pid = 0
parent process: pid = 26290
child process: pid = 0
parent process: pid = 26290
child process: pid = 0
parent process: pid = 26290
child process: pid = 0
child process: pid = 0
frank@under:~/tmp$ child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
echo $?
1
frank@under:~/tmp$ 

可以看到,其返回值是1,猜想正確。

wait() 系統調用

使用wait()的主要是為了和子進程的同步性。

  1. 暫時將父進程掛起,直到某一個子進程終止。
  2. 返回值是終止子進程的PID,對於一個成功返回的進程,父進程將會回收子進程 。
  3. 如果child_status != NULLstatus 的值將會反映子進程終止的原因。
  4. 如果父進程有多個子進程,那么wait()將會在任何一個子進程終止的時候返回。
  5. waitpid()可以被用來等待特定的子進程。

父進程需要知道什么時候它的子進程終止了或者狀態改變了或者接收到一個信號而停止了。wait()就是監視子進程的其中一個方法(另一個是SIGCHLD信號)。

(譯者注:SIGCHLD 20,17,18 Ign Child stopped or terminated)

wait()會鎖住調用的進程直到它的子進程退出或者接收到了一個信號wait()會接受一個整型的地址參數並返回完成的子進程的PID

#include <sys/wait.h>
pid_t wait(int *child_status);

再一次說明。調用wait()的一個主要目的就是等待子進程執行完畢。

wait()的執行可以分為兩種情況:

  1. 如果調用wait()的時候存在子進程,調用者將暫時被掛起,直到其中一個子進程終止它才會恢復運行。
  2. 如果調用wait()的時候沒有子進程,那么wait()相當於不起作用。

系統調用wait(&status) 有兩個目的:

  1. 如果調用子進程沒有調用 exit()退出,調用者將暫時被掛起,直到其中一個子進程終止它才會恢復運行。(譯者:這他媽不都講了幾遍啊,嚴重懷疑作者湊字數)
  2. 子進程的終止狀態返回到了wait()的status變量里。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//譯者:#include <unistd.h>
//譯者:#include <sys/wait.h>
//譯者:這作者是不是喝酒了啊,自己都說wait在sys/wait.h里面。。)
#define BUF_SIZE 150

int main()
{
  int pid = fork();
  char buf[BUF_SIZE];
  int print_count;

  switch (pid)
  {
    case -1:
      perror("fork failed");
      exit(1);
    case 0:
      print_count = 10;
      sprintf(buf,"child process: pid = %d", pid);
      break;
    default:
      print_count = 5;
      sprintf(buf,"parent process: pid = %d", pid);
      break;
  }
  //if(!pid) { 譯者注:又TMD寫錯了,這是子進程,醉了。。
    if(pid) {
    int status;
    int pid_child = wait(&status;);
  }
  for(;print_count > 0; print_count--) puts(buf);
  exit(0);
}

現在父進程會等待子進程執行完畢才會開始打印:

child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
parent process: pid = 22652
parent process: pid = 22652
parent process: pid = 22652
parent process: pid = 22652
parent process: pid = 22652

譯者注:稍微改了一下,看看wait()的返回值和對status做的改變:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define BUF_SIZE 150

int main()
{
  int pid = fork();
  char buf[BUF_SIZE];
  int print_count;
  int status = 12345;
  int pid_child;

  switch (pid)
  {
    case -1:
      perror("fork failed");
      exit(1);
    case 0:
      print_count = 10;
      sprintf(buf,"child process: pid = %d", pid);
      break;
    default:
      print_count = 5;
      sprintf(buf,"parent process: pid = %d", pid);
      break;
  }
  if(pid) {
    pid_child = wait(&status);
  }
  for(;print_count > 0; print_count--) puts(buf);
    if (pid)
    {

      printf("pid = %d\n", pid);
      printf("pid_child = %d\n", pid_child);
      printf("status = %d\n", status);
    }
  exit(0);
}

運行輸出:

frank@under:~/tmp$ ./a.out 
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
child process: pid = 0
parent process: pid = 842
parent process: pid = 842
parent process: pid = 842
parent process: pid = 842
parent process: pid = 842
pid = 842
pid_child = 842
status = 0
frank@under:~/tmp$ 

很明顯,status被改變為0,wait()返回的值就是子進程的PID

exit() 庫函數調用 / _exit() 系統調用

exit(status)庫函數是用來終止進程的,同時使得進程占用的所有資源(內存,打開的文件描述符等待)釋放掉,被內核進行再分配處理,以便被別的進程所使用。傳入的status參數決定了這個進程結束時候的狀態,這個狀態是可以被wait()所捕獲的。

另外,exit()_exit()系統調用的抽象......在fork()之后,通常情況下只有一個父進程的子進程會通過exit()終止掉,其余的進程應該使用_exit() 。” — The Linux Programming Interface

譯者:參考 "Linux Programmer's Manual" :

The function _exit() terminates the calling process "immediately".  Any open file descriptors belonging to the process are closed; any children of the process are inherited by process 1, init, and the process's parent is sent a SIGCHLD signal.

The  value  status  is  returned to the parent process as the process's exit status, and can be collected using one of the  wait(2)  family  of calls.

The function _Exit() is equivalent to _exit().

僵屍進程

父進程和子進程的存活時間一般都不相同:要么父進程活得長要么相反。

那么,如果子進程在父進程還沒有調用wait()之前終止了會怎么樣?事實是,即使子進程已經終止了,父進程應該還是允許調用wait()查看這個子進程的終止狀態。內核通過將子進程變為一個僵屍進程來處理這樣的情況。這意味着大多數子進程占用的資源都被釋放掉以便系統再分配利用。

事實上,當一個進程終止后,它不會立即從內存中消失——它的進程描述符 (譯者注:進程描述符我以后會在另一篇定義進程狀態的文章中列出的)還會駐留在內存中(這只會占用很少內存)。進程的狀態會變為 EXIT_ZOMBIE 並且通過信號 SIGCHLD 告知其父進程它已經“死亡”了。父進程應該通過調用wait()來讀取這個僵屍進程的退出狀態和其他信息。在wait()調用之后,這個僵屍進程就會完全從內存中消失掉。

這通常發生的非常快,所以你不會看到僵屍進程在你的電腦上不斷增加。然而,如果一個父進程從來不調用wait() ,它產生的僵屍進程就會在程序結束前一直駐留內存。 -來自 what-is-a-zombie-process-on-linux.

// file - zombie.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//譯者注:少了#include <unistd.h>
#define BUF_SIZE 150

int main()
{
  int pid = fork();
  char buf[BUF_SIZE];
  int print_count;

  switch (pid)
  {
    case -1:
      perror("fork failed");
      exit(1);
    case 0:
      print_count = 2;
      sprintf(buf,"child process: pid = %d", pid);
      break;
    default:
      print_count = 10;
      sprintf(buf,"parent process: pid = %d", pid);
      break;
  }
  for(;print_count > 0; print_count--) {
      puts(buf);
      sleep(1);
  }
  exit(0);
}

如果你運行以上代碼,子進程會在父進程結束前結束,並且會變成一個僵屍進程直到父進程結束。如下所示:

譯者注(PID為25351,S為Z,CMD為<defunct>)

$ ./zombie
$ ps -la
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S   601 25350 12377  0  75   0 -   381 -      pts/0    00:00:00 zombie
1 Z   601 25351 25350  0  78   0 -     0 exit   pts/0    00:00:00 zomb <defunct>
0 R   601 25352 12377  0  77   0 -  1054 -      pts/0    00:00:00 ps

以下描述來自於 what-is-a-zombie-process-on-linux.

僵屍進程不會消耗任何系統資源(事實上,每一個僵屍進程只會使用一丁點內存來保存進程描述符)。然而,每一個僵屍進程還是會保留它的PID。Linux在32位系統上有一個固定的PID范圍:32767。如果僵屍進程以很快的速度累計——例如,一個編寫錯誤的服務程序,那么很快就將沒有剩余的PID可以使用,其他正常的進程也啟動不了了

所以,少部分僵屍進程還是無傷大雅的,雖然在一定程度上反映了其父進程存在一些bug。

如果父進程非正常終止,它的子進程會變成init的子進程。僵屍進程會駐留在內存中直到init將其釋放。雖然只是一小段時間,它們也會再釋放前占用PID。

我們不能像終止正常進程那樣使用 SIGKILL 信號終止一個僵屍進程。對於僵屍進程,UNIX像電影中的那樣——它不能被信號終止,甚至是(silver bullet,譯者注:銀色子彈是指一種雞尾酒。援引自西方的魔幻故事中驅魔的銀色子彈,在魔幻故事中有驅魔效果。) SIGKILL 都不行。事實上,這是一個故意為之的特性,為了確保父進程總是可以最終調用wait() 。記住,我們不需要為一小撮僵屍進程擔憂,除非它們快速累計起來了。但是,還是有一些方法拜托僵屍進程的。

其中的一種方法就是像僵屍進程的父進程發送SIGCHLD 信號。這個信號告訴父進程執行wait()然后清除僵屍子進程。可以使用kill命令發送這個信號:(其中的pid是父進程的PID

kill -s SIGCHLD pid

然而,如果父進程沒有正確處理SIGCHLD信號,這就不會有效果。我們必須終止父進程——這些剩下的僵屍子進程的父進程會變成init,而init會定期執行wait() 系統調用去清理僵屍子進程,所以init會使得僵屍進程不那么“囂張”。

如果父進程持續創造僵屍進程,我們就必須debug它了,讓它正確的調用wait()來回收它的僵屍子進程。

僵屍進程並不同於孤兒進程(orphan process)。孤兒進程是指一個持續運行的程序,但是它的父進程已經終止了。它們不是僵屍——它們會被init收養。(譯者:哈哈,生動形象)
換句話說,在父進程終止后,對子進程調用getppid() 會得到1(init )。這可以用來判斷一個進程的父進程是否已經終止。(假設這個子進程不是一開始就是init創建的)



Signals

信號是一種通知,一種由操作系統或者應用程序發出的消息。信號是一種單向異步通知方法,其可能是由內核傳給進程的,也可能是由進程傳給進程的,也可能是自己傳給自己的。信號一般都是用來告知進程一些事件,例如分段錯誤或者用戶按下了CTRL-C。

Linux內核實現了大概30種信號,每一種信號都標記為一個整數,從1到31.信號不會有任何參數,它們自己的名字也大概解釋了它們的含義。例如SIGKILL 或者9號信號告訴程序有人想要殺死它, SIGHUP體現出發生了一個終端上的掛起操作,它在i386架構上是1號信號。

除了SIGKILLSIGSTOP 總是終止或者停止進程,進程可以控制如何處理它們收到的信號。它們可以:

  1. 接受信號默認的操作,例如終止進程、終止並coredump、停止進程、什么都不做等等。
  2. 或者,進程可以選擇忽略或者處理信號。
    1. 默默丟棄信號。
    2. 程序收到信號后跳到用戶實現的信號處理模塊,處理完成后控制重新回到之前被打斷的地方並繼續執行程序。
Signal Name Description
SIGHUP 1 Hangup (POSIX)
SIGINT 2 Terminal interrupt (ANSI)
SIGQUIT 3 Terminal quit (POSIX)
SIGILL 4 Illegal instruction (ANSI)
SIGTRAP 5 Trace trap (POSIX)
SIGIOT 6 IOT Trap (4.2 BSD)
SIGBUS 7 BUS error (4.2 BSD)
SIGFPE 8 Floating point exception (ANSI)
SIGKILL 9 Kill(can't be caught or ignored) (POSIX)
SIGUSR1 10 User defined signal 1 (POSIX)
SIGSEGV 11 Invalid memory segment access (ANSI)
SIGUSR2 12 User defined signal 2 (POSIX)
SIGPIPE 13 Write on a pipe with no reader, Broken pipe (POSIX)
SIGALRM 14 Alarm clock (POSIX)
SIGTERM 15 Termination (ANSI)
SIGSTKFLT 16 Stack fault
SIGCHLD 17 Child process has stopped or exited, changed (POSIX)
SIGCONTv 18 Continue executing, if stopped (POSIX)
SIGSTOP 19 Stop executing(can't be caught or ignored) (POSIX)
SIGTSTP 20 Terminal stop signal (POSIX)
SIGTTIN 21 Background process trying to read, from TTY (POSIX)
SIGTTOU 22 Background process trying to write, to TTY (POSIX)
SIGURG 23 Urgent condition on socket (4.2 BSD)
SIGXCPU 24 CPU limit exceeded (4.2 BSD)
SIGXFSZ 25 File size limit exceeded (4.2 BSD)
SIGVTALRM 26 Virtual alarm clock (4.2 BSD)
SIGPROF 27 Profiling alarm clock (4.2 BSD)
SIGWINCH 28 Window size change (4.3 BSD, Sun)
SIGIO 29 I/O now possible (4.2 BSD)
SIGPWR 30 Power failure restart (System V)

術語產生(raise)代表信號的生成,術語捕獲(catch)代表信號的接受。

信號是由錯誤的條件引發的,它們可能是由shell和終端的處理程序發出的終端指令,也可能是由一個進程向另一個進程傳送的修改行為的指令。

信號可以被:

  1. 產生
  2. 捕獲
  3. 采取行動
  4. 忽略

如果一個進程收到了像 SIGFPE, SIGKILL, 這樣的信號,進程會立即終止,並且會創建一個 core dump文件,這個文件是該進程的內存鏡像,我們可以利用它進行debug。

可參考 coredump debugging

舉個常見的栗子,當我們輸出 interrupt character 的時候(即Ctrl+C),ISGINT 信號就會被送給前台程序 (即目前正在運行的程序)。這會使得程序終止,除非它有捕獲該信號的安排。

kill命令可以用來給進程發送信號。例如我們想要給PID為pid_number發送 hangup信號:

kill -HUP pid_number

kill命令有一個好用的變體killall ,它可以向所有運行了一個命令的進程發送同一個信號。例如,我們給所有運行了 inetd的進程發送一個 reread信號:

   $ killall -HUP inetd

上面這個命令可以使得inetd程序重新讀取他的配置文件。

在下面這個例子中,程序會對Ctrl+C做出反應而不是終止前台程序。但是如果我們再次輸入Ctrl+C的話,它就會終止:

譯者注:(關於signal這個函數)

SYNOPSIS
#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

DESCRIPTION
The  behavior  of  signal()  varies  across UNIX versions, and has also varied historically across different versions of Linux.  Avoid its use: use sigaction(2) instead.   See  Portability below.

signal()  sets  the  disposition  of the signal signum to handler, which is either SIG_IGN,SIG_DFL, or the address of a programmer-defined function (a "signal handler").

If the signal signum is delivered to the process, then one of the following happens:

*  If the disposition is set to SIG_IGN, then the signal is ignored.

*  If the disposition is set to SIG_DFL, then the default action associated with the signal(see signal(7)) occurs.

*  If  the  disposition is set to a function, then first either the disposition is reset to SIG_DFL, or the signal is blocked (see Portability below), and then  handler  is  called with  argument  signum.   If  invocation of the handler caused the signal to be blocked, then the signal is unblocked upon return from the handler.

The signals SIGKILL and SIGSTOP cannot be caught or ignored.

程序:

    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>

    void my_signal_interrupt(int sig)
    {
      printf("I got signal %d\n", sig);
      (void) signal(SIGINT, SIG_DFL);
    }

    int main()
    {
      (void) signal(SIGINT,my_signal_interrupt);

      while(1) {
          printf("Waiting for interruption...\n");
          sleep(1);
      }
    }

輸出如下:

    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...
    I got signal 2
    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...
    Waiting for interruption...

當我們按下Ctrl+C的時候SIGINT信號被傳入進該進程,由於我們設置了由my_signal_interrupt()處理這個信號,程序不會終止,而是進入my_signal_interrupt() ,在my_signal_interrupt() 中,我們打印出“I got signal %d\n”,並將對 SIGINT的處理重新變為信號默認的動作,所以第二次傳入 SIGINT信號時,程序就會執行終止操作。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM