【Linux 應用編程】進程管理 - 進程、線程和程序

基本概念

程序和進程的區別

程序是平臺相關的二進制文件,只佔用磁盤空間。編寫完程序代碼後,編譯爲可執行的二進制文件便可。web

進程是運行中的程序,佔用 CPU、內存等系統資源。shell

經過 Shell 命令,能夠在終端啓動進程,例如執行 ls 命令:api

  • 找到命令對應的二進制文件
  • 使用 fork() 函數建立新的進程
  • 在新建立的進程中調用 exec 函數組,加載命令對應的二進制文件,並從 main 函數開始執行

併發和並行

併發 concurrent:在一個時間段內,處理的請求總數。個數越多,併發越大。
並行 parallel:任意時刻可以同時處理的請求數。一般跟 CPU 內核數量相關。數組

Linux 進程的狀態

Linux 的進程有如下 6 種狀態:bash

  • D:深度睡眠狀態,不可中斷,處於這種狀態的進程不能響應異步信號
  • R:進程處於運行態或就緒狀態,只有在該狀態的進程纔可能在 CPU 上運行
  • S:可中斷的睡眠狀態,處於這個狀態的進程由於等待某種事件的發生而被掛起
  • T:暫停狀態或跟蹤狀態
  • X:退出狀態,進程即將被銷燬
  • Z:退出狀態,進程成爲殭屍進程

進程狀態轉換圖

命令行控制進程

ps 命令

經過 ps aux 能夠查看當前機器上的進程,其中 STAT 列的第一個字符就是進程的狀態:數據結構

# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.2 190764  2140 ?        Ss    2018  12:35 /usr/lib/systemd/systemd --system --deserialize 20
root         2  0.0  0.0      0     0 ?        S     2018   0:00 [kthreadd]
root         3  0.0  0.0      0     0 ?        S     2018   1:24 [ksoftirqd/0]

kill 命令

kill 命令用於結束進程,語法以下:併發

kill [-s signal|-p] [-q sigval] [-a] [--] pid...
kill -l [signal]

執行 kill 命令,系統會發送一個 SIGTERM 信號給對應的進程,請求進程正常關閉。SIGTERM 是有可能會被阻塞的。kill -9 命令用於強制殺死進程,系統給對應程序發送的信號是 SIGKILL,即 exit。exit 信號不會被系統阻塞。
示例:異步

kill -9 11235

PID

每一個進程在建立的時候,內核都會爲之分配一個全局惟一的進程號。svg

getpid 和 getppid 函數

經過 getpid 函數能夠獲取當前進程的 PID。getppid 函數能夠獲取父進程的 PID。函數

經過 ps aux 能夠查看進程的 PID,資源消耗狀況,經過 ps -ef 能夠查看當前進程及其父進程的 PID。經過 pstree 命令能夠以樹狀關係查看全部進程。

函數原型:

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

pid_t getpid(void);
pid_t getppid(void);

環境變量

除了經過 main 函數的第三個參數獲取環境變量,還能夠經過 environ 全局變量或 getenv() 函數來獲取。

getenv 函數原型:

#include <stdlib.h>

char *getenv(const char *name);
char *secure_getenv(const char *name);

環境變量示例

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

extern char** environ;

int main(int argc, char* argv[], char* env[])
{
    int i = 0;
    char* myenv = NULL;
    while(env[i])
    {
        printf("env[%d] is: %s\n", i, env[i++]);
    }
    i = 0;
    while(environ[i])
        puts(environ[i++]);

    myenv = getenv("PATH");
    puts(myenv);
    return 0;
}

孤兒進程和殭屍進程

PCB(Process Control Block,進程控制塊)是每一個進程都有的數據結構,用於保存進程運行所需的信息,例如文件描述符表。

wait() 函數用來幫助父進程獲取其子進程的退出狀態。當進程退出時,內核爲每個進
程保存了退出狀態信息。若是父進程未調用 wait() 函數,則子進程的退出信息將一直保存在內存中。

孤兒進程

