操作系统:进程和进程通信

来源:互联网 发布:热门的数据库研究方向 编辑:程序博客网 时间:2024/05/21 17:01
  • 实验目的:
    • 加深对进程概念的理解,明确进程和程序的区别。进一步认识并发执行的实质。
    • 了解信号处理
    • 认识进程间通信(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”。

输出结果:XXXX1XX1XX00

截图:
这里写图片描述

按照我们的分析,程序应当输出6个X,2个1,2个0。可如今程序输出了8个X,这是为什么呢?

  • 现在我们来假设多出的两个x来自于哪里:

    • 对于printf函数来说,如果输出没有换行,则输出的内容会残留在缓冲区中,直到下一个回撤出现时清空。

    • 在建立子进程三的时候,也就是主进程执行第二次循环的时候,由于主进程第一次循环输出“X”而没有换行,所以其缓冲区中存在“X”,所以子进程的缓冲区中也存在了“X”,待会输出时也会输出这个“X”。这是第一个“X”。

    • 子进程二在执行第二次循环创建子进程四时,第一次循环输出“X”没有换行,所以其缓冲区中也残留了“X”,这个“X”在子进程四输出其他内容时也会被打印出来。这是第二个“X”

现在我们来验证一下我们的假设:

printf("X");后面加上一句fflush(stdout);,这一句起到清空缓存区的作用,下面我们再编译执行函数,结果如下:

输出结果:XXX1XX1X00

果然,清除了缓冲区,程序就如我们预想结果一致了~

截图:
这里写图片描述

也可用类似于程序一中的进程树进行分析。

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 10typedef 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 50int 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是如何实现的,如何实现它的指令执行,以及如何实现它历史记录的查询、执行。

总而言之,本次操作系统实验让我受益匪浅。

0 0
原创粉丝点击