天天看點

unix程序控制及程序環境--自APUE概述程序終止環境變量程序堆空間申請和釋放程序ID子程序執行新的程式

文章目錄

  • 概述
    • 1、什麼是程序
    • 2、孤兒程序和僵屍程序
    • 3、程序的分裂生長模式
  • 程序終止
    • 程序的編譯和啟動
    • 程序終止的步驟
    • 程序8種終止方式
    • 程序退出函數1:exit
    • 程序退出函數2:_exit
    • 程序退出函數3:_Exit
    • 注冊終止處理程式:atexit
  • 環境變量
    • 通過main函數傳參
    • 全局的環境變量表:environ
    • 擷取環境變量:getenv
    • 修改環境變量:putenv
    • 修改環境變量:setenv
  • 程序堆空間申請和釋放
    • 申請指定大小的記憶體:malloc
    • 申請初始化的記憶體:calloc
    • 修改已申請記憶體大小:realloc
  • 程序ID
    • 擷取程序ID:getpid
    • 擷取父程序ID:getppid
  • 子程序
    • 建立子程序:fork
    • 建立子程序:vfork
    • 回收子程序:wait
    • 回收一個特定子程序:waitpid
  • 執行新的程式
    • 指定參數和環境變量:execve
    • 以清單方式傳參:execl
    • 以向量方式傳參:execv
    • 以清單方式傳參并傳遞環境變量:execle
    • 特定執行順序:execlp、execvp、execvpe
    • 簡單執行shell指令:system

概述

1、什麼是程序

程序是程式的一次運作,是一個動态過程。

程序控制塊PCB:核心中專門用于管理程序的資料結構,裡面記錄了程序的各種資訊。

2、孤兒程序和僵屍程序

  • 僵屍程序

一個程序有兩類資源,一個是運作時候向核心申請的資源,包括IO資源(比如open檔案産生的IO資源)、記憶體資源(malloc産生的)等;第二個是程序自帶的用于描述程序自身的資源(占8KB),包括描述程序相關資訊的資料結構task_struct和棧記憶體,這類資料是程序建立之初就生成的。

一個程序退出後,核心會回收程序的運作時資源,但是8KB記憶體作業系統不會去回收,隻能由其父程序去回收(誰建立誰負責回收的原則),這些資源被父程序通過wait函數擷取後被釋放,如果一個子程序退出後,父程序沒有wait這些資訊,那麼這個子程序就變成了僵屍程序。

防止程序變成僵屍程序的幾種方式:

  1. 父程序顯示通過wait回收子程序的資源
  2. 父程序退出的時候會隐式的去回收子程序的資源
  3. 子程序退出時,核心會向父程序發送SIGCHILD,父程序處理該信号的時候通過wait擷取退出資訊
  4. 讓程序被程序1接管,程序1會wait每一個退出的子程序
  • 孤兒程序

父程序退出後,子程序還沒有退出,那麼子程序就會被程序1接管變成孤兒程序。是以子程序可以通過下面的指令來判斷父程序是否退出了:

3、程序的分裂生長模式

核心加載一個程序的時候會有很多前置工作,如果建立一個新程序的時候,從頭做一遍,效率會很低,是以核心采用了一種方法:把父程序的内容先原封不動的複制一份,然後做一些修改,比如修改pid号等,這樣就能大大提高新程序建立的效率。

程序終止

程序的編譯和啟動

程序的編譯連結:裸機程式設計需要寫連結腳本,但是linux下程式設計就不用了,因為每個程序的連結方式都是固定的,gcc會自動把事先準備好的引導代碼連結到main函數的前面,這段代碼每個程式都是一樣的。

程序的加載:程序運作的時候,加載器會把程序加載到記憶體中,然後去執行。

程序編譯的時候使用連結器,運作的時候使用加載器

argc和argv的傳參:shell下執行程序的時候,這倆參數首先被shell解析,然後傳遞給加載器,最後通過main函數傳遞給程序,是以我們在main函數中能使用這兩個參數。

程序終止的步驟

unix程式控制及程式環境--自APUE概述程式終止環境變量程式堆空間申請和釋放程式ID子程式執行新的程式

程序啟動的時候,核心會通過exec打開一個啟動例程,這個啟動例程通過下面的函數執行目标的程序,如果目标程序在main函數中return 0的時候,就相當于直接執行了exit(0),是以return之後執行的步驟和exit一樣;

