【linux】系統編程-1-進程、管道和訊號
- 2020 年 12 月 28 日
- 筆記
- /label/linux, /label/linux/syspro, /label/lzm, linux, 嵌入式
1. 進程
1.1 概念
- 程式
- 程式是存放在存儲介質上的一個可執行文件
- 進程
- 進程是程式執行的過程,是程式在執行過程中分配和管理資源的基本單位
- 程式是靜態的,進程是動態的。進程的狀態是變化的,其包括進程的創建、調度和消亡
- 執行緒
- 執行緒是CPU調度和分派的基本單位,它可與同屬一個進程的其他的執行緒共享進程所擁有的全部資源
- 一個執行緒只能屬於一個進程,而一個進程可以有多個執行緒,但至少有一個執行緒
- 進程ID
- 進程ID 是一個16位的正整數,默認取值範圍是從 2 到 32768(可以修改)
- PID數字為1的值一般是為特殊進程 init 保留
- 父進程
- 任何進程(除init進程)都是由另一個進程啟動,該進程稱為被啟動進程的父進程(ID號稱為:PID),被啟動的進程稱為子進程(ID號稱為:PPID),
- 父進程號無法在用戶層修改
1.2 查看進程
- 查看進程命令
ps -aux
- 查看系統進程
pstree
- 將進程以樹狀關係列出來
1.3 啟動新進程
- 介紹三種方法啟動新進程
- system() 函數
- fork() 函數
- exec() 函數
1.3.1 system() 函數
- 可以理解為 啟動新進程
- system()啟動了一個運行著/bin/sh的子進程
- 說明 system() 函數依賴與 shell
int system (const char *string )
- 效果就相當於執行
sh –c string
- 效果就相當於執行
- system() 函數的特點
- 建立獨立進程,擁有獨立的程式碼空間,記憶體空間
- 等待新的進程執行完畢,system才返回。(阻塞)
- 常式
- system 運行完才會返回,才會在當前終端列印出數據
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t result;
printf("This is a system demo!\n\n");
/*調用 system()函數*/
result = system("ls -l");
printf("Done!\n\n");
return result;
}
1.3.2 fork() 函數
- 可以理解為 複製進程
- 頭文件
#include<unistd.h>
#include<sys/types.h>
pid_t fork( void);
- 若成功調用一次則
- 子進程返回 0
- 父進程返回子進程 ID
- 出錯返回 -1
- 若成功調用一次則
- 常式
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
pid_t result;
printf("This is a fork demo!\n\n");
/*調用 fork()函數*/
result = fork();
/*通過 result 的值來判斷 fork()函數的返回情況,首先進行出錯處理*/
if(result == -1) {
printf("Fork error\n");
}
/*返回值為 0 代表子進程*/
else if (result == 0) {
printf("The returned value is %d, In child process!! My PID is %d\n\n", result, getpid());
}
/*返回值大於 0 代表父進程*/
else {
printf("The returned value is %d, In father process!! My PID is %d\n\n", result, getpid());
}
return result;}
1.3.2 exce 系列函數
- 可以理解為 替換進程
- 調用 exec 並不創建新進程,所以前後的進程 ID 並未改變
- exec 只是用另一個新程式替換了當前進程的正文、數據、堆和棧段
- 在原進程中已經打開的文件描述符,在新進程中仍將保持打開,除非它們的「執行時關閉標誌」(close on exec flag)被置位
- 任何在原進程中已打開的目錄流都將在新進程中被關閉
- 舉個例子,A進程調用 exce 系列函數啟動一個進程B,此時進程B會替換進程A,進程A的記憶體空間、數據段、程式碼段等內容都將被進程B佔用,進程A將不復存在
1.3.2.1 exce 系列函數說明
- exec 系列函數有 6 個不同的 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 execve(const char *path, char *const argv[], char *const envp[])
- 函數說明
- 名稱包含 l 字母的函數(execl、 execlp 和execle)接收參數列表」list」作為調用程式的參數
- 名稱包含 p 字母的函數(execvp 和execlp)接受一個程式名作為參數,然後在當前的執行路徑中搜索並執行這個程式
- 名字不包含 p 字母的函數在調用時必須指定程式的完整路徑,其實就是在系統環境變數」PATH」搜索可執行文件
- 名稱包含 v 字母的函數(execv、execvp 和 execve)的命令參數通過一個數組」vector」傳入
- 名稱包含 e 字母的函數(execve 和 execle)比其它函數多接收一個指明環境變數列表的參數,並且可以通過參數envp傳遞字元串數組作為新程式的環境變數,這個envp參數的格式應為一個以 NULL 指針作為結束標記的字元串數組,每個字元串應該表示為」environment =virables」的形式
1.3 終止進程
- 可以分為 5 種進程終止
- 正常終止
- 從 main 函數返回
- 調用 exit() 終止
- 調用 _exit() 函數終止
- 異常終止
- 調用 abort() 函數終止
- 由系統訊號終止
- 正常終止
1.4 等待進程
- 父進程中調用wait()或者waitpid()函數讓父進程等待子進程的結束
1.4.1 wait() 函數
- wait()函數只是 waitpid() 函數的一個特例,在 Linux內部實現 wait 函數時直接調用的就是 waitpid 函數
pid_t wait(int *wstatus);
- wait() 函數在被調用的時候,系統將暫停父進程的執行,直到有訊號來到或子進程結束
- 如果在調用 wait() 函數時子進程已經結束,則會立即返回子進程結束狀態值
- 子進程的結束狀態資訊會由參數wstatus返回
- 該函數的返回值為子進程的PID
- 注意
- wait()要與fork()配套出現,且 fork() 調用先
- 參數wstatus用來保存被收集進程退出時的一些狀態
- 可以使用以下宏來判斷退出狀態
- WIFEXITED(status) :如果子進程正常結束,返回一個非零值
- WEXITSTATUS(status): 如果WIFEXITED非零,返回子進程退出碼
- WIFSIGNALED(status) :子進程因為捕獲訊號而終止,返回非零值
- WTERMSIG(status) :如果WIFSIGNALED非零,返回訊號程式碼
- WIFSTOPPED(status): 如果子進程被暫停,返回一個非零值
- WSTOPSIG(status): 如果WIFSTOPPED非零,返回一個訊號程式碼
1.4.2 waitpid() 函數
- wait()函數只是 waitpid() 函數的一個特例,在 Linux內部實現 wait 函數時直接調用的就是 waitpid 函數
pid_t waitpid(pid_t pid, int *wstatus, int options);
- pid:參數pid為要等待的子進程ID
- pid < -1:等待進程組號為pid絕對值的任何子進程
- pid = -1:等待任何子進程,此時的waitpid()函數就等同於wait()函數
- pid = 0:等待進程組號與目前進程相同的任何子進程,即等待任何與調用waitpid()函數的進程在同一個進程組的進程
- pid > 0:等待指定進程號為pid的子進程
- wstatus:與wait()函數一樣
- options:參數 options 提供了一些另外的選項來控制waitpid()函數的行為。如果不想使用這些選項,則可以把這個參數設為0
- pid:參數pid為要等待的子進程ID
2. 管道
2.1 概念
- 管道
- 管道是 Linux 由 Unix 那裡繼承過來的進程間的通訊機制,它是Unix早期的一個重要通訊機制。
- 其思想是,在記憶體中創建一個共享文件,從而使通訊雙方利用這個共享文件來傳遞資訊。由於這種方式具有單向傳遞數據的特點,所以這個作為傳遞消息的共享文件就叫做「管道」
- 管道分類
- 匿名管道(無名管道)(PIPE)
- 命名管道(有名管道)(FIFO)
2.2 匿名管道
2.2.1 匿名管道特徵
- 沒有名字,因此不能使用 open() 函數打開,但可以使用 close() 函數關閉
- 只提供單向通訊
- 只能用於具有血緣關係的進程間通訊,通常用於父子進程建通訊
- 管道是基於位元組流來通訊的
- 依賴於文件系統,它的生命周期隨進程的結束而結束
- 寫入操作不具有原子性,因此只能用於一對一的簡單通訊情形
- 管道也可以看成是一種特殊的文件,對於它的讀寫也可以使用普通的read()和write()等函數。但是它又不是普通的文件,並不屬於其他任何文件系統,並且只存在於內核的記憶體空間中,因此不能使用lseek()來定位
2.2.2 pipe() 函數
- pipe() 函數用於創建一個匿名管道,一個可用於進程間通訊的單向數據通道。
- 頭文件
#include <unistd.h>
- 函數原型
int pipe(int pipefd[2]);
- pipefd[0] 指向管道的 讀取 端
- pipefd[1] 指向管道的 寫 端
- 返回 0:匿名管道創建成功
- 返回 -1:創建失敗
- 使用步驟
- 父進程調用 pipe() 函數創建匿名管道
- 父進程調用 fork() 函數啟動(創建)一個子進程
- 若想從父進程將數據傳遞給子進程
- 父進程:關閉讀取端
- 子進程:關閉寫端
- 若想從子進程將數據傳遞給父進程
- 父進程:關閉寫端
- 子進程:關閉讀取端
- 當不需要使用管道時,關閉所有埠即可
- 常式
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DATA_LEN 256
#define DELAY_TIME 1
int main()
{
pid_t pid;
int pipe_fd[2]; //(1)
char buf[MAX_DATA_LEN];
const char data[] = "Pipe Test Program";
int real_read, real_write;
memset((void*)buf, 0, sizeof(buf));
/* 創建管道 */
if (pipe(pipe_fd) < 0) //(2)
{
printf("pipe create error\n");
exit(1);
}
/* 創建一子進程 */
if ((pid = fork()) == 0)
{
/* 子進程關閉寫描述符,並通過使子進程暫停 3s 等待父進程已關閉相應的讀描述符 */
close(pipe_fd[1]);
sleep(DELAY_TIME * 3);
/* 子進程讀取管道內容 */
if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
{
printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
}
/* 關閉子進程讀描述符 */
close(pipe_fd[0]);
exit(0);
}
else if (pid > 0)
{
/* 父進程關閉讀描述符,並通過使父進程暫停 1s 等待子進程已關閉相應的寫描述符 */
close(pipe_fd[0]);
sleep(DELAY_TIME);
if((real_write = write(pipe_fd[1], data, strlen(data))) != -1)
{
printf("Parent write %d bytes : '%s'\n", real_write, data);
}
/*關閉父進程寫描述符*/
close(pipe_fd[1]);
/*收集子進程退出資訊*/
waitpid(pid, NULL, 0);
exit(0);
}
}
2.3 命名管道
2.3.1 命名管道特徵
- 有名字,存儲於普通文件系統之中
- 任何具有相應許可權的進程都可以使用 open() 來獲取命名管道的文件描述符
- 跟普通文件一樣:使用統一的 read()/write() 來讀寫
- 跟普通文件不同:不能使用 lseek() 來定位,原因是數據存儲於記憶體中
- 具有寫入原子性,支援多寫者同時進行寫操作而數據不會互相踐踏
- 遵循先進先出(First In First Out)原則,最先被寫入 FIFO 的數據,最先被讀出來
2.3.2 創建命名管道命令
mkfifo
- 如
mkfifo test
- test 文件為命名管道文件
- 如
2.3.3 fifo() 函數
- fifo() 函數
- 頭文件
#include <unistd.h>
- 函數原型
int mkfifo(const char * pathname,mode_t mode);
- pathname:命名管道文件
- mode:
- O_RDONLY:讀管道
- O_WRONLY:寫管道
- O_RDWR:讀寫管道
- O_NONBLOCK:非阻塞
- O_CREAT:如果該文件不存在,那麼就創建一個新的文件,並用第三個參數為其設置許可權
- O_EXCL:如果使用 O_CREAT 時文件存在,那麼可返回錯誤消息
- 返回值:
- 0:成功
- EACCESS:參數 filename 所指定的目錄路徑無可執行的許可權
- EEXIST:參數 filename 所指定的文件已存在
- ENAMETOOLONG:參數 filename 的路徑名稱太長
- ENOENT:參數 filename 包含的目錄不存在
- ENOSPC:文件系統的剩餘空間不足
- ENOTDIR:參數 filename 路徑中的目錄存在但卻非真正的目錄
- EROFS:參數 filename 指定的文件存在於只讀文件系統內
- 常式
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>
#define MYFIFO "myfifo" /* 命名管道文件名*/
#define MAX_BUFFER_SIZE PIPE_BUF /* 4096 定義在於 limits.h 中*/
void fifo_read(void)
{
char buff[MAX_BUFFER_SIZE];
int fd;
int nread;
printf("***************** read fifo ************************\n");
/* 判斷命名管道是否已存在,若尚未創建,則以相應的許可權創建*/
if (access(MYFIFO, F_OK) == -1)
{
if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
{
printf("Cannot create fifo file\n");
exit(1);
}
}
/* 以只讀阻塞方式打開命名管道 */
fd = open(MYFIFO, O_RDONLY);
if (fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
memset(buff, 0, sizeof(buff));
if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)
{
printf("Read '%s' from FIFO\n", buff);
}
printf("***************** close fifo ************************\n");
close(fd);
exit(0);
}
void fifo_write(void)
{
int fd;
char buff[] = "this is a fifo test demo";
int nwrite;
sleep(2); //等待子進程先運行
/* 以只寫阻塞方式打開 FIFO 管道 */
fd = open(MYFIFO, O_WRONLY | O_CREAT, 0644);
if (fd == -1)
{
printf("Open fifo file error\n");
exit(1);
}
printf("Write '%s' to FIFO\n", buff);
/*向管道中寫入字元串*/
nwrite = write(fd, buff, MAX_BUFFER_SIZE);
if(wait(NULL)) //等待子進程退出
{
close(fd);
exit(0);
}
}
int main()
{
pid_t result;
/*調用 fork()函數*/
result = fork();
/*通過 result 的值來判斷 fork() 函數的返回情況,首先進行出錯處理*/
if(result == -1)
{
printf("Fork error\n");
}
else if (result == 0) /*返回值為 0 代表子進程*/
{
fifo_read();
}
else /*返回值大於 0 代表父進程*/
{
fifo_write();
}
return result;
}
3. 訊號
3.1 概念及特徵
- 訊號(signal)
- 又稱為軟中斷訊號,用於通知進程發生了非同步事件
- 它是Linux系統響應某些條件而產生的一個事件
- 它是在軟體層次上對中斷機制的一種模擬
- 是一種非同步通訊方式
- 在原理上,一個進程收到一個訊號與處理器收到一個中斷請求可以說是一樣的
- 訊號是進程間通訊機制中唯一的非同步通訊機制
- 訊號產生
- 訊號可能是由於系統中某些錯誤而產生
- 也可以是某個進程主動生成的一個訊號
3.2 系統支援的訊號
- 查詢系統支援的訊號種類命令:
kill -l
- linux支援62種訊號(沒有 32 號和 33 號訊號)
- 非實時訊號(不可靠):1-32
- 沒有排隊功能,訊號可能被丟棄
- 不會立即執行
- 先放入該進程式控制制塊(PCB),待合適的時候處理
- 實時訊號(可靠訊號):34-64
- 有排隊功能
- 非實時訊號(不可靠):1-32
3.3 訊號處理
- 訊號類似可分為三大類型:程式錯誤、外部事件以及顯式請求
- 當訊號發生時,訊號可以採取如下三種操作:
- 忽略訊號(SIGTOP 和 SIGKILL 是絕不能被忽略的)
- 捕獲訊號
- 讓默認訊號起作用
- 終止進程並且生成記憶體轉儲文件
- 終止終止進程但不生成core文件
- 忽略訊號
- 暫停進程
- 若進程是暫時暫停,恢復進程,否則將忽略訊號
3.4 發送訊號函數
- kill()
- raise()
- alarm()
3.4.1 kill()
- 命令:
kill [訊號或選項] PID(s)
- 函數
- 頭文件:
#include <sys/types.h> #include <signal.h>
- 函數原型:
int kill(pid_t pid, int sig);
- pid 取值如下
- pid > 1:將訊號sig發送到進程ID值為pid指定的進程
- pid = 0:訊號被發送到所有和當前進程在同一個進程組的進程
- pid = -1:將sig發送到系統中所有的進程,但進程1(init)除外
- pid < -1:將訊號sig發送給進程組號為-pid (pid絕對值)的每一個進程
- sig 為 訊號值
- 返回值
- 0:發送成功
- -1:發送失敗
- pid 取值如下
- 頭文件:
3.4.2 raise()
- raise() 函數為進程向自身發送訊號
- 函數
- 頭文件
#include <signal.h>
- 函數原型:
int raise(int sig);
- sig 為 訊號值
- 返回值
- 0:發送成功
- -1:發送失敗
- 頭文件
3.4.3 alarm()
- alarm() 稱為鬧鐘函數,設置時間為 seconds 秒,時間到後,它就向進程發送SIGALARM訊號。在時間未到時便重新調用 alarm() 函數,會更新到時值。
- 函數
- 頭文件
#include <unistd.h>
- 函數原型:
unsigned int alarm(unsigned int seconds);
- 頭文件
3.5 捕獲訊號函數
- signal()、sigaction()等函數
3.5.1 signal()
- signal()主要是用於捕獲訊號,可以改變進程中對訊號的默認行為
- 函數
- 頭文件
#include <signal.h>
- 函數原型
typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
- signum 是指定捕獲的訊號,如果指定的是一個無效的訊號,或者嘗試處理的訊號是不可捕獲或不可忽略的訊號(如SIGKILL),errno將被設置為EINVAL
- handler 是一個函數指針,它的類型是
void(*sighandler_t)(int)
類型 - handler 也可以是一個宏定義
- SIG_IGN:忽略該訊號
- SIG_DFL:採用系統默認方式處理訊號
- 頭文件
3.5.2 sigaction() *
- 不推薦讀者使用signal(),而推薦使用
sigaction();
- 函數
- 頭文件
#include <signal.h>
- 函數原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
-
signum:指定捕獲的訊號值
-
act:是一個結構體
- sa_handler 是一個函數指針,是捕獲訊號後的處理函數
- sa_sigaction 是擴展訊號處理函數,它也是一個函數指針,不僅可以接收到int 型的訊號值,還會接收到一個 siginfo_t 類 型的結構體指針,還有一個void類型的指針,還有需要注意的就是,不要同時使用 sa_handler 和 sa_sigaction,因為這兩個處理函數是有聯合的部分(聯合體)
- sa_mask 是訊號掩碼,它指定了在執行訊號處理函數期間阻塞的訊號的掩碼,被設置在該掩碼中的訊號,在進程響應訊號期間被臨時阻塞。除非使用 SA_NODEFER 標誌,否則即使是當前正在處理的響應的訊號再次到來的時候也會被阻塞
- re_restorer 則是一個已經廢棄的成員變數,不要使用
- oldact 返回原有的訊號處理參數,一般設置為NULL即可
- sa_flags 是指定一系列用於修改訊號處理過程行為的標誌
- SA_NOCLDSTOP 使父進程在它的子進程暫停或繼續運行時不會收到 SIGCHLD 訊號。即當它們接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU(停止)中的一種時或接收到SIGCONT(恢復)時,父進程不會收到通知
- SA_NOCLDWAIT 從Linux 2.6開始就存在這個標誌了,它表示父進程在它的子進程終止時不會收到 SIGCHLD 訊號,這時子進程終止則不會成為殭屍進程。
- SA_NODEFER 一般情況下, 當訊號處理函數運行時,內核將阻塞該給定訊號。但是如果設置了 SA_NODEFER標記, 那麼在該訊號處理函數運行時,內核將不會阻塞該訊號
- SA_RESETHAND 訊號處理之後重新設置為默認的處理方式。
- SA_SIGINFO 從Linux 2.2開始就存在這個標誌了,使用 sa_sigaction成員而不是使用sa_handler 成員作為訊號處理函數。
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
- siginfo_t
siginfo_t { int si_signo; /* 訊號數值 */ int si_errno; /* 錯誤值 */ int si_code; /* 訊號程式碼 */ int si_trapno; /*導致硬體生成訊號的陷阱號,在大多數體系結構中未使用*/ pid_t si_pid; /* 發送訊號的進程ID */ uid_t si_uid; /*發送訊號的真實用戶ID */ int si_status; /* 退出值或訊號狀態*/ clock_t si_utime; /*消耗的用戶時間*/ clock_t si_stime; /*消耗的系統時間*/ sigval_t si_value; /*訊號值*/ int si_int; /* POSIX.1b 訊號*/ void *si_ptr; int si_overrun; /*計時器溢出計數*/ int si_timerid; /* 計時器ID */ void *si_addr; /*導致故障的記憶體位置 */ long si_band; int si_fd; /* 文件描述符*/ short si_addr_lsb; /*地址的最低有效位 (從Linux 2.6.32開始存在) */ void *si_lower; /*地址衝突時的下限*/ void *si_upper; /*地址衝突時的上限 (從Linux 3.19開始存在) */ int si_pkey; /*導致的PTE上的保護密鑰*/ void *si_call_addr; /*系統調用指令的地址*/ int si_syscall; /*嘗試的系統調用次數*/ unsigned int si_arch; /* 嘗試的系統調用的體系結構*/ }
-
- 頭文件
3.6 訊號集
- 數據類型 sigset_t 是訊號集,訊號掩碼就是這種類型
- 頭文件:
#include <signal.h>
- 函數
int sigemptyset(sigset_t *set);
- 將訊號集初始化為空,使進程不會屏蔽任何訊號
int sigfillset(sigset_t *set);
- 將訊號集初始化為包含所有已定義的訊號
int sigaddset(sigset_t *set, int signum);
- 添加一個訊號到訊號集中
int sigdelset(sigset_t *set, int signum);
- 從訊號集中刪除一個訊號
int sigismember(const sigset_t *set, int signum);
- 判斷一個訊號是否在訊號集中
- 注意:
- 一個應用程式,在使用訊號集前,必須對其進行初始化,即是調用 sigemptyset() 或 sigfillset()
3.7 例子
- 常式來自野火
- 實驗現象
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
/** 訊號處理函數 **/void signal_handler(int sig) //(1){
printf("\nthis signal numble is %d \n",sig);
if (sig == SIGINT) {
printf("I have get SIGINT!\n\n");
printf("The signal is automatically restored to the default handler!\n\n");
/** 訊號自動恢復為默認處理函數 **/
}
}
int main(void){
struct sigaction act;
printf("this is sigaction function test demo!\n\n");
/** 設置訊號處理的回調函數 */
act.sa_handler = signal_handler;
/* 清空屏蔽訊號集 */
sigemptyset(&act.sa_mask);
/** 在處理完訊號後恢復默認訊號處理 */
act.sa_flags = SA_RESETHAND;
sigaction(SIGINT, &act, NULL);
while (1)
{
printf("waiting for the SIGINT signal , please enter \"ctrl + c\"...\n\n");
sleep(1);
}
exit(0);
}
參考:
* 野火