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

  • 实验目的:
    • 加深对进程概念的理解,明确进程和程序的区别。进一步认识并发执行的实质。
    • 了解信号处理
    • 认识进程间通信(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**”.

截图:
操作系统:进程和进程通信_第1张图片

其实也可以画进程树来猜测输出结果:
操作系统:进程和进程通信_第2张图片

程序二:

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 
# include 
# include 
# include 
# include 
# include 
# include 

# 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);
    }
}

对程序的分析在代码注释中已经写的很明白了。现在来谈谈实现的过程。

  • 实现的过程
    • 首先先定义共享空间的结构:包含一个存储斐波那契数列的数组和一个保存长度的变量。
    • 然后在程序中判断输入的合法性:包括输入的参数个数以及输入参数的大小范围是否合法。
    • 接着便是分配共享空间,获取共享空间的地址
    • 创建子进程,在子进程中生成斐波那契数列并存储在共享内存中,存储完毕后断开连接。
    • 在主进程中将共享内存中的斐波那契数列输出,输出完毕后断开连接。
    • 最后删除共享空间

操作系统:进程和进程通信_第3张图片

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)输入指令:
操作系统:进程和进程通信_第4张图片
(2)ctrl+c 以及r指令的执行:

操作系统:进程和进程通信_第5张图片
(3)带参数的r指令的执行:
操作系统:进程和进程通信_第6张图片

5.实验心得

(1)通过本次实验,我加深了对进程概念的理解:进程本质上就是程序的一次执行过程;
(2)并且进一步认识了并发执行的实质:减少程序的顺序性,提高系统的并行性;
(3)了解到signal()能捕捉信号并作出相应的处理;
(4)了解到进程间通信的其中一种方式:进程间共享内存;
(5)通过实现shell,了解到shell执行指令时使用的系统调用,对shell有了进一步的认识;
(6)对于实验一,我了解到若printf输出的内容没有换行,那么输出的内容就仍然保留在缓冲区中,因而在fork时会复制到子进程中;
(7)对于实验二,我了解到主进程与子进程处于“waiting”状态时,即处于等待信号的状态时,一旦我发出SIGINT信号,主进程与子进程都能够接收到。为了使主进程接收到信号并处理而子进程不处理,那么需要给子进程中加入信号屏蔽语句;
(8)对于实验三,我了解到了主进程与子进程间实现数据共享的方式—共享内存;
(9)对于实验四,我了解到了一个简单版的shell是如何实现的,如何实现它的指令执行,以及如何实现它历史记录的查询、执行。

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

你可能感兴趣的:(C语言,操作系统)