文章目錄
- 概述
-
- 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這些資訊,那麼這個子程序就變成了僵屍程序。
防止程序變成僵屍程序的幾種方式:
- 父程序顯示通過wait回收子程序的資源
- 父程序退出的時候會隐式的去回收子程序的資源
- 子程序退出時,核心會向父程序發送SIGCHILD,父程序處理該信号的時候通過wait擷取退出資訊
- 讓程序被程序1接管,程序1會wait每一個退出的子程序
- 孤兒程序
父程序退出後,子程序還沒有退出,那麼子程序就會被程序1接管變成孤兒程序。是以子程序可以通過下面的指令來判斷父程序是否退出了:
3、程序的分裂生長模式
核心加載一個程序的時候會有很多前置工作,如果建立一個新程序的時候,從頭做一遍,效率會很低,是以核心采用了一種方法:把父程序的内容先原封不動的複制一份,然後做一些修改,比如修改pid号等,這樣就能大大提高新程序建立的效率。
程序終止
程序的編譯和啟動
程序的編譯連結:裸機程式設計需要寫連結腳本,但是linux下程式設計就不用了,因為每個程序的連結方式都是固定的,gcc會自動把事先準備好的引導代碼連結到main函數的前面,這段代碼每個程式都是一樣的。
程序的加載:程序運作的時候,加載器會把程序加載到記憶體中,然後去執行。
程序編譯的時候使用連結器,運作的時候使用加載器
argc和argv的傳參:shell下執行程序的時候,這倆參數首先被shell解析,然後傳遞給加載器,最後通過main函數傳遞給程序,是以我們在main函數中能使用這兩個參數。
程序終止的步驟

程序啟動的時候,核心會通過exec打開一個啟動例程,這個啟動例程通過下面的函數執行目标的程序,如果目标程序在main函數中return 0的時候,就相當于直接執行了exit(0),是以return之後執行的步驟和exit一樣;
程序終止的幾種場景如下:
- 如果功能函數調用return:那麼傳回main函數;
- 如果功能函數調用_exit或_Exit:那麼直接進入到核心
- 如果功能函數調用exit:那麼執行終止處理函數、清理IO、删除臨時檔案後進入核心
- 如果main函數調用return:那麼傳回啟動例程,然後啟動例程會調用exit,進入exit處理流程後進入核心
- 如果main函數調用_exit或_Exit:同功能函數
- 如果main函數調用exit:同功能函數
程序8種終止方式
正常終止方式:
- 從main傳回
- 調用exit
- 調用_exit或_Exit
- 最後一個線程 從其啟動例程傳回
- 最後一個線程調用thread_exit
異常終止:
- 調用abort
- 接到一個信号
- 最後一個線程對取消請求做出響應
程序退出函數1:exit
這是一種程序正常退出的庫函數,通過man 3 exit可以檢視,exit函數沒有傳回值,會傳回status狀态碼給調用他的父程序。
程序調用exit時候,會執行以下步驟:
- 先調用終止處理程式。終止處理程式通過atexit函數注冊,一個程序最多注冊32個。調用的順序和注冊的順序相反。終止處理程式沒注冊一次會被調用一次,盡管是相同的函數注冊了多次
- 所有打開的标準IO流會被flush和close
- 通過tmpfile建立的臨時檔案會被删除
#include <stdlib.h>
void exit(int status);
status:給父程序的傳回碼
程序退出函數2:_exit
_exit是一個系統調用,作用也是退出程式,但是不會去執行終止處理函數、清理IO、删除臨時檔案等步驟,直接進入到核心:
- 所有的檔案描述符會被直接關閉
- 然後所有的子程序被程序1接管
- 給父程序傳遞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
作用如下:
- 如果環境變量不存在則添加環境變量
- 如果環境變量已經存在,那麼把環境變量的值設定成最新的值。
注意事項:
- putenv了之後,string位址會添加到environ表中。
- putenv了之後,如果修改了string的内容,那麼環境變量也會對應被修改。
- 環境變量的修改隻會影響目前程序和子程序的環境變量表,對父程序無效
#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設定修改後的記憶體大小,可以改大也可以改小,大體分為以下幾個場景:
- 如果改小:那麼修改前的記憶體的起始位址到size大小的範圍内的資料不會被修改,傳回指向修改前的記憶體指針
- 如果改大,将在原來堆位址繼續往高位址空間擴充,有兩種可能,1)一是空間連續且足夠,那麼傳回原來的空間位址,注意新增加的記憶體不會被初始化;2)二是連續的空間不夠,那麼尋找新的位址空間,并将原來的資料轉移到新的空間中,原來的記憶體會被自動釋放掉,傳回新的空間位址
ptr指針和size之間的組合關系大體分為以下幾種情況:
- ptr等于NULL:realloc等效于malloc()
- ptr不等于NULL:ptr必須是通過malloc(), calloc(), realloc()配置設定過的
- 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之間的差別在于:
- 子程序和父程序之間共享記憶體資料,包括代碼段、資料段、堆、棧等,建立子程序的效率比fork高
- 子程序先執行,父程序會卡主,直到子程序調用了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腳本,腳本可以被執行有兩個條件:
- 腳本具有可執行權限
- 腳本開頭必須指定解釋器,比如:#!/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相同,不同點在于:
- 參考shell尋找可執行檔案的邏輯
- 如果不是絕對路徑,先從PATH環境變量中找,找不到就從目前目錄下尋找。
- 如果PATH路徑下程式找到了,但是沒有可執行權限,那麼繼續從下一級目錄下尋找
- 如果被執行的程式是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指令的傳回碼。