Linux 中,每一個進程在退出的時候,能夠釋放用戶區空間,可是沒法釋放進程自己的 PCB 所佔用的內存資源。PCB 必須由父進程釋放。

父進程在建立子進程後退出,子進程變成孤兒進程。爲防止內存泄漏,孤兒進程被 init 進程領養,init 進程變成孤兒進程的父進程。

下面示例中,父進程先退出:

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

int main()
{
	pid_t pid = fork();
	
	if (pid == 0) 
	{
		printf("child pid is: %d, ppid is: %d\n", getpid(), getppid());
		sleep(1);
		printf("child pid is: %d, ppid is: %d\n", getpid(), getppid());
	}
	else if (pid > 0)
	{	
		sleep(0.5);
		printf("parent pid is: %d, ppid is: %d\n", getpid(), getppid());
		printf("parent exit\n");
	}
	return 0;
}

輸出:

parent pid is: 3348, ppid is: 713
parent exit
child pid is: 3349, ppid is: 1
child pid is: 3349, ppid is: 1

殭屍進程

子進程退出了,父進程一直未調用 wait 或 waitpid 函數,子進程就變成了殭屍進程。

代碼控制進程

進程入口函數 main

可執行的二進制文件,都是從 main 函數開始執行的。main 函數有 3 種原型定義:

int main();
int main(int argc, char *argv[]);
int main(int argc, char *argv[], char *env[]);

參數:

  • argc:參數個數
  • argv:參數數組,每一個參數都是字符串
  • env:環境變量數組,每一個環境變量都是字符串

進程基本操做

注意,Shell 終端沒法檢測進程是否建立了子進程。在進程執行完畢後,Shell 會當即回到交互狀態,此時若是子進程還在輸出數據,會打印在 Shell 的命令提示符以後。能夠在父進程中 sleep 一下。

建立進程

Linux 中用 fork() 函數建立新進程,函數原型以下:

#include<unistd.h>

pid_t fork(void);

返回值:
成功建立進程時,會對父子進程各返回一次,對父進程返回子進程的 PID,對子進程返回 0。經過條件分支語句能夠分別進行不一樣處理。失敗則返回小於 1 的錯誤碼。

fork 函數執行的時候,會將當前正在運行的進程完整的複製一份,提供給子進程。子進程從 fork 函數以後開始執行

建立多個子進程

經過 for 循環,能夠建立多個子進程,只須要在每次 fork 以後判斷若是是子進程則結束循環便可。經過循環下標能夠判斷當前子進程是第幾回建立的:

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

int main()
{
	int i = 0;
	int number = 5;
	pid_t pid;
	
	for (i = 0; i < number; i++)
	{
		pid = fork();
		if (pid == 0) break;
	}
	
	// 子進程
	if (i == 0) printf("first process, pid = %d\n", getpid());
	if (i == 1) printf("second process, pid = %d\n", getpid());
	//...
	
	// 父進程
	if (i == number) printf("parent process, pid = %d\n", getpid());
	return 0;
}

終止進程

進程的終止分爲兩種:

  • 正常終止:
    • 從 main() 函數中 return 返回,實際上也是調用的 exit() 函數
    • 調用 exit() 類函數
  • 異常終止:
    • 調用 abort() 函數
    • 接收到終止信號

exit 函數原型以下:

#include <stdlib.h>

void exit(int status);

exit(0) 等價於 return 0

exec 函數組

fork 函數能夠複製一份父進程,獲得的子進程跟父進程有徹底同樣的代碼跟數據。以後兩個進程各自執行,互不影響。

實際上咱們一般須要子進程執行不一樣的代碼,這時就須要經過 exec 函數加載代碼段,並跳轉到新代碼段的 main 入口執行。

函數原型:

#include <unistd.h>

extern char **environ;

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 execvpe(const char *file, char *const argv[], char *const envp[]);