程序終止的幾種場景如下:

  • 如果功能函數調用return:那麼傳回main函數;
  • 如果功能函數調用_exit或_Exit:那麼直接進入到核心
  • 如果功能函數調用exit:那麼執行終止處理函數、清理IO、删除臨時檔案後進入核心
  • 如果main函數調用return:那麼傳回啟動例程,然後啟動例程會調用exit,進入exit處理流程後進入核心
  • 如果main函數調用_exit或_Exit:同功能函數
  • 如果main函數調用exit:同功能函數

程序8種終止方式

正常終止方式:

  1. 從main傳回
  2. 調用exit
  3. 調用_exit或_Exit
  4. 最後一個線程 從其啟動例程傳回
  5. 最後一個線程調用thread_exit

異常終止:

  1. 調用abort
  2. 接到一個信号
  3. 最後一個線程對取消請求做出響應

程序退出函數1:exit

這是一種程序正常退出的庫函數,通過man 3 exit可以檢視,exit函數沒有傳回值,會傳回status狀态碼給調用他的父程序。

程序調用exit時候,會執行以下步驟:

  1. 先調用終止處理程式。終止處理程式通過atexit函數注冊,一個程序最多注冊32個。調用的順序和注冊的順序相反。終止處理程式沒注冊一次會被調用一次,盡管是相同的函數注冊了多次
  2. 所有打開的标準IO流會被flush和close
  3. 通過tmpfile建立的臨時檔案會被删除
#include <stdlib.h>
void exit(int status);
status:給父程序的傳回碼
           

程序退出函數2:_exit

_exit是一個系統調用,作用也是退出程式,但是不會去執行終止處理函數、清理IO、删除臨時檔案等步驟,直接進入到核心:

  1. 所有的檔案描述符會被直接關閉
  2. 然後所有的子程序被程序1接管
  3. 給父程序傳遞SIGCHLD信号
#include <unistd.h>
void _exit(int status);
status:給父程序的傳回碼
           

程序退出函數3:_Exit

同_exit

#include <stdlib.h>
void _Exit(int status);
           

注冊終止處理程式:atexit

#include <stdlib.h>
int atexit(void (*function)(void));
傳回值:成功,傳回0,失敗傳回非0數字。
           

示例代碼:

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

static void test1(void)
{
        printf("test1\n");
}

static void test2(void)
{
        printf("test2\n");

}

int
main(int argc, char **argv)
{
        if (atexit(test1) != 0) {
                printf("atexist test1 failed.\n");
        }
        if (atexit(test2) != 0) {
                printf("atexist test2 failed.\n");
        }

        exit(1);
}
           

執行結果:

[[email protected] exit]# ./atexit 
test2
test1
           

環境變量

通過main函數傳參

環境變量可以通過main函數直接傳參進來,需要main函數按照以下格式定義。這種方式其實就是把全局環境變量表environ的位址傳進來。

#include <stdio.h>
int
main(int argc, char **argv, char **envp)
{
        int i = 0;
        for (i = 0; i < argc; i++){
                printf("%s\n", argv[i]);
        }
        i = 0;
        while(envp[i]) {
                printf("%s\n", envp[i]);
                i++;
        }
        return 0;
}
           

全局的環境變量表:environ

程序打開之後會有一個預設的環境變量表,這個環境變量表是shell環境變量的一份拷貝,通過一個全局的指針數組可以通路到表裡的内容:

#include <unistd.h>
extern char **environ;
           

擷取環境變量表的示例代碼如下,擷取的結果和shell 指令env的結果基本一緻:

#include <unistd.h>
#include <stdio.h>
extern char **environ;
int
main(int argc, char **argv)
{
        int i = 0;
        while(environ[i] != NULL) {
                printf("%d\t%s\n", i+1, environ[i]);
                i++;
        }
        return 0;
}
           

運作結果如下:

[[email protected] getenv]# vim environ.c ^C
[[email protected] getenv]# ./environ 
1       XDG_SESSION_ID=17
2       HOSTNAME=localhost.localdomain
3       RTE_INCLUDE=/usr/include/dpdk
4       TERM=xterm
5       SHELL=/bin/bash
6       HISTSIZE=1000
           

