歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> Unix 環境編程: 進程控制

Unix 環境編程: 進程控制

日期:2017/3/1 11:57:24   编辑:關於Linux

進程是程序在內存中運行的一個實例,這裡說出了進程與程序的一個區別就是進程是在內存空間裡的,程序實際上是存儲在你的硬盤上的那個文件,又可以叫做可執行文件。

1. 如何標識一個進程

1)當然是用一個ID來標識一個進程啦,在Linux系統上貌似任何東西的標識都是使用一個整數也就是ID,進程當然也不例外,那麼一個進程除了PID 以外還有哪些標識呢?

-->PID : 最常用的就是 process ID

-->PPID : 所有的進程都有一個父進程 Parent Process ID

-->UID : 用戶ID,誰運行的這個進程

-->EUID : 有效用戶ID,雖然一個進程是由用戶A執行起來的,但是進程擁有的權限卻可以與A不同, 這個EUID是跟可執行文件的S權限息息相關的

-->GID : 組ID

-->EGID :有效組ID

PID與PPID很直白很容易理解, UID/GID也還可以,畢竟進程都是由用戶執行起來的嘛, 但是EUID?EGID是什麼鬼?關於這兩個ID,大家可以使用系統調用getUID() getEUID(),來觀察一下,EUID與UID一般情況下是一樣的,那麼什麼情況下會不一樣呢 ?那就是當設置可執行文件的SUID這個權限的時候,也是使用了chmod u+s XXX。

SUID又是什麼鬼? OK,看下面的實驗吧

首先寫一個測試程序

#include 
#include 

int main()
{
    uid_t uid = getuid();
    uid_t euid = geteuid();

    printf("uid = %d, euid = %d\n", uid, euid);

    return 0;
}

gcc test.c -o test 之後得到 可執行文件 : test

ls -l
-rwxrwxr-x 1 gengj gengj 13451  8月 22 10:25 test

chown root test
chmod u+x test
ls -l 
-rwsrwxr-x 1 root  gengj 13451  8月 22 10:25 test
看到沒有, 剛開始 test 文件的 用戶和組都是gengj, 這個時候執行test,得到 uid == euid,但是當執行了後邊的操作也就是設置了S權限之後(這個時候實際上test是以root權限來運行的,也就是它的有效用戶是 root),再執行,可以看到euid變成了 0, UID沒有改變。關於GID/EGID與上面的描述是一樣的。

2)現在我們知道了如何獲得關於進程的各種ID,那麼怎麼對這些ID進程設置呢?

實際上PPID是沒有辦法改變的,PID是內核分配的,這兩個ID是不能改變的,也沒有必要改變。可以改變的是UID EUID GID EGID, 這裡我們不討論組只討論用戶 ID, 因為他們的操作是一樣的。

設置UID我們可以使用系統調用 setuid(uid_t uid); 關於這個函數需要說明一下:

-->如果進程擁有root權限的話,該進程調用setuid(uid) 可以把 本進程的UID, EUID, saved set-uid全部設置成參數 uid;

-->如果進程不是root權限,參數uid==UID 或者 uid == SUID的話, 本函數將會把EUID設置成uid,其他的ID都不變。

各種ID搞得人比較暈,我也不是特別的清楚, 不過只要記得不同的UID會使得進程擁有不同的權限,如果設置了EUID, 則表示了進程的實際可以擁有的權限。

2. 創建新的進程 fork

1)系統調用 pid_t fork(void) 是用來創建一個新的子進程的, 這個函數比較特殊,一次調用會返回兩次:

#include 
#include 

int main()
{
    pid_t pid;

    int i = 0;

    if ((pid = fork()) < 0) {
        printf("fork failed\n");
        return -1;
    } else if (pid == 0) { // this is child process
        printf("this is in child process\n");
        i++;
    } else {               //this is in parent process
        sleep(2);
        printf("this is in parent process\n");
    }

    //this is in bothprocess

    printf("i = %d\n", i);

    return 0;
}

this is in child process i = 1 this is in parent process i = 0

this is in child process
i = 1
this is in parent process
i = 0

輸出: 

-------------------------------------------------------------------------------------------

this is in child process
i = 1
this is in parent process
i = 0

this is in child process
i = 1
this is in parent process
i = 0
this is in child process
i = 1
this is in parent process
i = 0
this is in child process

i = 1

...(sleep 2)

this is in parent process

i = 0

-------------------------------------------------------------------------------------------

如上顯示的, fork返回的pid如果為0 則表示這是從子進程返回的值,如果大於 0則表示是從父進程返回的值,返回值就是子進程的PID。

如上顯示的,fork之後,父子進程都會繼續執行接下來的代碼,也就是說父子進程共享一個代碼段 (text segment),但是數據是不一樣的,因為在子進程對 i 賦值並不會影響父進程中的 i,說明,父子進程的數據段是不一樣的,實際上子進程復制了父進程的數據段,所以導致父進程與子進程都有一個數據 i 的copy,各自的改變並不會影響另一個進程。

2) fork中的文件描述符

父進程中打開的文件描述符將在子進程中同樣有效,也就是說父子進程共享文件描述符。這種共享會導致父子進程產生競爭現象,需要在編程中避免這種競爭。

