操作系統:進程和進程通信

  • 實驗目的:
    • 加深對進程概念的理解,明確進程和程序的區別。進一步認識併發執行的實質。
    • 瞭解信號處理
    • 認識進程間通信(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 刪除共享內存
# 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型指令
    • SIGINT信號處理
      • 按下ctrl+c時,打印歷史記錄中的所有指令
    • r指令的處理
      • 沒有參數的r型指令,取最近的一條指令執行
      • 有參數的r型指令,搜索到匹配的指令執行

運行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是如何實現的,如何實現它的指令執行,以及如何實現它歷史記錄的查詢、執行。

總而言之,本次操作系統實驗讓我受益匪淺。

發佈了146 篇原創文章 · 獲贊 268 · 訪問量 53萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章