程序,是指編譯好的二進制文件,在磁盤上,不占用系統資源(CPU、內存、打開的文件、設備、鎖等等)。
進程,是一個抽象的概念,與操作系統原理聯系緊密。進程是活躍的程序,占用系統資源。在內存中執行(程序運行起來,產生一個進程)。
程序 --> 劇本(紙),進程 -->戲(舞台、演員、燈光、道具等等)。同一個劇本可以在多個舞台同時上演。同樣,同個程序也可以加載為不同的進程(彼此之間互不影響)。如:同時開兩個終端。各自都有一個bash,但彼此ID不同。
並發,在操作系統中,一個時間段中有多個進程都處於已啟動運行到運行完畢之間的狀態。但任一個時刻點上仍只有一個進程在運行。
例如,當下,我們使用計算機時可以邊聽音樂邊聊天上網。若籠統的將他們均看做一個進程的話,為什麼可以同時運行呢?因為並發。
分時復用CPU
在計算機內存中同時存放幾道相互獨立的程序,它們在管理程序控制之下,相互穿插的運行。多道程序設計必須有硬件基礎作為保證。
時鐘中斷即為多道程序設計模型的理論基礎。並發時,任意進程在執行期間都不希望放棄CPU。因此系統需要一種強制讓進程讓出CPU資源的手段。時鐘中斷有硬件基礎作為保障,對進程而言不可抗拒。操作系統中的中斷處理函數,來負責調度程序執行。
在多道程序設計模型中,多個進程輪流使用CPU(分時復用CPU資源)。而當下常見CPU為納米級,1秒可以執行大約10億條指令。由於人眼的反應速度是毫秒級,所以看似同時在運行。
1s = 1000ms
1ms = 1000us
1us = 1000ms
實質上,並發是宏觀並行,微觀串行! -- 推動了計算機蓬勃發展,將人類引入了多媒體時代。
我們知道,每個進程在內核中都有一個進程控制塊(PCB)來維護進程相關的信息,Linux內核的進程控制塊是task_struct結構體。
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h文件中可以查看struct task_struct結構體定義。其內部成員有很多,我們重點掌握以下部分即可:
環境變量,是指在操作系統中用來指定操作系統運行環境的一些參數。通常具備以下特征:
練習:打印當前進程的所有環境變量。
#include <stdio.h>
extern char **environ;
int main(int argc, char *argv[])
{
int i;
for(i = 0; environ[i]; i++)
{
printf("%s\n", environ[i]);
}
return 0;
}
按照慣例,環境變量字符串都是name=value這樣的形式,大多數name由大寫字母加下劃線組成,一般把name的部分叫做環境變量,value的部分則是環境變量的值。環境變量定義了進程的運行環境,一些比較重要的環境變量的含義如下:
PATH
可執行文件的搜索路徑。ls命令也是一個程序,執行它不需要提供完整的路徑名/bin/ls,然而通常我們執行當前目錄下的程序a.out卻需要提供完整的路徑名./a.out,這是因為PATH環境變量的值裡面包含了ls命令所在的目錄/bin,卻不包含a.out所在的目錄。PATH環境變量的值可以包含多個目錄,用:號隔開。在shell中用echo命令可以查看這個環境變量的值:
echo $PATH
SHELL
TERM
LANG
HOME
獲取環境變量
char *getenv(const char *name);
練習:編程實現getenv函數。
設置環境變量的值
int setenv(const char *name, const char * value, int overwrite);
刪除環境變量name的定義
int unsetenv(const char *name);
示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char * argv[])
{
char * val;
const char * name = "ABD";
val = getenv(name);
printf("1, %s = %s\n", name, val);//ABD = NULL
setenv(name, "efg", 1);
val = getenv(name);
printf("2, %s = %s\n", name, val);//ABD = efg
int ret = unsetenv(name);
printf("ret = %d \n", ret);//0
val = getenv(name);
printf("3, %s = %s \n", name, val);//ABD = NULL
return 0;
}
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf("father process exec begin...");
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
printf("I'm child, pid = %u, ppid = %u \n", getpid(), getppid());
}
else
{
printf("I'm father, pid = %u, ppid = %u \n", getpid(), getppid());
sleep(1);
}
printf("father process exec end...");
return 0;
}
循環創建n個子進程
錯誤示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf("father process exec begin...");
pid_t pid;
int i;
for(i = 0; i < 5; i++)
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
printf("I'm %dth child, pid = %u, ppid = %u \n", i+1, getpid(), getppid());
}
else
{
printf("I'm father, pid = %u, ppid = %u \n", getpid(), getppid());
sleep(1);
}
}
printf("father process exec end...");
return 0;
}
正確的調用方式
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf("father process exec begin...");
pid_t pid;
int i;
for(i = 0; i < 5; i++)
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
//不讓子進程現創建孫子進程
break;
}
}
if(i<5)
{
sleep(i);
printf("I'm %dth child, pid = %u, ppid = %u \n", i+1, getpid(), getppid());
}
else
{
sleep(i);
printf("I'm father");
}
return 0;
}
getpid函數
pid_t getpid(void);
getppid函數
pid_t getppid(void);
getuid函數
uid_t getuid(void);
uid_t geteuid(void);
getgid函數
gid_t getgid(void);
gid_t getegid(void);
進程共享
gdb調試
set follow-fork-mode child
命令設置gdb在fork之後跟蹤子進程。set follow-fork-mode parent
設置跟蹤父進程。fork創建子進程後執行的是和父進程相同的程序(但有可能執行不同的代碼分支), 子進程往往要調用一種exec函數以執行另一個程序。當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用exec並不創建新進程,所以調用exec前後該進程的id並未改變。
將當前進程的.text、.data替換為所要加載的程序的.text、.data,然後讓進程從新的.text第一條指令開始執行,但進程ID不變,換核不換殼。
其實有六種以exec開頭的函數,統稱exec函數:
加載一個進程,借助PATH環境變量
int execlp(const char *file, const char *arg, ...); 成功:無返回;失敗:-1。
參數1:要加載的程序的名字。該函數需要配合PATH環境變量來使用,當PAHT中所有目錄搜索後沒有參數1則出錯返回。
該函數通常用來調用系統程序。如:ls、date、cp、cat等命令。
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char * argv[])
{
pid_t pid;
pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if (pid > 0)
{
sleep(1);
printf("parent");
}
else
{
execlp("ls", "ls", "-l", "-a", NULL);
}
return 0;
}
加載一個進程,通過路徑+程序名來加載。
int execl(const char *path, const char *arg, ...);成功:無返回;失敗:-1
對比execlp, 如加載“ls”命令帶有-l,-F參數
execlp("ls", "ls", "-l", "-F", NULL); 使用程序名在PATH中搜索
execl("/bin/ls", "ls", "-l", "-F", NULL); 使用參數1給出的絕對路徑搜索
加載一個進程,使用自定義環境變量env。
int execvp(const char *file, const char *argv[]);
變參形式:1、... 2、argv[] (main 函數也是變參函數,形式上等同於int main(int argc, char *argv0, ...))
變參終止條件:1、NULL結尾;2、固參指定。
execvp與execlp參數形式不同,原理一致。
char *argv[] = {"ls", "-l", "-a", NULL};
execvp("ls", argv);
execv("/bin/ls", argv);
練習:將當前系統中的進程信息,打印到文件中。
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int fd;
fd = open("ps.out", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if(fd < 0)
{
perror("open ps.out error");
exit(1);
}
dup2(fd, STDOUT_FILENO);
execlp("ps", "ps", "aux", NULL);//執行成功,後面的語句不會執行
perror("execlp error");
exit(1);
return 0;
}
exec函數一旦調用成功即執行新的程序,不返回。只有失敗才返回,錯誤值-1。所以通常我們直接在exec函數調用後直接調用perror和exit(),無需if判斷。
l(list) 命令行參數列表。
p(path) 搜索file時使用path變量
v(vector) 使用命令行參數數組
e(environment) 使用環境變量數組,不使用進程原有的環境變量,設置新加載程序運行的環境變量。
事實上,只有execve是真正的系統調用,其它五個函數最終都是調用execve,所以execve在man手冊第2節,其它函數在man手冊第3節。這些函數之間的關系如下圖所示。
孤兒進程:父進程先於子進程結束,則子進程成為孤兒進程,子進程的父進程成為init進程,稱為init進程領養孤兒進程。
示例,產生一個孤兒進程:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char * argv[])
{
pid_t pid;
pid = fork();
if(pid == 0)
{
while(1)
{
printf("I am child, my parent pid is %d\n", getppid());
sleep(1);
}
}
else if(pid >0)
{
printf("I am parent, my pid is %d \n", getpid());
sleep(9);
printf("----------parent going to die---------\n");
}
else
{
perror("fork");
return 1;
}
return 0;
}
僵屍進程:進程終止,父進程尚未回收,子進程殘留資源(PCB)存放於內核中,變成僵屍(Zombie)進程。
特別注意:僵屍進程是不能使用kill命令清除掉的。因為kill命令只是用來終止進程的,而僵屍進程已經終止。
思考,用什麼辦法可清除僵屍進程呢?
示例,產生一個僵屍進程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid;
pid = fork();
if(pid == 0)
{
printf("---child, my parent=%d, going to sleep 10s \n", getppid());
sleep(10);
printf("-------------child die--------------\n");
}
else if(pid > 0)
{
while(1)
{
printf("I am parent, pid = %d, myson = %d \n", getpid(), pid);
}
}
else {
perror("fork error");
exit(1);
}
return 0;
}
一個進程在終止時會關閉所有文件描述符,釋放在用戶空間分配的內存,但它的PCB還保留著,內核在其中保存了一些信息:如果是正常終止則保存著退出狀態,如果是異常終止則保存著導致該進程終止的信號是哪個。這個進程的父進程可以調用wait或waitpid獲取這些信息,然後徹底清除掉這個進程。我們知道一個進程的退出狀態可以在shell中用特殊變量$?查看,因為Shell是它的父進程,當它終止時Shell調用wait或waitpid得到它的退出狀態,同時徹底清除掉這個進程。
父進程調用wait函數可以回收子進程終止信息。該函數有三個功能:
當進程終止時,操作系統的隱式回收機制會:
可使用wait函數傳出參數status來保存進程的退出狀態。借助宏函數來進一步判斷進程終止的具體原因。宏函數可以分為如下三組:
1、WIFEXITED(status) 為非0 --> 進程正常結束
WEXITSTATUS(status) 如上宏為真,使用此宏 --> 獲取進程退出狀態(exit的參數)
2、WIFSIGNALED(status) 為非0 --> 進程異常結束
WTERMSIG(status) 如上宏為真,使用此宏 --> 取得使進程終止的那個信號的編號。
3、WIFSTOPPED(status) 為非0 --> 進程處於暫停狀態
WSTOPSIG(status) 如上宏為真,使用此宏 --> 取得使進程暫停的那個信號的編號。
WIFCONTINUED(status) 為真 --> 進程暫停後已經繼續運行。
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
pid_t pid, wpid;
int status;
pid = fork();
if(pid == 0)
{
printf("---child, my parent=%d, going to sleep 10s \n", getppid());
sleep(30);
printf("-------------child die--------------\n");
//exit(100);
return 100;
}
else if(pid > 0)
{
wpid = wait(&status);
if(wpid == -1)
{
perror("wait error");
exit(1);
}
if(WIFEXITED(status))
{
printf("child exit with %d \n", WEXITSTATUS(status));
}
if(WIFSIGNALED(status))
{
printf("child killed by %d \n", WTERMSIG(status));
}
while(1)
{
printf("I am parent, pid = %d, myson = %d \n", getpid(), pid);
}
}
else {
perror("fork error");
exit(1);
}
return 0;
}
作用同wait,但可指定pid進程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, int options);成功:返回清理掉的子進程ID;失敗:-1(無子進程)。
特殊參數和返回情況:
參數pid:
>0 回收指定ID的子進程
-1 回收任意子進程(相當於wait)
0 回收和當前調用waitpid一個組的所有子進程
<-1 回收指定進程組內的任意子進程
參數status
參數options:
0 (wait)阻塞回收
WNOHANG 非阻塞回收(輪詢)
返回:
成功 pid
失敗 -1
0 參數3傳WNOHANG,並且子進程尚未結束
注意:一次wait或waitpid調用只能清理一個子進程,清理多個子進程應使用循環。
作業:父進程fork 3個子進程,三個子進程一個調用ps命令,一個調用自定義程序1(正常),一個調用自定義程序2(會出現錯誤)。父進程使用waitpid對其子進程進行回收。
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int n = 5, i;//默認創建五個子進程
pid_t p, q;
pid_t wpid;
if(argc == 2)
{
n = atoi(argv[1]);
}
for(i = 0; i < n; i++)
{//出口1,父進程專用出口
p = fork();
if(p == 0)
{
break;//出口2,子進程出口,i不自增
}
else if (i == 3)
{
q = p;
}
}
if(n == i)
{
sleep(n);
printf("I am parent, pid = %d\n", getpid(), getgid());
//waitpid(q, NULL, 0); //1、回收第三個子進程
//while(waitpid(-1, NULL, 0)); //2、等價於wait(NULl),阻塞回收任意子進程
do
{
//3、非阻塞回收任意子進程
//如果wpid == 0 說明子進程正在運行
wpid = waitpid(-1, NULL, WNOHANG);
if(wpid > 0)
{
n--;
}
sleep(1);
}
while(n > 0)
printf("wait finish\n");
}
else
{
sleep(i);
printf("I'm %dth child, pid = %d, gid = %d \n", i+1, getpid(), getgid());
}
return 0;
}
管道是一種最基本的IPC機制,作用於有血緣關系的進程之間,完成數據傳遞。調用pipe系統函數即可創建一個管道。有如下特質:
管理的原理:管道實為內核使用環形隊列機制,借助內核緩沖區(4k)實現。
管道的局限性:
int pipe(int pipefd[2]);
參數
返回值
讀管道
寫管道
父子進程間通信ls | wc -l
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int fd[2];
pipe(fd);
pid = fork();
//子進程
if(pid == 0){
//子進程從管道中讀數據,關閉寫端
close(fd[1]);
//讓wc從管道中讀取數據
dup2(fd[0], STDIN_FILENO);
//wc命令默認從標准讀入取數據
execlp("wc", "wc", "-l", NULL);
}else {
//父進程向管道中寫數據,關閉讀端
close(fd[0]);
//將ls的結果寫入管道中
dup2(fd[1], STDOUT_FILENO);
//ls輸出結果默認對應屏幕
execlp("ls", "ls", NULL);
}
return 0;
}
兄弟進程間通信
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
int fd[2], i;
pipe(fd);
for(i = 0; i < 2; i++){
if((pid = fork()) == 0){
break;
}
}
if(i == 0){ //兄
close(fd[0]); //寫,關閉讀端
dup2(fd[1], STDOUT_FILENO);
execlp("ls", "ls", NULL);
}else if(i == 1){ //弟
close(fd[1]); //讀,關閉寫端
dup2(fd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
}else {
close(fd[0]);
close(fd[1]);
for(i = 0; i < 2; i++){ //兩個兒子wait兩次
wait(NULL);
}
}
return 0;
}
命令:ulimit -a
函數:fpathconf
, 參數2:__PC_PIPE_BUF
優點:
缺點:
命名管道(Linux基礎文件類型)
創建
mkfifo
int mkfifo(const char *pathname, mode_t mode);
無血緣關系進程間通信
使用文件也可以完成IPC,理論依據是,fork後,父子進程共享文件描述符。也就共享打開的文件。
練習:編程測試,父子進程共享打開的文件。借助文件進行進程間通信。
思考:無血緣關系的進程可以打開同一個文件進行通信嗎?為什麼?
示例
/**
*父子進程共享打開的文件描述符------使用文件完成進程間共享
*/
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int fd1, fd2;
pid_t pid;
char * str = "----test for shared fd in parent child process----\n";
pid = fork();
if(pid < 0)
{
perror("fork error");
exit(1);
}
else if(pid == 0)
{
fd1 = open("test.txt", O_RDWR);
if(fd1 < 0)
{
perror("open error");
exit(1);
}
//子進程寫入數據
write(fd1, str, strlen(str));
printf("child wrote over...\n");
}
else
{
fd2 = open("test.txt", O_RDWR);
if(fd2 < 0)
{
perror("open error");
exit(1);
}
sleep(1); //保證子進程寫入數據
//父進程讀取數據
int len = read(fd2, buf, sizeof(buf));
write(STDOUT_FILENO, buf, len);
wait(NULL);
}
return 0;
}
存儲映射I/O(Memory-mmapped I/O)使一個磁盤文件與存儲空間中一個緩沖區相映射。於是當從緩沖區取數據,就相當於讀文件中的相應字節。於此類似,將數據存入緩沖區,則相應的字節就自動寫入文件。這樣,就可在不適用read和write函數的情況下,使用地址(指針)完成I/O操作。
使用這種方法,首先應通知內核,將一個指定文件映射到存儲區域中。這個映射工作可以通過mmap函數來實現。
mmap函數
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
返回:
參數:
示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mmap.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
int main(int argc, char *argv[])
{
char *p = NULL;
int fd = open("test.txt", O_CREAT|O_REWR, 0644);
if(fd < 0)
{
sys_err("open error");
}
int len = ftruncate(fd, 4);
if(len == -1)
{
sys_err("ftruncate error");
}
p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(p == MAP_FAILED)
{
sys_err("mmap error");
}
strcpy(p, "abc"); //寫數據
int ret = munmap(p, 4);
if(ret == -1)
{
sys_err("munmap error");
}
close(fd);
return 0;
}
munmap函數
同malloc函數申請內存空間類似的,mmap建立的映射區在使用結束後也應調用類似free的函數來釋放。
int munmap(void *addr, size_t length);
借鑒malloc和free函數原型,嘗試封裝自定義smalloc,sfree來完成映射區的建立和釋放。思考函數應如何設計?
mmap注意事項
思考:
總結:使用mmap時務必注意以下事項:
父子等有血緣關系的進程之間也可以通過mmap建立的映射區來完成數據通信。但相應的要在創建映射區的時候指定對應的標志位參數flags:
練習:父進程創建映射區,然後fork子進程,子進程修改映射區內容,然後,父進程讀取映射區內容,查驗是否共享。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int var = 100;
int main(int argc, char *argv[])
{
int *p;
pid_t pid;
int fd;
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if(fd < 0)
{
perror("open error");
exit(1);
}
unlink("temp"); //刪除臨時文件目錄項,使之具備被釋放條件
ftruncate(fd, 4);
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
//p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
if(p == MAP_FAILED) //注意:不是p == NULL
{
perror("mmap error");
exit(1)
}
close(fd); //映射區建立完畢,即可關閉文件
pid = fork();//創建子進程
if(pid == 0)
{
*p = 2000;
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
}
else
{
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);
}
int ret = mnumap(p, 4);//釋放映射區
if(ret == -1)
{
perror("mnumap error");
exit(1);
}
return 0;
}
結論:父子進程共享:
使用MAP_ANONYMOUS(或MAP_ANON),如:
int *p = mmap(NUll, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
"4"隨意舉例,該位置大小,可依實際需要填寫。
需要注意的是,MAP_ANONYMOUS和MAP_ANON這兩個宏是Linux操作系統特有的宏。在類Unix系統中如無該宏定義,可使用如下兩步來完成匿名映射區的建立。
1、fd = open("/dev/zero", O_RDWR);
2、p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int var = 100;
int main(int argc, char *argv[])
{
int *p;
pid_t pid;
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, -1, 0);
if(p == MAP_FAILED) //注意:不是p == NULL
{
perror("mmap error");
exit(1)
}
pid = fork();//創建子進程
if(pid == 0)
{
*p = 2000;
var = 1000;
printf("child, *p = %d, var = %d\n", *p, var);
}
else
{
sleep(1);
printf("parent, *p = %d, var = %d\n", *p, var);
wait(NULL);
}
int ret = mnumap(p, 4);//釋放映射區
if(ret == -1)
{
perror("mnumap error");
exit(1);
}
return 0;
}
實質上mmap是內核借助文件幫我們創建了一個映射區,多個進程之間利用該映射區完成數據傳遞。由於內核空間多進程共享,因此無血緣關系的進程間也可以使用mmap來完成通信。只要設置相應的標志位參數flags即可。若想實現共享,當然應該使用MAP_SHARED了。
示例
讀端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
struct STU
{
int id;
char name[20];
char sex;
}
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(int argc, char *argv[])
{
int fd;
struct STU student;
struct STU *mm;
if(argc < 2)
{
printf("./a.out file_shared\n");
exit(-1);
}
fd = open(argv[1], O_RDONLY);
if(fd == -1)
{
sys_err("open error");
}
mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
if(mm == MAP_FAILED)
{
sys_err("mmap error");
}
close(fd);
while(1)
{
printf("id=%d\t name=%s\t %c\n", mm->id, mm->name, mm->sex);
sleep(2);
}
munmap(mm, sizeof(student));
return 0;
}
寫端
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <string.h>
struct STU
{
int id;
char name[20];
char sex;
}
void sys_err(char *str)
{
perror(str);
exit(-1);
}
int main(int argc, char *argv[])
{
int fd;
struct STU student = {10, "xiaoming", 'm'};
char *mm;
if(argc < 2)
{
printf("./a.out file_shared\n");
exit(-1);
}
fd = open(argv[1], O_RDWR | O_CREAT, 0664);
ftruncate(fd, sizeof(student));
mm = mmap(NULL, sizeof(student), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(mm == MAP_FAILED)
{
sys_err("mmap error");
}
close(fd);
while(1)
{
memcpy(mm, &student, sizeof(student));
student.id++;
sleep(1);
}
munmap(mm, sizeof(student));
return 0;
}
使用已學習的各種C函數實現一個簡單的交互式的Shell, 要求:
你的程序應該可以處理以下命令:
ls -l -R > file1
cat < file1 | wc -c > file1
實現步驟:
可以使用kill -l
命令來查看當前系統可使用的信號有哪些。
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
不存在編號為0的信號。其中1-31號信號稱之為常規信號(也叫普通信號或標准信號),34-64稱之為實時信號,驅動編程與硬件相關。名字上區別不大。而前32個名字各不相同。
可通過man 7 signal
查看幫助文檔獲取。也可查看/usr/src/linux-headers-3.16.0-30/arch/s390/include/uapi/asm/signal.h
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no readers
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background process
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
在標准信號中,有一些信號是有三個“Value”,第一個值通常對alpha和sparc架構有效,中間值針對x86、arm和其他架構,最後一個應用於mips架構。一個'-'表示在對應架構中尚未定義該信號。
man 7 signal
幫助文檔中可看到:The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
這裡特別強調了9) SIGKILL
和19) SIGSTOP
信號,不允許忽略和捕捉,只能執行默認動作。甚至不能將其設置為阻塞。ctrl+c
組合鍵時,用戶終端向正在運行中的由該終端啟動的程序發出此信號。默認動作為終止進程。ctrl+\
組合鍵時產生該信號,用戶終端向正在運行中的由該終端啟動的程序發出此信號。默認動作為終止進程。ctrl+z
組合鍵時發出這個信號。默認動作為暫停進程。 ctrl+c 2) SIGINT(終止/中斷) "INT" -- Interrupt
ctrl+z 20)SIGTSTP(暫停/停止) "T" -- Terminal終端
ctrl+\ 3) SIGQUIT(退出)
除0操作 8) SIGFPE(浮點數例外) "F" -- float 浮點數
非法訪問內存 11)SIGSEGV(段錯誤)
總線錯誤 7) SIGBUS
kill -SIGKILL pid
int kill(pid_t pid, int sig);
示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
#define N 5
int main(int argc, char *argv[])
{
int i;
pid_t pid;
for(i = 0; i < N; i++)
{
pid = fork();
if(pid == 0)
break;
if(i == 2)
q = pid;
}
if(i < N)
{
while(1)
{
printf("I am child %d, getpid() = %u \n", i+1, getpid());
sleep(1);
}
}
else
{
sleep(1);
kill(q, SIGKILL);
while(1);
}
// int ret = kill(getpid(), SIGKILL);
// if(ret == -1)
// exit(1);
return 0;
}
raise(signo) == kill(getpid(), signo)
int raise(int sig);
void abort(void);
該函數無返回alarm函數
unsigned int alarm(unsigned int seconds);
練習:編寫程序,測試你使用的計算機1秒鐘能數多少個數。
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int i;
alarm(1);
for(i = 0; ; i++)
{
printf("%d\n", i);
}
return 0;
}
使用time命令查看程序執行的時間。程序運行的瓶頸在於IO,優化程序,首選優化IO。
time ./alarm > out
實際執行時間 = 系統時間 + 用戶時間 + 等待時間
setitimer函數
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
which
:指定定時方式
ITIMER_REAL --> 14)SIGLARM
,計算自然時間。ITIMER_VIRTUAL --> 26)SIGVTALRM
,只計算進程占用CPU時間。ITIMER_PROF --> 27)SIGPROF
,計算占用CPU及執行系統調用的時間。練習:使用setitimer函數實現alarm函數,重復計算機1秒數數程序。
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>
// struct itimerval {
// struct timeval it_interval; /* Interval for periodic timer */
// struct timeval it_value; /* Time until next expiration */
// };
// struct timeval {
// time_t tv_sec; /* seconds */
// suseconds_t tv_usec; /* microseconds */
// };
unsigned int my_alarm(unsigned int sec)
{
struct itimerval it, oldit;
int ret;
it.it_value.tv_sec = sec;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 0;
it.it_interval.tv_usec = 0;
ret = setitimer(ITIMER_REAL, &it, &oldit);
if(ret == -1)
{
perror("setitimer");
exit(1);
}
return oldit.it_value.tv_sec;
}
int main(void)
{
int i;
my_alarm(1);
for(i = 0; ; i++)
{
printf("%d\n", i);
}
return 0;
}
拓展練習,結合man page
編寫程序,測試it_interval、it_value這兩個參數的作用。
示例
#include <stdio.h>
#include <sys/time.h>
#include <signal.h>
void myfunc(int signo)
{
printf("hello\n");
}
int main(void)
{
struct itimerval it, oldit;
signal(SIGALRM, myfunc); //注冊SIGALRM信號的捕捉處理函數
it.it_value.tv_sec = 5;
it.it_value.tv_usec = 0;
it.it_interval.tv_sec = 3;
it.it_interval.tv_usec = 0;
if(setitimer(ITIMER_REAL, &it, &oldit) == -1)
{
perror("setitimer error");
return -1;
}
while(1);
return 0;
}
sigset_t set; //typedef unsigned long sigset_t;
int sigemptyset(sigset_t *set); 將某個信號集清0 成功:0;失敗:-1
int sigfillset(sigset_t *set); 將某個信號集置1 成功:0;失敗:-1
int sigaddset(sigset_t *set, int signum); 將某個信號加入信號集 成功:0;失敗:-1
int sigdelset(sigset_t *set, int signum); 交某個信號清出信號集 成功:0;失敗:-1
int sigismember(const sigset_t *set, int signum); 判斷某個信號是否在信號集中 不在:0;在:1;出錯:-1
sigset_t類型的本質是位圖。但不應該直接使用位操作,而應用使用上述函數,保證跨系統操作有效。
對比認知select函數。
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
mask = mask | set
mask = mask & ~set
mask = set
,若調用sigprocmask解除了對當前若干信號的阻塞,則在sigprocmask返回前,至少將其中一個信號遞達。int sigpending(sigset_t *set);
練習:編寫程序。把所有常規信號的未決狀態打印至屏幕。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void printped(sigset_t *ped)
{
int i;
for(i = 1; i < 32; i++)
{
if(sigismember(ped, i) == 1)
{
putchar('1');
}
else
{
putchar('0');
}
}
printf("\n");
}
int main(void)
{
sigset_t myset, oldset, ped;
sigemptyset($myset);
sigaddset(&myset, SIGQUIT);
sigaddset(&myset, SIGINT);
sigaddset(&myset, SIGTSTP);
sigaddset(&myset, SIGSEGV);
sigaddset(&myset, SIGKILL); //9,19不能屏蔽,加入也沒用
sigprocmask(SIG_BLOCK, &myset, &oldset);
while(1)
{
sigpending(&ped);
printped(&ped);
sleep(1);
}
return 0;
}
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
void (*signal(int signum, void (*sighandler_t)(int)))(int);
示例
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
typedef void (*sighandler_t)(int);
void catchsigiint(int signo)
{
printf("-----SIGINIT-----\N");
}
int main(void)
{
sighandler_t handler;
handler = signal(SIGINT, catchsigiint);
if(handler == SIG_ERR)
{
perror("signal error");
exit(1);
}
while(1);
return 0;
}
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_restorer:該元素是過時的,不應該使用,POSIX.1標准將不指定該元素。(棄用)
sa_sigaction:當sa_flags被指定為SA_SIGINFO標志時,使用該信號處理程序。(很少使用)
重點掌握:
1、sa_handler:指定信號捕捉後和處理函數名(即注冊函數)。也可賦值為SIG_IGN表忽略或SIG_DFL表執行默認動作。
2、sa_mask:調用信號處理函數時,所要屏蔽的信號集(信號屏蔽字)。注意:僅在修理函數被調用期間屏蔽生效,是臨時性設置。
3、sa_flags:通常設置為0,表使用默認屬性。
示例
#include <stdio.h>
#include <stdlib.h>
#include <siganl.h>
#include <unistd.h>
void docatch(int signo)
{
printf("%d signal is catched\n", signo);
}
int main(void)
{
int ret;
struct sigaction act;
act.sa_handler = docatch;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0; //默認屬性。信號捕捉函數執行期間,自動屏蔽本信號
ret = sigaction(SIGINT, &act, NULL);
if(ret < 0)
{
perror("sigaction error");
exit(1);
}
while(1);
return 0;
}
信號捕捉特性
int pause(void);
,返回值:-1,並設置errno為EINTR。練習,使用pause和alarm來實現sleep函數。
實現示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void catch_signalrm(int signo)
{
printf("%d signal is catched.", signo);
}
unsigned int mysleep(unsigned int seconds)
{
int ret;f
struct sigaction act, oldact;
act.sa_handler = catch_signalrm;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
ret = sigaction(SIGALRLM, &act, &oldact);
if(ret == -1)
{
perror("sigaction error");
exit(1);
}
alarm(seconds);
ret = pause(); // 主動掛起,等信號
if(ret == -1 && errno == EINTR)
{
printf("pause sucess\n");
}
ret = alarm(0); //鬧鈴清零
sigaction(SIGALRM, &oldact, NULL); //恢復SIGALRM信號舊有的處理方式。
return ret;
}
int main(void)
{
while(1)
{
mysleep(3);
printf("-------------------\n");
}
return 0;
}
前導例
時序問題分析
解決時序問題
int sigsuspend(const sigset_t *mask);
,掛起等待信號。改進版mysleep
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void sig_alrm(int signo)
{
printf("%d signal is catched.", signo);
}
unsigned int mysleep(unsigned int seconds)
{
int ret;f
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
//1、為SIGALRM設置捕捉函數,一個空函數
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
//2、設置阻塞信號集,阻塞SIGALRM信號
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
//3、定時n秒,到時後可以產生SIGALRM信號
alarm(seconds);
//4、構造一個調用sigsuspend臨時有效的阻塞信號集,在臨時阻塞信號集裡解除SIGALRM的阻塞
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
//5、sigsuspend調用期間,采用臨時阻塞信號集suspmask替換原有阻塞信號集
//這個信號集中不包含SIGALR信號,同時掛起等待
//當sigsuspend被信號喚醒返回時,恢復原有的阻塞信號集
sigsuspend(&suspmasks);
unslept = alarm(0);
//6、恢復SIGALRM原有的處理動作
sigaction(SIGALRM, &oldact, NULL);
//7、解除對SIGALRM的阻塞
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return (unslept);
}
int main(void)
{
while(1)
{
mysleep(3);
printf("-------------------\n");
}
return 0;
}
總結
分析如下父子進程交替的數數程序。當捕捉函數裡面的sleep取消,程序即會出現問題。請分析原因。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
int n = 0; flag = 0;
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(int num)
{
printf("I am child %d\t%d\n", getpid(), n);
n+=2;
flag = 1;
//sleep(1);
}
void do_sig_parent(int num)
{
printf("I am parent %d\t%d\n", getpid(), n);
n+=2;
flag = 1;//數數完成
//sleep(1);
}
int main(void)
{
pid_t pid;
struct sigaction act;
if((pid = fork()) < 0)
{
sys_err("fork");
}
else if(pid > 0 )
{
n = 1;
sleep(1);
act.sa_handler = do_sig_parent;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR2, &act, NULL); //注冊自己的信號捕捉函數
do_sig_parent(0);
while(1)
{
// wait for signal
if(flag == 1)//父進程數數完成
{
kill(pid, SIGUSR1);
flag = 0;//標志已經給子進程發送完信號
}
}
}
else if(pid == 0)
{
n = 2;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGUSR1, &act, NULL);//父進程數數完成發送SIGUSR1給子進程。
while(1)
{
// wait for signal
if(flag == 1)
{
kill(getppid(), SIGUSR2);
flag = 0;
}
}
}
}
示例中,通過flag變量標記程序實行進度。flag置1表示數數完成,flag置0表示給對方發送信號完成。
顯示,insert函數是不可重入函數,重入調用,會導致意外結果呈現。究其原因,是該函數內部實現使用了全局變量。
注意事項
子進程結束運行,其父進程會收到SIGCHLD信號。該信號的默認處理動作是忽略。可以捕捉該信號,在捕捉函數中完成子進程狀態的回收。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void sys_err(str)
{
perror(str);
exit(1);
}
void do_sig_child(int signo)
{
int status;
pid_t pid;
while((pid = waitpid(0, &status, WNOHANG)) > 0){
if(WIFEXITED(status))
printf("-----------child %d exit %d \n", pid, WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("child %d cancel signal %d \n", pid, WTERMSIG(status));
}
}
int main(void)
{
pid_t pid;
int i;
//阻塞SIGCHLD
for(i = 0; i < 10; i++){
if((pid = fork()) == 0)
break;
else if(pid < 0)
sys_err("fork");
}
if(pid == 0){ //10個子進程
int n = 1;
while(n--){
printf("child ID %d \n", getpid());
sleep(1);
}
return i+1;
}else if(pid > 0){
//SIGCHLD阻塞
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
//解除對SIGCHLD的阻塞
while(1){
printf("Parent ID %d \n", getpid());
sleep(1);
}
}
return 0;
}
分析該例子。結合 17)SIGCHLD信號默認動作,掌握不使用捕捉函數回收子進程的方式。
sigqueue函數對應kill函數,但可在向指定進程發送信號的同時攜帶參數
int sigqueue(pid_t pid, int sig, const union sigval value);
成功:0
失敗:-1,設置errno
union sigval{
int sival_int;
void *sival_ptr;
}
向指定進程發送指定信號的同時,攜帶數據。但,如傳地址,需注意,不同進程之間虛擬地址空間各自獨立,將當前進程地址傳遞給另一進程沒有實際意義。
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
簡單來說,一個Linux系統啟動,大致經歷如下步驟:
init --> fork --> exec --> getty --> 用戶輸入帳號 --> login --> 輸入密碼 --> exec --> bash
硬件驅動程序負責讀寫實際的硬件設備,比如從鍵盤讀入字符和把字符輸出到顯示器,線路規程像一個過濾器,對於某些特殊字符並不是讓它直接通過,而是做特殊處理,比如在鍵盤上按下ctrl+z,對應的字符並不會被用戶程序的read讀到,而是被線程規程截獲,解釋成SIGTSTP信號發給前台進程,通常會使該進程停止。線路規程應該過濾哪些字符和做哪些特殊處理是可以配置的。
下面我們借助ttyname函數,通過實驗看一下各種不同的終端所對應的設備文件名。
#include <unistd.h>
#include <stdio.h>
int main(void)
{
printf("fd 0: %s\n", ttyname(0));
printf("fd 1: %s\n", ttyname(1));
printf("fd 2: %s\n", ttyname(2));
return 0;
}
kill -SIGKILL -進程組ID(負的)
來將整個進程組內的進程全部殺死。getpgrp函數
pid_t getpgrp(void);
getpgid函數
pid_t getpgid(pid_t pid);
setpgid函數
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
if((pid = fork()) < 0){
perror("fork");
exit(1);
}else if(pid == 0){
printf("child PID == %d\n", getpid());
printf("child Group ID == %d\n", getpgid(0)); //返回組ID
//printf("child Group ID == %d\n", getpgrp()); //返回組ID
sleep(7);
printf("----Group ID of child is changed to %d\n", getpgid(0));
exit(0);
}else if(pid > 0){
sleep(1);
setpgid(pid, pid);//讓子進程自立門戶,成為進程組組長,以它的pid為進程組id
sleep(13);
printf("\n");
printf("parent PID == %d\n", getpid());
printf("parent's parent process PID == %d\n", getppid());
printf("parent Group ID == %d\n", getpgid(0));
sleep(5);
setpgid(getpid(), getppid());//改變父進程的組ID為父進程的父進程
printf("\n -----Group ID of parent is changed to %d \n", getpgid(0));
while(1);
}
return 0;
}
練習:fork一個子進程,並使其創建一個新會話。查看進程組ID、會話ID前後變化。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid;
if((pid = fork()) < 0){
perror("fork error");
exit(1);
} else if(pid == 0){
printf("child process PID is %d\n", getpid());
printf("Group ID of child is %d\n", getpgid(0));
printf("Session ID of child is %d\n", getsid(0));
sleep(0);
setsid();//子進程非組長進程,故其成為新會話首進程,且成為組長進程。該進程組ID即為會話進程
printf("Changed:\n");
printf("child process PID is %d\n", getpid());
printf("Group ID of child is %d\n", getpgid(0));
printf("Session ID of child is %d\n", getsid(0));
sleep(20);
exit(0);
}
return 0;
}
示例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void)
{
pid_t pid, sid;
int ret;
pid = fork();
if(pid > 0)
{
exit(1);
}
sid = setsid();
ret = chdir("/home/super/");
if(ret == -1)
{
perror("chdir error");
exit(1);
}
umask(0022);
close(STDIN_FILENO);
open("/dev/null", O_RDWR);
dup2(0, STDOUT_FILENO);
dup2(0, STDERR_FILENO);
while(1);
return 0;
}