exec 函數組是在 exec 上加 l、v、p、e 四個後綴造成的,這些函數做用相同,只是在參數列表上存在差異。

  • 後綴 p:用文件名作參數,若是文件名中不含路徑則會去 PATH 環境變量所指定的各個目錄中搜索可執行文件。無後綴 p 則使用絕對路徑名來指定可執行文件的位置。
  • 後綴 e:表示能夠傳遞一個指向環境字符串指針數組的指針,環境數組須要以 NULL 結束。而無此後綴的函數則使用調用進程中 environ 變量爲新程序複製現有的環境。
  • 後綴 l:用 list 形式來傳遞新程序的參數,傳給新程序的全部參數以可變參數的形式傳遞,最後一個參數必須是 NULL。
  • 後綴 v:用 vector 形式來傳遞新程序的參數,傳給新程序的全部參數放入一個字
    符串數組中,數組以 NULL 結束。

返回值:exec 族函數報錯時纔有返回值 -1,不然無返回值。若是執行到後面的代碼,就是出錯了。

示例:

char* myenv[] = {"TEST=666", "HOME=/home/kika", NULL};
execle("home/kika/test", "hello", "world", myenv);

完整示例:

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

int main()
{
	int pid = fork();
	if (pid > 0) {
		exit(0);
	} else if (pid == 0) {
		execle("home/kika/test", "hello", "world", myenv);
		perror("execle error");
		exit(-1);
	} else {
		perror("fork error");
	}
	return -1;
}

wait 和 waitpid 函數

一般,在父進程中調用 wait 函數,能夠查看子進程的退出信息,讓子進程撤單結束。wait 函數是阻塞式的,waitpid 能夠設置爲非阻塞的。父進程根據建立的子進程個數,在循環中經過 wait 函數逐個回收子進程。而 waitpid 函數則能夠經過 PID 等待指定的子進程退出。

wait 函數調用一次只會回收一個子進程。多個子進程須要調用屢次 wait。

函數原型:

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

wait 參數:

  • status:進程退出的緣由。能夠經過預約義的宏來檢查:
    • WIFEXITED(status):若爲正常終止子進程返回狀態,則爲真
    • WEXITSTATUS(status):獲取子進程傳給 exit 或 return 的參數
    • WIFSIGNALED(status):若爲異常終止子進程返回狀態,則爲真
    • WTERMSIG(status):獲取使子進程退出的信號

waitpid 參數:

  • pid:用於設置 waitpid 工做方式。
    • -1:等價於 wait,等待任意子進程退出
    • 0:等待組 ID 等於調用進程的組 ID 的任一子進程退出
    • > 0:等待 PID 等於該數值的進程退出
    • < -1:等待其組 ID 等於該數值的任一子進程退出
  • options:用於設置 waitpid 是否阻塞執行。0 則阻塞,設爲 WNOHANG 則非阻塞。

返回值:成功時返回退出子進程的 PID,沒有子進程時返回 -1.

綜合示例

建立三個子進程,分別運行自定義程序,shell 程序,未定義程序(段錯誤)。而後在父進程中經過 wait 回收全部子進程,並分別判斷退出緣由:

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <errno.h>

int main(int argc, char* argv[])
{
    int num = 3;
    int i;
    pid_t pid;
    for (i = 0; i < 3; i++) {
        pid = fork();
        if (pid == 0) break;
    }
    
    if (i == 0) {
        execlp("ps", "ps", "aux", NULL);
        perror("execlp ps error");
        exit(1);
    } else if (i == 1) {
        execl("/root/test/api/process/myls", "", NULL);
        perror("execl myls error");
        exit(1);
    } else if (i == 2) {
        execl("./error", "", NULL);
        perror("execl ./error");
        exit(1);
    } else {
        int status;
        pid_t pid;
        while (pid = wait(&status) != -1) {
            printf("children PID is: %d\n", pid);
            if (WIFEXITED(status)) {
                printf("return value is: %d\n", WEXITSTATUS(status));
            } else if (WIFSIGNALED(status)) {
                printf("died by signal: %d\n", WTERMSIG(status));
            }
        }
    }
    return 0;
}
相關文章
相關標籤/搜索