擷取環境變量:getenv

環境變量都是key=value的格式,getenv在環境變量表中,查找key對應的value,傳回指向value的指針(不會帶上"key="),如果找不到傳回NULL。

#include <stdlib.h>
char *getenv(const char *name);
name:環境變量的key;
成功,傳回指向value指針,失敗或者找不到傳回NULL;
           

修改環境變量:putenv

作用如下:

  1. 如果環境變量不存在則添加環境變量
  2. 如果環境變量已經存在,那麼把環境變量的值設定成最新的值。

注意事項:

  1. putenv了之後,string位址會添加到environ表中。
  2. putenv了之後,如果修改了string的内容,那麼環境變量也會對應被修改。
  3. 環境變量的修改隻會影響目前程序和子程序的環境變量表,對父程序無效
#include <stdlib.h>
int putenv(char *string);
string:必須是"key=value"的格式;
傳回值:成功傳回0,失敗傳回非0,并且置上errno;
           

代碼示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
extern char **environ;
int
main(int argc, char **argv)
{
        char buf[256] = "name=xiaoming";
        int i = 0;
        if (0 != putenv(buf)) {
                perror("putenv failed.\n");
                return 0;
        }
        printf("%s=%s\n", "name", getenv("name"));
        strcpy(buf, "name=xiaowang");
        printf("%s=%s\n", "name", getenv("name"));
        while(environ[i]) {
                printf("%s\n", environ[i]);
                i++;
        }
        return 0;
}
           

輸出結果:

name=xiaowang
           

修改環境變量:setenv

#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
作用:把name=value的環境變量加入到環境變量表中;
overwrite:當overwrite為0,如果環境變量已經存在,那麼不會覆寫掉原來的值,如果環境變量不存在,則添加環境變量;當overwrite為非0,如果環境變量已經存在,那麼覆寫掉原來的值,如果環境變量不存在,則添加環境變量;
傳回值:成功,傳回0,失敗傳回-1,并且置上errno;
           

删除環境變量

程序堆空間申請和釋放

申請指定大小的記憶體:malloc

malloc用于申請指定大小的記憶體,傳回指針指向申請的記憶體。如果size的值為0,傳回值是NULL或者是一個特定值得指針,這個指針可以作為free函數的參數被釋放而不會報錯。

#include <stdlib.h>
void *malloc(size_t size);
size:申請記憶體以位元組為機關的大小;
傳回指向新申請記憶體的指針
           

申請初始化的記憶體:calloc

calloc用于申請一段指定大小、指定數目的記憶體,該記憶體會被初始化成0,如果大小和數目為0,傳回值是NULL或者是一個特定值得指針,這個指針可以作為free函數的參數被釋放而不會報錯。

#include <stdlib.h>
void *calloc(size_t nmemb, size_t size);
nmemb:記憶體數目;
size:記憶體大小;
傳回新申請記憶體的指針
           

修改已申請記憶體大小:realloc

realloc用于修改已申請記憶體的大小,ptr指向修改前的記憶體,size設定修改後的記憶體大小,可以改大也可以改小,大體分為以下幾個場景:

  1. 如果改小:那麼修改前的記憶體的起始位址到size大小的範圍内的資料不會被修改,傳回指向修改前的記憶體指針
  2. 如果改大,将在原來堆位址繼續往高位址空間擴充,有兩種可能,1)一是空間連續且足夠,那麼傳回原來的空間位址,注意新增加的記憶體不會被初始化;2)二是連續的空間不夠,那麼尋找新的位址空間,并将原來的資料轉移到新的空間中,原來的記憶體會被自動釋放掉,傳回新的空間位址

ptr指針和size之間的組合關系大體分為以下幾種情況:

  1. ptr等于NULL:realloc等效于malloc()
  2. ptr不等于NULL:ptr必須是通過malloc(), calloc(), realloc()配置設定過的
  3. size等于0:realloc等效于free()
#include <stdlib.h>
void *realloc(void *ptr, size_t size);
ptr:NULL或者指向修改前的記憶體;
size:修改後的記憶體大小;
傳回值:修改成功傳回新的記憶體位址,修改失敗傳回NULL,此時原來的ptr還可以繼續用,資料也不會被修改;
           

程序ID

實作原理就是從程序控制塊PCB中吧對應的資料結構擷取出來

