介紹
由於每個操作系統提供的系統調用各不同,爲了同一個應用程序在不同的操作系統上的可移植性,逐漸形成了一些可移植操作系統的接口標準。POSIX是Portable Operating System Interface for Unix的簡稱,中文譯爲可移植操作系統接口,X表明其是對UNIX API的傳承。在POSIX被提出之前,世界上存在着很多不同的UNIX操作系統,如FreeBSD, UNIX System V,Solaris, NetBSD等。這些接口各異的操作系統對應用程序開發人員造成了比較大的困擾。因此,IEEE於20世紀80年代爲能同時在不同的UNIX操作系統上運行的軟件定義了一套標準的操作系統API,其正式名稱爲IEEE Std 1003,國際標準的名稱爲ISO/IEC 9945。POSIX標準通常通過C library (libc)來實現。常見的libc包括glibc、musl、eglibc。Android也實現了一個名爲bionic的libc。通常而言,應用程序只需要調用libc提供的接口就可以實現對操作系統功能的調用。同時也實現了應用在類UNIX系統(包括Linux)上的可移植性。不同的操作系統也可以通過對libc的移植來支持現有的應用生態。
在POSIX或者操作系統調用的基礎上還可以封裝面向不同領域的領域應用接口。爲了開發的便攜性(如更多可複用的功能),人們逐漸開始爲各個應用領域定義應用開發接口與軟件架構。例如,面向汽車領域,一些車企聯合起來定義AUTOSAR(AUTomotive Open System ARchitecture),以方便汽車電子平臺各個部件的開發者遵循同一個標準和軟件架構進行開發。隨着汽車智能化引起的功能需求不斷增加,AUTOSAR也逐步演進到李Adaptive AUTOSAR,並提供更爲豐富的應用開發接口與軟件架構。
接下來我們主要針對POSIX的進程編程
什麼是進程。操作系統提出來進程的抽象:每個進程都對應於一個運行中的程序。進程可以處於以下幾種狀態。
- 新生狀態(new): 該狀態表示一個進程剛剛被創建出來,還未被初始化,不能被調度執行。在經過初始化之後,進程遷至就緒狀態。
- 就緒狀態(ready):該狀態表示進程可以被調度執行,但還未被調度器選擇。由於CPU數量可能少於進程數量,在某一時刻只有部分進程能被調度到CPU上執行。此時,系統中其他的可被調度的進程都處於預備狀態。在被調度器選擇執行後,進程遷移至運行狀態。
- 運行狀態(running):該狀態表示進程正在CPU上運行。當一個進程執行—段時間後,調度器可以選擇中斷它的執行並重新將其放回調度隊列,它就遷移至預備狀態。當進程運行結束,它會遷移至終止狀態。如果—個進程需要等待某些外部事件,它可以放棄CPU並遷移至阻塞狀態。
- 阻塞狀態(blocked):該狀態表示進程需要等待外部事件(如某個I/O請求的完成),暫時無法被調度。當進程等待的外部事件完成後,它會遷移至預備狀態。
- 終止狀態(terminated):該狀態表示進程已經完成了執行,且不會再被調度。
當一個程序或者進程被加載到內存裏,它會被分配一個進程標識符(Process IDentifier, PID)。進程具有獨立的虛擬內存空間。進程內存空間佈局包括用戶棧(Stack),代碼庫(Shared libraries),用戶堆(Heap),數據段(Data)和代碼段(Text)。還包含打開的文件描述符(File Descriptor, fd)表,用戶ID,組ID,定時器或者其他更多的資源。
線程在進程裏運行,一個進程至少有一個線程在運行。進程裏的所有線程享有進程所有資源。
每個線程都有它自己獨立的棧控件,進程的其他資源共享。
下面有幾個進程創建方式:
- fork(),vfork()
通過拷貝創建一個進程
- exec*()
通過加載一個程序觸發創建一個進程
- posix_spawn(),spawn*()
進程創建: fork()
Linux的進程創建方式非常有趣。在Linux中,進程一般是通過調用fork
接口 ,從已有的進程中“分裂”出來的。由於是從別的進程“分裂”而來,fork
接口非常簡單,不接收任何參數,返回值是當前進程的PID。當—個進程調用fork
時,操作系統會爲該進程創建—個幾乎一模一樣的新進程。我們—般將調用fork
的進程稱爲父進程,將新創建的進程稱爲子進程. 當fork
剛剛完成時,兩個進程的內存、寄存器、程序計數器等狀態都完全—致;但它們是完全獨立的兩個進程,擁有不同的PID與虛擬內存空間,在fork
完成後它們會各自獨立地執行,互不干擾。
爲每個命令使用 fork
調用生成一個獨立的子進程來處理鬧鐘。fork
版本是異步方式的的一種實現,該程序可以隨時輸入命令行,它們被彼此獨立地執行。 新版本並不比同步版本複雜多少。 該版本的主要難點在於對所有己終止子進程的 reap
。如果程序不做這個工作,則要等到程序退出的時候由系統回收,通常回收子進程的方法是調用某個 wait
系列函數。在本例中,我們調用 waitpid 函數,並設置 WNOHANG(父進程不必掛等待子進程的結束)。
下面使用一個簡單的鬧鐘實例程序來演示基本的異歩編程方法。該程序循環接受用戶輸入信息,直到出錯或者輸入完畢,用戶輸入的每行信息中,第一部分是鬧鐘等待的時間( 以秒爲單位),第二部分是鬧鐘時間到迖時顯示的文本消息。
#include <sys/types.h>
#include <wait.h>
#include "errors.h"
int main(void) {
pid_t pid;
int seconds;
char line[128];
char message[64];
while(1) {
printf("Alarm> ");
if (fgets(line, sizeof(line), stdin) == NULL)
exit(0);
if (strlen(line) <= 1)
continue;
if (sscanf(line, "%d %64[^\n]", &seconds, message) < 2) {
fprintf(stderr, "Bad command\n");
} else {
pid = fork();
if (pid == -1)
errno_abort("Fork");
if (pid == (pid_t)0) {
//子進程
sleep(seconds);
pid = getpid();
printf("Child process pid is (%d)\n",pid);
printf("(%d) %s\n", seconds, message);
exit(0);
} else {
//父進程
pid = getpid();
printf("Parent process pid is (%d)\n",pid);
do {
pid = waitpid((pid_t)-1, NULL, WNOHANG);
if (pid == (pid_t) -1)
errno_abort("Wait for child");
} while(pid != (pid_t)0);
}
}
}
}
輸入命令10 I Love China!
,表示10秒後子進程會發送(10) I Love China!
字符,查看終端log如下:
bspserver@ubuntu:~/workspace/posix_threads/bin$ ./alarm_fork
Alarm> 10 I Love China!
Parent process pid is (4966)
Alarm>
Child process pid is (4967)
(10) I Love China!
使用top
命令查詢進程的狀態變化:
top - 00:11:42 up 6:51, 2 users, load average: 0.03, 0.04, 0.00
Tasks: 299 total, 1 running, 298 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.2 sy, 0.0 ni, 99.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 3896.7 total, 2252.9 free, 890.2 used, 753.5 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 2771.1 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4376 root 20 0 37076 8860 7224 S 0.0 0.2 0:00.00 cupsd
4377 root 20 0 178400 12512 10940 S 0.0 0.3 0:00.00 cups-browsed
4379 lp 20 0 15336 6540 5740 S 0.0 0.2 0:00.00 dbus
4659 root 20 0 0 0 0 I 0.0 0.0 0:00.01 kworker/u256:0-events_power_efficient
4670 root 20 0 0 0 0 I 0.0 0.0 0:00.00 kworker/u256:1-events_unbound
4699 bspserv+ 20 0 369764 22732 14972 S 0.0 0.6 0:00.04 tracker-store
4966 bspserv+ 20 0 2648 564 480 S 0.0 0.0 0:00.00 alarm_fork
4967 bspserv+ 20 0 2648 108 0 S 0.0 0.0 0:00.00 alarm_fork
你會發現子進程在發送(10) I Love China!
字符之後,子進程的什麼週期結束之後。進程狀態由S
變成了Z
。Z
意思殭屍進程(zombie).
一般父進程沒有調用wait
操作,或者還沒來得及調用wait
操作,就算子進程已經終止了,它的所佔用的資源也不會完全釋放,這種情況進程稱爲殭屍進程(zombie)。
top - 00:11:42 up 6:51, 2 users, load average: 0.03, 0.04, 0.00
Tasks: 299 total, 1 running, 298 sleeping, 0 stopped, 1 zombie
%Cpu(s): 0.0 us, 0.2 sy, 0.0 ni, 99.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 3896.7 total, 2252.9 free, 890.2 used, 753.5 buff/cache
MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 2771.1 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4376 root 20 0 37076 8860 7224 S 0.0 0.2 0:00.00 cupsd
4377 root 20 0 178400 12512 10940 S 0.0 0.3 0:00.00 cups-browsed
4379 lp 20 0 15336 6540 5740 S 0.0 0.2 0:00.00 dbus
4659 root 20 0 0 0 0 I 0.0 0.0 0:00.01 kworker/u256:0-events_power_efficient
4670 root 20 0 0 0 0 I 0.0 0.0 0:00.00 kworker/u256:1-events_unbound
4699 bspserv+ 20 0 369764 22732 14972 S 0.0 0.6 0:00.04 tracker-store
4966 bspserv+ 20 0 2648 564 480 S 0.0 0.0 0:00.00 alarm_fork
4967 bspserv+ 20 0 0 0 0 Z 0.0 0.0 0:00.00 alarm_fork
也可通過ps -e -o stat,ppid,pid,cmd|egrep '^[Zz]'
命令查詢是否有殭屍進程(zombie)。
ps:ps命令用於獲取當前系統的進程信息.
-e:參數用於列出所有的進程
-o:參數用於設定輸出格式,這裏只輸出進程的stat(狀態信息)、ppid(父進程pid)、pid(當前進程的pid),cmd(即進程的可執行文件。
egrep:是linux下的正則表達式工具
'^[Zz]'
:這是正則表達式,^表示第一個字符的位置,[Zz],表示z或者大寫的Z字母,即表示第一個字符爲Z或者z開頭的進程數據,只所以這樣是因爲殭屍進程的狀態信息以Z或者z字母開頭。
bspserver@ubuntu:~$ ps -e -o stat,ppid,pid,cmd|egrep '^[Zz]'
STAT PPID PID CMD
Z+ 4966 4967 [alarm_fork] <defunct>
爲什麼會出現殭屍進程(zombie),大家可以帶着這個問題思考一下樣例代碼是不是有問題?
在QNX中用法一樣。但是QNX不支持多線程的進程fork。
進程的執行: exec*()
在fork完成後,我們得到了一個與父進程幾乎完全相同的子進程,可是在很多時候’用戶需要子進程執行與父進程完全不同的任務。當我們調用fork()創建了一個進程之後,通常將子進程替換成新的進程映象,這可以用exec系列的函數來進行。當然,exec系列的函數也可以將當前進程替換掉。
函數原型
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[]);
參數說明
path:可執行文件的路徑名字 arg:可執行程序所帶的參數,第一個參數爲可執行文件名字,沒有帶路徑且arg必須以NULL結束 file:如果參數file中包含/,則就將其視爲路徑名,否則就按 PATH環境變量,在它所指定的各目錄中搜尋可執行文件。
exec族函數參數極難記憶和分辨,函數名中的字符會給我們一些幫助: l : 使用參數列表 p:使用文件名,並從PATH環境進行尋找可執行文件 v:應先構造一個指向各參數的指針數組,然後將該數組的地址作爲這些函數的參數。 e:多了envp[]數組,使用新的環境變量代替調用進程的環境變量
fork()和exec()函數實例
int main(int argc, char *argv[])
{
pid_t pid;
char *arg[] = {"ls", "-l", NULL};
cout << "This is main process, PID is " << getpid() << endl;
pid = fork();
if (pid < 0){
cout << "fork error..." << endl;
exit(-1);
}
else if (pid == 0){//This is the child process
cout << "This is child process, PID is " << getpid() << endl;
//execl("/bin/ls", "ls", "-l", NULL);
//execlp("ls", "ls", "-l", NULL);
//execle("/bin/ls", "ls", "-l", NULL, NULL);
//execv("/bin/ls", arg);
//execvp("ls", arg);
execve("/bin/ls", arg, NULL);//上面的六個函數的運行結果都是一樣的
exit(11);//將子進程的退出碼設置爲11
}
else{//This is the main process
cout << "This is main process waiting for the exit of child process." << endl;
int child_status;
pid = wait(&child_status);
cout << "This is main process. The child status is " << child_status << ", and child pid is " << pid << ", WIFEXITED(child_status) is " << WIFEXITED(child_status) << ", WEXITSTATUS(child_status) is " << WEXITSTATUS(child_status) << endl;
}
exit(0);
}
顯示結果如下:
bspserver@ubuntu:~/workspace/posix_threads/bin$ ./fork_exec
This is main process, PID is 6429
This is main process waiting for the exit of child process.
This is child process, PID is 6430
total 10
-rwxrwxr-x 1 bspserver bspserver 23496 Dec 6 22:03 alarm_cond
-rwxrwxr-x 1 bspserver bspserver 20672 Dec 9 00:56 alarm_fork
-rwxrwxr-x 1 bspserver bspserver 22392 Dec 6 22:03 alarm_mutex
-rwxrwxr-x 1 bspserver bspserver 21048 Dec 6 22:03 alarm_thread
-rwxrwxr-x 1 bspserver bspserver 39736 Dec 9 03:04 fork_exec
-rwxrwxr-x 1 bspserver bspserver 21840 Dec 9 01:19 posix_spawn
-rwxrwxr-x 1 bspserver bspserver 21400 Dec 6 22:03 pthread_barriers
-rwxrwxr-x 1 bspserver bspserver 22080 Dec 6 22:03 pthread_rwlock
-rwxrwxr-x 1 bspserver bspserver 21416 Dec 6 22:03 pthread_semaphore
-rwxrwxr-x 1 bspserver bspserver 21176 Dec 6 22:03 pthread_spinlock
This is main process. The child status is 0, and child pid is 6430, WIFEXITED(child_status) is 1, WEXITSTATUS(child_status) is 0
從**fork()和fork()-exec()實例程序,可以得出fork()與fork()-exec()**創建進程的流程圖如下:
進程創建: posix_spawn*()
posix_spawn是POSIX提供的另一種創建進程的方式,最初是爲不支持fork的機器設計的。posix_spawn可以被認爲是fork和exec兩者功能的結合,它會使用類似於fork的方法(或者直接調用fork)獲得一份進程的拷貝’然後調用exec執行。它可以用來用fork和exec代替相對複雜的“fork-exec-wait”方法。
雖然posix_spawn完成的任務類似於fork和exec的組合,但它的實現並不是對fork和exec的簡單調用。目前,posix_spawn的性能要明顯優於“fork+exec”且執行時間與原進程的內存無關。因此’當進程創建的性能比較關鍵時,應用程序可以選擇犧牲“fork+exec”的靈活度,改用posix_spawn.
函數原型
#include <spawn.h>
int posix_spawn(pid_t *restrict pid, const char *restrict path,
const posix_spawn_file_actions_t *restrict file_actions,
const posix_spawnattr_t *restrict attrp,
char *const argv[restrict],
char *const envp[restrict]);
int posix_spawnp(pid_t *restrict pid, const char *restrict file,
const posix_spawn_file_actions_t *restrict file_actions,
const posix_spawnattr_t *restrict attrp,
char *const argv[restrict],
char *const envp[restrict]);
參數說明
pid: 子進程 pid(pid 參數指向一個緩衝區,該緩衝區用於返回新的子進程的進程ID)
path: 可執行文件的路徑 path(其實就是可以調用某些系統命令,只不過要指定其完整路徑)
file_actions: 參數指向生成文件操作對象,該對象指定要在子對象之間執行的與文件相關的操作
attrp: 參數指向一個屬性對象,該對象指定創建的子進程的各種屬性.指向一個posix_spawnattr_t對象,也可以是NULL表示使用默認值,這個對象 使用posix_spawnattr_init初始化,使用*_destroy銷燬,使用*_setflags、__setpgroup、__setsigdefault、_setsigmask等設置參數,其中_setflags可以設置POSIX_SPAWN_RESETIDS、__SETPGROUP、__SETSIGMASK、*_SETSIGDEFAULT等(Linux、FreeBSD、macOS分別還有其它支持的參數,這裏只列出了公共部分)
argv: 參數指定在子進程中執行的程序的參數列表
envp: 參數指定在子進程中執行的程序的環境變量
posix_spawn()函數實例
#include <spawn.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
#include <errno.h>
#define errExit(msg) do { perror(msg); \
exit(EXIT_FAILURE); } while (0)
#define errExitEN(en, msg) \
do { errno = en; perror(msg); \
exit(EXIT_FAILURE); } while (0)
char **environ;
int
main(int argc, char *argv[])
{
pid_t child_pid;
int s, opt, status;
sigset_t mask;
posix_spawnattr_t attr;
posix_spawnattr_t *attrp;
posix_spawn_file_actions_t file_actions;
posix_spawn_file_actions_t *file_actionsp;
/* Parse command-line options, which can be used to specify an
attributes object and file actions object for the child. */
attrp = NULL;
file_actionsp = NULL;
while ((opt = getopt(argc, argv, "sc")) != -1) {
switch (opt) {
case 'c': /* -c: close standard output in child */
/* Create a file actions object and add a "close"
action to it. */
s = posix_spawn_file_actions_init(&file_actions);
if (s != 0)
errExitEN(s, "posix_spawn_file_actions_init");
s = posix_spawn_file_actions_addclose(&file_actions,
STDOUT_FILENO);
if (s != 0)
errExitEN(s, "posix_spawn_file_actions_addclose");
file_actionsp = &file_actions;
break;
case 's': /* -s: block all signals in child */
/* Create an attributes object and add a "set signal mask"
action to it. */
s = posix_spawnattr_init(&attr);
if (s != 0)
errExitEN(s, "posix_spawnattr_init");
s = posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK);
if (s != 0)
errExitEN(s, "posix_spawnattr_setflags");
sigfillset(&mask);
s = posix_spawnattr_setsigmask(&attr, &mask);
if (s != 0)
errExitEN(s, "posix_spawnattr_setsigmask");
attrp = &attr;
break;
}
}
/* Spawn the child. The name of the program to execute and the
command-line arguments are taken from the command-line arguments
of this program. The environment of the program execed in the
child is made the same as the parent's environment. */
s = posix_spawnp(&child_pid, argv[optind], file_actionsp, attrp,
&argv[optind], environ);
if (s != 0)
errExitEN(s, "posix_spawn");
/* Destroy any objects that we created earlier. */
if (attrp != NULL) {
s = posix_spawnattr_destroy(attrp);
if (s != 0)
errExitEN(s, "posix_spawnattr_destroy");
}
if (file_actionsp != NULL) {
s = posix_spawn_file_actions_destroy(file_actionsp);
if (s != 0)
errExitEN(s, "posix_spawn_file_actions_destroy");
}
printf("PID of child: %jd\n", (intmax_t) child_pid);
/* Monitor status of the child until it terminates. */
do {
s = waitpid(child_pid, &status, WUNTRACED | WCONTINUED);
if (s == -1)
errExit("waitpid");
printf("Child status: ");
if (WIFEXITED(status)) {
printf("exited, status=%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("killed by signal %d\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {
printf("stopped by signal %d\n", WSTOPSIG(status));
} else if (WIFCONTINUED(status)) {
printf("continued\n");
}
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
exit(EXIT_SUCCESS);
}
顯示結果如下:
bspserver@ubuntu:~/workspace/posix_threads/bin$ ./posix_spawn date
PID of child: 8698
//子進程 date 運行結果
Thu Dec 9 17:49:21 PST 2021
//父進程檢測子進程結束狀態
Child status: exited, status=0
// -c 命令表示關閉了對應 data應用標準輸出使用到的文件描述符
bspserver@ubuntu:~/workspace/posix_threads/bin$ ./posix_spawn -c date
date: write error: Bad file descriptor
PID of child: 8715
Child status: exited, status=1
bspserver@ubuntu:~/workspace/posix_threads/bin$ ./posix_spawn -s sleep 60 &
[1] 8720
bspserver@ubuntu:~/workspace/posix_threads/bin$ PID of child: 8721
bspserver@ubuntu:~/workspace/posix_threads/bin$ kill -KILL 8721
bspserver@ubuntu:~/workspace/posix_threads/bin$ Child status: killed by signal 9
[1]+ Done ./posix_spawn -s sleep 60
fork(), fork()+exec(), posix_spawn()創建的進程都是異步進程;
參考文獻:
《現代操作系統:原理與實現》機械工業出版社 作者是陳海波、夏虞斌 等著。
Programming with POSIX Threads
https://www.man7.org/linux/man-pages/man3/posix_spawn.3.html