3)子進程繼承的東東與沒有繼承的東東

先看看有哪些東西沒有被子進程繼承吧:

-- PID & PPID

-- time (tms_utime, tms_stime) 在子進程中被置 0

-- 文件鎖 在子進程中沒有

-- 定時器

-- 信號集

被繼承下來的東西:

-- UID , GID

-- 進程組

-- 會話(session) ID

-- 控制終端

-- set-user-id/ set-group-id

-- root directory

-- current work directory

-- 文件權限掩碼 mask

-- 信號掩碼

-- close-on-exec 標志

-- 環境變量

-- 共享內存

-- 內存映射

4) fork為什麼會失敗

fork失敗意味著系統不能生成新的進程啦,這有可能是由於系統資源不足造成的, 也有可能是一個real user id 的進程數超出了限制造成的。

5)fork 與 exec函數

fork會產生一個新的進程,子進程會有父進程地址空間的一份copy, 但是如果在fork之後的子進程中調用exec函數,那麼子進程將會被新的程序替代,子進程將會從新程序的main函數開始執行。

這裡替換的意思不是創建一個新的進程,而是繼續在原來的地址空間裡面繼續執行,只不過代碼段,數據段,堆棧全部被替換啦,而且當新程序結束後也不再返回到子進程中啦。既然是在原子進程空間裡面運行新的程序,PID當然還是不會改變的啦。

#include 
#include 

int main()
{
        pid_t pid;

        if ((pid = fork()) < 0) {
                printf("fork failed\n");
                return -1;
        } else if (pid == 0) { // this is child process
                printf("this is in child process\n");
                printf("exec a new program\n");
                execlp("ls", "ls", "-1", NULL);              // run another program

                printf(" I am back into child process\n");   // never come back to here
        } else {
                sleep(2);
                printf("this is in parent process\n");
                global_num++;
        }

        //this is in bothprocess
        
        printf("i = %d, global_num = %d\n", i, global_num);

        return 0;
}

講到了exec 函數,有必要提一下另外一個系統調用system(), 實際上system()函數是用exec函數來實現的,只不過多了一些錯誤檢查。這個函數用於在我們的程序中執行另外的可執行文件,直到結束,也就是說它是阻塞的,一個簡單的system實現如下.

parent-process --> fork --> exec

|-----------------------------waitpid()

3 進程結束

結束一個進程有很多種方法:

正常的退出

-- main函數中調用 return 或者 exit(),這兩者是等價的

-- 調用 _exit 或者 _Exit

不正常退出

-- 調用abort, 將會產生SIGABRT信號

-- 接收到特定的信號, 比如運行中我們按下ctl-c也就是發送了TERM 信號給進程

不正常的退出現在先不講,只討論一下正常的退出中的exit _exit _Exit

1) exit 與return一樣是一種比較安全溫和的退出方式,將會flush 所有IO然後關閉所有的文件, 依次調用注冊的at_exit 函數, 最後退出

2)_exit 與_Exit這是一種簡單粗暴的退出方式,不會調用at_exit注冊的函數,也可能不會flush IO (it depends on OS implementation)。

當子進程正常退出時,父進程可以獲得子進程的退出狀態值,退出狀態值是exit函數的參數,當子進程非正常退出時,內核來把退出狀態值送給父進程,總之,父進程總是可以獲得子進程的退出狀態值。 這裡我們考慮以下兩種情況

1)父進程先於子進程退出

如果父進程比子進程先退出,那麼init將會成為子進程的父進程,這是因為Linux要確保每一個進程都有一個父進程

2)子進程先於父進程退出

如果子進程先退出啦,那麼內核會在內存中維護這個子進程的一些信息,以便父進程在調用wait或者waitpid時能夠獲得子進程的退出狀態,一般來講內核保存的信息必然會包括PID和退出狀態值, 因為這些都是wait函數需要的。

子進程退出後,如果父進程調用了wait或者waitpid,那麼內核就不需要在保存這些殘留信息了,那麼這個子進程就完全退出了。如果父進程不調用wait&waitpid,那麼這個子進程就變成了僵屍進程(zombie)。

是時候說說wait & waitpid函數了,這兩個函數都會使得內核對子進程進行清理,也就是消除zombie。 我們先來講講這兩個函數的特點吧

1)wait: 阻塞的,直到有一個子進程退出他才返回,實際上任何一個子進程的退出都會導致wait返回,也就是說,如果有很多子進程就需要調用很多次wait;如果沒有子進程的話,wait也會立即返回,只不過是返回error。

2) waitpid: 相對於wait,waitpid比較人性化,他會等待特定的有其參數指定的進程退出。他可以是阻塞的也可以通過其參數配置成非阻塞的。haha, 可以配置的這個特性不錯,大家可以試試看,這裡不多說。

總結

這篇文章講述了進程的創建fork exec, 進程的退出 exit, 如何避免僵屍進程waitpid,並給了簡單的demo來說明fork等的應用。本文並沒有講述在什麼情形中使用子進程這個特性,算是不足之處,以後盡量補上。

歡迎閱讀提問,如果有不對的地方,請指正。

Copyright © Linux教程網 All Rights Reserved