- 實驗目的:
- 加深對進程概念的理解,明確進程和程序的區別。進一步認識併發執行的實質。
- 瞭解信號處理
- 認識進程間通信(IPC):進程間共享內存
- 實現shell:瞭解程序運行
1.實驗一:進程的創建實驗
程序一:
int main(void) {
int pid1 = fork();
printf("**1**\n");
int psid2 = fork();
printf("**2**\n");
if (pid1 == 0) {
int pid3 = fork();
printf("**3**\n");
} else {
printf("**4**\n");
}
return 0;
}
程序執行過程:
Line 6: 創建子進程1,即主進程與子進程1共存。
Line 7: 主進程輸出“**1**”
。
Line 8:主進程繼續創建子進程2,即主進程與兩個子進程共存。
Line 9:主進程輸出“**2**”
。
Line 14:主進程輸出“**4**”
。
Line 7:子進程輸出“**1**”
。
Line 8:子進程1創建子進程3,即三個子進程共存。
Line 9:子進程1輸出“**2**”
。
Line 11:子進程1繼續創建子進程4,即四個子進程共存。
Line 12:子進程1輸出“**3**”
。
Line 9 :子進程2輸出“**2**”
。
Line 14: 由於子進程2的父進程中pid1不爲0,所以輸出“**4**”
。
Line 9 : 子進程3輸出“**2**”
。
Line 11:由於子進程3的父進程中pid1爲0,所以創建子進程5。
Line 12:子進程3輸出“**3**”
。
Line 12:子進程4輸出“**3**”
。
Line 12: 子進程5輸出“**3**”
。
輸出結果:
**1**
**1**
**2**
**4**
**2**
**3**
**3**
**2**
**3**
**2**
**4**
**3**
輸出結果的順序與我的分析不同,原因是進程的執行是搶佔式的,哪個進程搶佔到了CPU,哪個進程就執行輸出。但是輸出的內容是一樣的:2個“**1**”
,4個“**2**”
,4個“**3**”
,2個“**4**”
.
截圖:
其實也可以畫進程樹來猜測輸出結果:
程序二:
int main(void) {
pid_t pid;
if ((pid = fork()) == -1) { // 生成子進程1
printf("Error");
exit(-1);
}
if (pid != 0) {
pid_t pid1;
if ((pid1 = fork()) == -1) { // 生成子進程2
printf("Error");
exit(-1);
}
if (pid1 != 0) {
printf("a");
} else {
printf("b");
exit(0);
}
} else {
printf("c");
exit(0);
}
wait(0); // 等待子進程執行完畢
wait(0);
exit(0); // 主進程退出
}
該程序是主進程與兩個子進程併發執行的過程。其中主程序輸出a,子程序分別輸出b、c。
輸出結果:
cba
截圖:
也可用類似於程序一中的進程樹進行分析。
程序三:
int main(void) {
int a = 0;
pid_t pid;
if ((pid=fork())) {
a = 1;
}
for (int i = 0; i < 2; i++) {
printf("X");
}
if (pid == 0) {
printf("%d\n", a);
}
return 0;
}
程序的執行過程:
Line 8: 調用fork()生成子進程1,即父進程與子進程1共存;
Line 9: 父進程執行a = 1;
Line 11: 父進程執行兩次循環,輸出兩個“X”;
Line 11: 子進程執行兩次循環,輸出兩個“X”;
Line 15: 子進程輸出a的值,即“0”
輸出結果:
XXXX0
即輸出四個X,一個0
截圖:
也可用類似於程序一中的進程樹進行分析。
程序四:
int main(void) {
int a = 0;
pid_t pid[2];
for (int i = 0; i < 2; i++) {
if ((pid[i]=fork())) {
a = 1;
}
printf("X");
//fflush(stdout);
}
if (pid[0] == 0) printf("%d\n", a);
if (pid[1] == 0) printf("%d\n", a);
return 0;
}
程序的執行過程:
Line 9: 通過第一次循環調用fork()函數創建子進程1,並將a賦值爲1,輸出“X”;i=0
Line 9: 通過第二次循環調用fork()函數創建子進程2,並將a賦值爲1,輸出“X”;i=1
Line 12: 子進程1輸出“X”;
Line 9: 子進程1創建子進程3,並將a賦值爲1;
Line 12: 子進程1輸出“X”;
Line 14: 子進程1輸出a的值,即“1”;
Line 12: 子進程2輸出“X”;
Line 14:由於子進程2的父進程的的pid[0]!=0,不輸出,繼承父進程的a=1(第一個循環中改變)
Line 15:輸出a 的值,即“1”。
Line 12:子進程3輸出“X”;
Line 14:由於子進程3的父進程的pid[0]==0,輸出a的值,即“0”;
Line 15:輸出a的值,即爲“0”。
輸出結果:
XXXX1
XX1
XX0
0
截圖:
按照我們的分析,程序應當輸出6個X,2個1,2個0。可如今程序輸出了8個X,這是爲什麼呢?
現在我們來假設多出的兩個x來自於哪裏:
對於printf函數來說,如果輸出沒有換行,則輸出的內容會殘留在緩衝區中,直到下一個回撤出現時清空。
在建立子進程三的時候,也就是主進程執行第二次循環的時候,由於主進程第一次循環輸出“X”而沒有換行,所以其緩衝區中存在“X”,所以子進程的緩衝區中也存在了“X”,待會輸出時也會輸出這個“X”。這是第一個“X”。
- 子進程二在執行第二次循環創建子進程四時,第一次循環輸出“X”沒有換行,所以其緩衝區中也殘留了“X”,這個“X”在子進程四輸出其他內容時也會被打印出來。這是第二個“X”
現在我們來驗證一下我們的假設:
在printf("X");
後面加上一句fflush(stdout);
,這一句起到清空緩存區的作用,下面我們再編譯執行函數,結果如下:
輸出結果:
XXX1
XX1
X0
0
果然,清除了緩衝區,程序就如我們預想結果一致了~
截圖:
也可用類似於程序一中的進程樹進行分析。
2.實驗二:信號處理實驗
程序一:
void waiting();
void stop();
int wait_mark;
int main(void) {
int p1, p2;
while((p1=fork())==-1); // 創建子進程1
if (p1 > 0) {
while((p2=fork())==-1); // 創建子進程2
if (p2 > 0) {
printf("%d %d %d\n", getpid(), p1, p2);
wait_mark = 1;
//signal(SIGINT, SIG_IGN);
signal(SIGINT, stop); // 處理ctrl+c的信號
waiting();
kill(p1, 16); // 向進程1發送16的信號
kill(p2, 17); // 向進程2發送17的信號
wait(0); // 等待子進程執行完畢
wait(0); // 等待子進程執行完畢
printf("parent process is killed!\n");
exit(0);
} else {
wait_mark = 1;
signal(SIGINT,SIG_IGN); // 忽略ctrl+c的影響,標註A
signal(17, stop); // 處理來自主進程的17信號
waiting();
printf("child process 2 is killed by parent!\n");
exit(0);
}
} else {
wait_mark = 1;
signal(SIGINT, SIG_IGN); // 忽略ctrl+c的影響,標註B
signal(16, stop); // 處理來自主程序的16信號
waiting();
printf("child process 1 is killed by parent!\n");
exit(0);
}
}
void waiting() {
while(wait_mark!=0);
}
void stop() {
wait_mark = 0;
}
對於程序的分析,我已用註釋表明,下面是對實驗結果的分析。
最開始程序是沒有標註A、標註B這兩個語句的,其運行的結果與我們預想的不同。
我們預想的結果是:(按下ctrl+C)
^Cchild process 1 is killed by parent!
child process 2 is killed by parent!
parent process is killed!
實際的結果如下:(按下ctrl+C)
^Cparent process is killed!
爲什麼實際的結果與我們預想的不一樣呢?因爲當我們程序運行後,無論是主進程還是子進程,都卡在了waiting()函數這裏等待中斷信號。一旦我們按下ctrl+c,系統會向主進程和兩個子進程同時發送SIGINT信號。對於主進程,根據設置,執行stop函數;對於子進程,由於沒有設置對該信號的處理,所以默認執行exit()函數而不輸出。無論主進程怎麼發出信號,子進程也是無法響應的。
解決以上問題的關鍵方法就是:讓子進程屏蔽SIGINT信號。其代碼如上所示。
或者不加標註A、標註B兩個語句,讓程序輸出主進程的pid(加入我得到的是10156).我們打開一個新的終端,輸入kill -SIGINT 10156
來中斷主程序,然後查看原終端,發現能得到一樣的結果。
截圖:
程序二:
void waiting();
void stop();
int wait_mark;
int main(void) {
int p1, p2;
signal(SIGINT,SIG_IGN);
// ctrl + c
signal(SIGQUIT, SIG_IGN);
// ctrl + \
while((p1=fork())==-1);
if (p1 > 0) {
while((p2=fork())==-1);
if (p2 > 0) {
wait_mark = 1;
signal(SIGINT, stop);
waiting();
kill(p1, 16);
kill(p2, 17);
wait(0);
wait(0);
printf("parent process is killed!\n");
exit(0);
} else {
wait_mark = 1;
signal(17, stop);
waiting();
printf("child process 2 is killed by parent!\n");
exit(0);
}
} else {
wait_mark = 1;
signal(16, stop);
waiting();
printf("child process 1 is killed by parent!\n");
exit(0);
}
return 0;
}
void waiting() {
while(wait_mark!=0);
}
void stop() {
wait_mark = 0;
}
要使程序徹底忽略ctrl+C信號,我們可以在main函數一開始就設置signal函數,或者是將主進程中的信號設置替換爲signal(SIGINT,SIG_IGN);
而不執行stop函數。當然還可以加入signal(SIGQUIT, SIG_IGN);
來屏蔽ctrl+\
信號。
3.進程間共享內存
- 函數介紹:
- shmget 創建或打開共享內存
- 爲什麼說是創建或打開共享內存麼?
- 例如現有兩個進程:父進程與子進程。如果其中一個進程先執行shmget,那麼它這個語句就是起到創建共享內存的作用,並返回進程id;後執行的進程這個語句就是起到打開共享內存的作用並返回進程id。
- 如果我要建立兩個共享內存,如何實現?
- 調用shmget時傳入的key不同,便能產生不同的共享內存。爲了產生不同的key,我們可以調用ftok()函數,傳入不同地址來生產不同的key。
- shmat 獲取共享內存地址
- shmdt 斷開與共享內存的連接
- shmctl 刪除共享內存
- shmget 創建或打開共享內存
# include <stdio.h>
# include <unistd.h>
# include <sys/shm.h>
# include <sys/stat.h>
# include <sys/types.h>
# include <sys/wait.h>
# include <stdlib.h>
# define MAX_SEQUENCE 10
typedef struct {
long fib_sequence[MAX_SEQUENCE];
int sequence_size;
} shared_data;
int main(int argc, char* argv[]) {
if (argc != 2) { // 判斷是否輸入了長度
fprintf(stderr, "Please enter the length of sequence\n");
exit(-1);
}
//int seq_size = argv[1]-'0';
int seq_size = atoi(argv[1]); // 將字符串轉化爲整型
if (seq_size > MAX_SEQUENCE) { // 判斷輸入的長度是否合法
fprintf(stderr, "Please enter the length less than 11\n");
}
int segment_id; // 創建或打開共享內存
if ((segment_id =shmget(IPC_PRIVATE, sizeof(shared_data), S_IRUSR| S_IWUSR)) == -1) {
fprintf(stderr, "Unable to create share memoriy");
exit(-1);
}
shared_data* shared_memory; // 獲取共享內存地址
if ((shared_memory = (shared_data*)shmat(segment_id, 0, 0)) == (shared_data*)-1) {
fprintf(stderr, "Unable to attach to segment%d\n", segment_id);
exit(-1);
}
shared_memory->sequence_size = seq_size;
pid_t pid; // 創建子進程
if ((pid = fork()) == -1) {
fprintf(stderr,"Unable to create a new process\n");
exit(-1);
}
if (pid == 0) { // 子進程生成斐波那契數列
shared_memory->fib_sequence[0] = 0;
shared_memory->fib_sequence[1] = 1;
for (int i = 2; i < seq_size; i++) {
shared_memory->fib_sequence[i] = shared_memory->fib_sequence[i-1]+shared_memory->fib_sequence[i-2];
}
if (shmdt(shared_memory) == -1) { // 斷開共享內存連接
fprintf(stderr, "Unable to detach");
exit(-1);
}
} else { // 主進程輸出斐波那契數列
wait(0);
for (int i = 0; i < seq_size; i++) {
printf("%ld ", shared_memory->fib_sequence[i]);
}
printf("\n");
if (shmdt(shared_memory) == -1) { // 斷開共享內存連接
fprintf(stderr, "Unable to detach");
exit(-1);
}
shmctl(segment_id, IPC_RMID, NULL); // 刪除共享內存
exit(0);
}
}
對程序的分析在代碼註釋中已經寫的很明白了。現在來談談實現的過程。
- 實現的過程
- 首先先定義共享空間的結構:包含一個存儲斐波那契數列的數組和一個保存長度的變量。
- 然後在程序中判斷輸入的合法性:包括輸入的參數個數以及輸入參數的大小範圍是否合法。
- 接着便是分配共享空間,獲取共享空間的地址
- 創建子進程,在子進程中生成斐波那契數列並存儲在共享內存中,存儲完畢後斷開連接。
- 在主進程中將共享內存中的斐波那契數列輸出,輸出完畢後斷開連接。
- 最後刪除共享空間
4.實現shell
# define MAX_LINE 80
# define BUFFER_SIZE 50
int next = 0; // 下一個指令存放的下標
char* history[10][MAX_LINE/2+1]; // 存放歷史記錄
int CommandLength[10] = {0}; // 標識指令的長度
void ProcessRCommand(char *args[]) { // 處理R指令的函數
int i, j, count=10;
char* newargs[MAX_LINE/2+1];
for(i = 0; i < MAX_LINE/2+1; ++i) {
newargs[i] = (char*)malloc((MAX_LINE/2+1)*sizeof(char));
}
history[next][0] = '\0';
if (args[1] == NULL){
i = (next + 9) % 10;
for(j = 0; j < CommandLength[i]; ++j){
strcpy(newargs[j], history[i][j]);
}
newargs[j]=NULL;
execvp(newargs[0], newargs);
} else {
i = next;
while (count--){
i = (i + 9) % 10;
if (strncmp(args[1], history[i][0], 1) == 0){
for(j = 0; j < CommandLength[i]; ++j) {
strcpy(newargs[j], history[i][j]);
}
newargs[j]=NULL;
execvp(newargs[0], newargs);
}
}
}
}
void setup(char inputBuffer[], char* args[], int* background) { // 指令的讀取
int length; // length:命令的字符數目
int i; // i:循環變量
int start; // start:命令的第一個字符位置
int ct; // ct:下一個參數存入args[]的位置
ct = 0;
length = read(STDIN_FILENO, inputBuffer, MAX_LINE);
start = -1;
if (length == 0) exit(0);
if (length < 0) {
perror("error reading the command");
exit(-1);
}
for (i = 0; i < length; i++) {
switch(inputBuffer[i]) {
case ' ':
case '\t':
if (start != -1) {
args[ct] = &inputBuffer[start];
ct++;
}
inputBuffer[i] = '\0'; // 起到分割作用
start = -1;
break;
case '\n':
if (start != -1) {
args[ct] = &inputBuffer[start];
ct++;
}
inputBuffer[i] = '\0';
args[ct] = NULL;
break;
default:
if (start == -1) {
start = i;
}
if (inputBuffer[i] == '&') {
*background = 1;
inputBuffer[i] = '\0';
}
}
}
args[ct] = NULL; // 不需要知道有多少個參數便能實現複製
}
void handle_SIGINT() { // 對CTRL+C的信號處理
int i, j;
printf("\n");
for (i = 0; i < 10; i++) {
for (j = 0; j < CommandLength[i]; j++) {
printf("%s ", history[i][j]);
}
printf("\n");
}
printf("COMMAND->");
fflush(stdout);
}
int main(void) {
char inputBuffer[MAX_LINE]; // 用於存儲指令
int background; // 用於標識子進程是否能與父進程並行
char* args[MAX_LINE/2+1]; // 用於存儲被切割後的指令
pid_t pid;
int i, j;
for(i = 0; i < 10; ++i) { // 爲存儲歷史記錄的函數分配空間
for(j = 0; j < MAX_LINE/2+1; ++j) {
history[i][j] = (char*)malloc(40*sizeof(char));
}
}
signal(SIGINT, handle_SIGINT); // 捕捉信號
while(1) { // 實現shell的輸入執行循環
background = 0;
printf("COMMAND->");
fflush(stdout);
setup(inputBuffer, args, &background);
i = 0;
if (args[0] != NULL && strcmp(args[0],"r") != 0){ // 記錄非r型指令
while(args[i] != NULL) {
strcpy(history[next][i], args[i]);
++i;
}
CommandLength[next] = i;
next = (next + 1) % 10;
}
if (args[0] != NULL && strcmp(args[0],"r") == 0) { // 記錄r型指令
if (args[1] == NULL) { // 記錄無參數的r型指令
i = (next + 9) % 10; // 獲取最後一條歷史指令的下標
for(j = 0; j < CommandLength[i]; ++j) {
strcpy(history[next][j], history[i][j]);
}
CommandLength[next] = j;
next = (next + 1) % 10;
} else { // 記錄有參數的r型指令
i = next;
int count = 10;
while(count--) {
i = (i + 9) % 10;
// 匹配指令第一個字母與第一個參數相同的指令
if (strncmp(args[1], history[i][0], 1) == 0) {
for(j = 0; j < CommandLength[i]; ++j) {
strcpy(history[next][j], history[i][j]);
}
CommandLength[next] = j;
next = (next + 1) % 10;
break;
}
}
}
}
if ((pid=fork()) == -1) { // 生成子進程
printf("Fork Error.\n");
}
if (pid == 0) { // 子進程執行的內容
if(strcmp(args[0],"r") == 0){ // 識別r型指令並調取處理函數
ProcessRCommand(args); // 處理r型指令
exit(0);
} else{ // 執行非r型指令
execvp(args[0],args);
exit(0);
}
}
if (background == 0) {
wait(0);
}
}
}
- 實現過程:
- 基礎:while(1)循環,其中包含:
- (1)setup函數,用於讀取用戶輸入的指令;
- 將指令切割然後存儲到數組中
- (2)存儲歷史指令;
- 構建一個二維數組
- 對於非r型指令,直接記錄
- 對於沒有參數的r型指令,取最近執行的指令複製到用於存儲最新指令的位置
- 對於有參數的r型指令,對歷史記錄搜尋,找到最近的且首字母與參數首字母相同的指令,將之父之道用於存儲最新指令的位置
- (3)創建子進程,執行指令。
- 執行非r型指令
- 執行r型指令
- (1)setup函數,用於讀取用戶輸入的指令;
- SIGINT信號處理
- 按下ctrl+c時,打印歷史記錄中的所有指令
- r指令的處理
- 沒有參數的r型指令,取最近的一條指令執行
- 有參數的r型指令,搜索到匹配的指令執行
- 基礎:while(1)循環,其中包含:
運行shell:
(1)輸入指令:
(2)ctrl+c 以及r指令的執行:
(3)帶參數的r指令的執行:
5.實驗心得
(1)通過本次實驗,我加深了對進程概念的理解:進程本質上就是程序的一次執行過程;
(2)並且進一步認識了併發執行的實質:減少程序的順序性,提高系統的並行性;
(3)瞭解到signal()能捕捉信號並作出相應的處理;
(4)瞭解到進程間通信的其中一種方式:進程間共享內存;
(5)通過實現shell,瞭解到shell執行指令時使用的系統調用,對shell有了進一步的認識;
(6)對於實驗一,我瞭解到若printf輸出的內容沒有換行,那麼輸出的內容就仍然保留在緩衝區中,因而在fork時會複製到子進程中;
(7)對於實驗二,我瞭解到主進程與子進程處於“waiting”狀態時,即處於等待信號的狀態時,一旦我發出SIGINT信號,主進程與子進程都能夠接收到。爲了使主進程接收到信號並處理而子進程不處理,那麼需要給子進程中加入信號屏蔽語句;
(8)對於實驗三,我瞭解到了主進程與子進程間實現數據共享的方式—共享內存;
(9)對於實驗四,我瞭解到了一個簡單版的shell是如何實現的,如何實現它的指令執行,以及如何實現它歷史記錄的查詢、執行。
總而言之,本次操作系統實驗讓我受益匪淺。