擷取程序ID:getpid

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
傳回值: 這個函數永遠傳回成功,傳回調用程序的ID;
           

擷取父程序ID:getppid

#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
傳回值: 這個函數永遠傳回成功,傳回調用程序的父程序ID;
           

子程序

建立子程序:fork

fork()系統調用用于建立一個子程序,子程序和父程序之間的關系為:

  • 子程序對資料、堆、棧都複制了一份副本,子程序有獨立的PCB,父子程序對資料的通路互相不影響,
  • 子程序複制父程序檔案描述符,但是指向同一個檔案表項,共享檔案偏移量,是以父子程序寫同一個檔案會互相影響,并且父程序或子程序return了之後,fd對應的檔案會被關閉,會影響到另一個程序對檔案的通路。
  • 父子程序傳回值不同
  • 父子程序ID不同
  • 子程序不繼承父程序的記憶體鎖和記錄鎖
  • 子程序不繼承父程序的定時器
  • 子程序的signal會被清空
  • 父程序退出,子程序未退出,子程序被init程序收養
  • 子程序退出,其資訊未被父程序通過wait函數收集,子程序會變成僵死程序
  • 子程序加入系統程序排程,且與父程序獨立,是以子程序和父程序的運作順序是随機的
#include <unistd.h>
pid_t fork(void);
傳回值:父程序傳回子程序的程序ID,子程序傳回0;如果建立失敗,父程序傳回-1,并且置上errno,沒有子程序被建立;
           

建立子程序:vfork

vfork也是建立一個子程序,和fork之間的差別在于:

  1. 子程序和父程序之間共享記憶體資料,包括代碼段、資料段、堆、棧等,建立子程序的效率比fork高
  2. 子程序先執行,父程序會卡主,直到子程序調用了exit或execve(不能是調用return),父程序繼續運作

應用場景:很多時候建立子程序隻是為了執行exec,這種場景下,沒必要對父程序所有的資料都進行複制,用vfork效率會更高。

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
傳回值:父程序傳回子程序的程序ID,子程序傳回0;如果建立失敗,父程序傳回-1,并且置上errno,沒有子程序被建立;
           

回收子程序:wait

wait是一種系統調用,用于父程序探測子程序的狀态變化。子程序退出的時候,核心還保留資料結構儲存退出狀态,當父程序調用wait,如果此時已經有子程序退出,那麼立即傳回,如果沒有,父程序會阻塞在wait調用上,直到至少一個子程序退出(核心給父程序發送SIGCHILD信号),然後系統會把子程序的資源徹底釋放。如果父程序沒有子程序,那麼會立即報錯傳回。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
傳回值: 成功傳回子程序的ID号,失敗傳回-1,置上errno;
           

回收一個特定子程序:waitpid

waitpid用于等待特定的子程序,并且可以設定阻塞還是非阻塞。

#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
pid:
	pid < -1: 那麼等待程序組ID等于pid絕對值的程序組中的程序;
	pid = -1: 等待所有的子程序;
	pid = 0: 等待程序組ID等于父程序組ID的程序組中的程序;
	pid > 0: 等待程序ID等于pid的子程序
options:
	WNOHANG: 正常waitpid的時候父程序會hang住,用這個參數,如果沒有子程序退出,立即傳回0;
	...
status:輸出型參數,如果status非NULL,那麼會傳回子程序的狀态,通過一些宏可以獲得狀态;
	WIFEXITED(*status): 子程序正常退出,傳回true;
	WEXITSTATUS(*status): 如果子程序正常退出的話,傳回傳回碼;
	WIFSIGNALED(*status): 子程序被信号中斷退出,傳回true;
	WTERMSIG(*status): 中斷子程序的信号ID;
	WCOREDUMP(*status): 子程序coredump了,傳回true,同時WIFSIGNALED也傳回true;
	...
傳回值: 成功,傳回子程序的ID号;失敗,傳回-1,置上errno;如果子程序ID不存在,或者存在但是非該程序的子程序,傳回-1,并且置上errno;如果子程序還沒結束,并且使用非阻塞模式,那麼傳回0.
           

執行新的程式

