操作系统:进程和进程通信
来源:互联网 发布:热门的数据库研究方向 编辑:程序博客网 时间: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 删除共享内存
- 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 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型指令
- (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是如何实现的,如何实现它的指令执行,以及如何实现它历史记录的查询、执行。
总而言之,本次操作系统实验让我受益匪浅。
- 操作系统:进程和进程通信
- 操作系统:进程间通信
- 操作系统:进程通信
- 操作系统实验-进程通信
- 操作系统中的进程通信
- 操作系统进程间通信
- 【操作系统】进程间通信
- 【操作系统总结】进程通信
- 操作系统-进程通信
- 操作系统--进程通信
- 操作系统------进程间通信
- 操作系统 进程通信实验
- 【现代操作系统】进程的通信
- 【操作系统】进程的通信方式
- 操作系统---进程/线程 间通信
- 操作系统---进程/线程 间通信
- 进程间的通信--------操作系统
- 操作系统 -- 进程间通信机制
- app前端请求,接口响应代码一览
- islider滑动控件
- HTTP ERROR 500
- js、jquery、zepto、angularjs 参考资料
- 安卓6.0不ROOT查看已连接WIFI密码的方法--普通人看了会流泪,程序员看了会微笑
- 操作系统:进程和进程通信
- 枚举类、内部类
- Spring boot 项目相关
- linux ttl线连接树莓派(演示的为rp3,硬件篇)
- linux虚拟机(红帽Red Hat6.4)安装mysql5.1x86_64简易教程!
- JavaBean和表单处理
- Base64处理byte[ ]和base64字符串之间的转换
- 选择排序
- File--各种文件操作