exec函數族用于執行一個新的程式代替目前程式。函數包括execl、execv、 execle、execve、execlp、execvp、execvp等,底層都是使用execve系統調用。這幾個函數命名方式有一定規律,v表示向量,就是用二維指針來傳遞參數,l表示list,就是用多個指針傳遞參數,e表示傳遞環境變量,p表示尋找可執行檔案的順序和shell一樣。

指定參數和環境變量:execve

execve系統調用用于執行filename指向的可執行程式或者shell腳本,腳本可以被執行有兩個條件:

  1. 腳本具有可執行權限
  2. 腳本開頭必須指定解釋器,比如:#!/bin/bash,否則會報 Exec format error

執行execve之後,目前程式的代碼段、資料段、bss段、棧等資訊會被filename指向的程式覆寫,是以沒有傳回值同時被執行程式的程序ID和目前程式的ID相同。

#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
filename: 被執行的程式;
argv: 傳遞給被執行程式的參數,以NULL結尾;
envp: 傳遞給被執行程式的環境變量(key=value的格式),以NULL結尾;
如果main函數的定義為:int main(int argc, char *argv[], char *envp[]);那麼可以argv和envp就指向execve中的argv和envp,注意,如果filename=/usr/bin/ls, argv={"-l", NULL},那麼ls執行的時候argv[0]="-l",這個和直接執行ls -l不同,可能會影響到傳參的解析,是以最好argv={"ls", "-l", NULL};
傳回值: 成功不傳回,失敗傳回-1,并且置上errno。
           

以清單方式傳參:execl

execl函數使用清單方式傳參,最後一個參數之後以NULL結尾,就是每個參數使用一個指針。不用傳遞環境變量,預設使用目前程式的環境變量。

#include <unistd.h>
int execl(const char *path, const char *arg, ...);
path: 被執行程式;
arg: 指向單個參數的指針;
傳回值:同execve;
           

代碼示例:

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

int
main(int argc, char **argv)
{
        if (argc != 2){
                printf("usage: %s filename\n", argv[0]);
                return 0;
        }
        char a[] = "hello";
        char b[] = "world";
        execl(argv[1], a, b, NULL);
        return 0;
}
           

以向量方式傳參:execv

execv函數使用向量方式傳參,預設使用目前程式的環境變量。

#include <unistd.h>
int execv(const char *path, char *const argv[]);
path: 被執行程式;
argv: 指向參數的二維指針;
傳回值:同execve;
           

以清單方式傳參并傳遞環境變量:execle

#include <unistd.h>
int execle(const char *path, const char *arg, ..., char * const envp[]);
path: 被執行程式;
arg: 指向單個參數的指針;
envp: 指向環境變量的指針;
傳回值:同execve;
           

代碼示例

#include <unistd.h>
#include <stdio.h>
int
main(int argc, char **argv)
{
        if (argc != 2){
                printf("usage: %s filename\n", argv[0]);
                return 0;
        }
        char a[] = "hello";
        char b[] = "world";
        char *c[] = {"name=xiaoming", "age=18", NULL};
        int ret;
        ret = execle(argv[1], a, b, NULL, c);
        if (ret == -1) {
                perror("execle: ");
        }
        return 0;
}
           

特定執行順序:execlp、execvp、execvpe

execlp、execvp、execvpe函數的功能分别和execl、execv、execve相同,不同點在于:

  1. 參考shell尋找可執行檔案的邏輯
  2. 如果不是絕對路徑,先從PATH環境變量中找,找不到就從目前目錄下尋找。
  3. 如果PATH路徑下程式找到了,但是沒有可執行權限,那麼繼續從下一級目錄下尋找
  4. 如果被執行的程式是shell腳本,但是沒有解釋器,比如:#!/bin/bash,那麼會預設使用/bin/sh來解釋

簡單執行shell指令:system

system庫函數用于執行一個shell指令,也可以是一個可執行程式,不需要傳參,被執行程式的環境變量和調用程式相同。system的底層邏輯為:fork一個子程序----子程序exec一個shell—父程序waitpid子程序執行結束—傳回status

#include <stdlib.h>
int system(const char *command);
command: 被執行的程式;
傳回值: 如果建立子程序失敗,傳回-1;如果sh沒有被執行,比如權限不足,傳回127;如果執行成功,傳回wait(*status)中的status,通過WEXITSTATUS(status)擷取command指令的傳回碼。
           

繼續閱讀