操作系统,揭开钢琴的盖子-4
C++软件与嵌入式软件面经解析大全(蒋豆芽的秋招打怪之旅)
本章讲解点
- 1.1 操作系统的来历
- 1.2 操作系统的功能
- 1.3 操作系统的硬件知识
- 1.4 linux下编译程序
- 1.5 Makefile
- 1.6 linux的常用指令
- 1.7 进程的概念
- 1.8 进程的状态转换
- 1.9 进程的创建
- 1.10 守护进程
- 1.11 僵尸进程与孤儿进程
- 1.12 wait()或waitpid()系统调用
- 1.13 进程通信——管道
- 1.14 进程通信——系统IPC
- 1.15 进程通信——socket套接字
- 1.16 线程
- 1.17 线程的创建
- 1.18 线程通信——互斥锁
- 1.19 线程通信——信号量
- 1.20 线程通信——条件变量
- 1.21 线程通信——读写锁
- 1.22 线程池
- 1.23 协程
- 1.24 虚拟内存
- 1.25 页表
- 1.26 缺页中断
- 1.27 缺页置换算法
- 1.28 锁
- 1.29 操作系统资源调度方法
- 1.30 IO模型类型
受众:本教程适合于C/C++已经入门的学生或人士,有一定的编程基础。
本教程适合于互联网、嵌入式软件求职的学生或人士。
故事背景
蒋 豆 芽:小名豆芽,芳龄十八,蜀中人氏。卑微小硕一枚,科研领域苟延残喘,研究的是如何炒好一盘豆芽。与大多数人一样,学习道路永无止境,间歇性踌躇满志,持续性混吃等死。会点编程,对了,是面对对象的那种。不知不觉研二到找工作的时候了,同时还在忙论文,豆芽都秃了,不过豆芽也没头发啊。
隔壁老李:大名老李,蒋豆芽的好朋友,技术高手,代码女神。给了蒋豆芽不少的人生指导意见。
导 师:蒋豆芽的老板,研究的课题是每天对豆芽嘘寒问暖。
故事引入
隔壁老李:话不多说,直接进入进程通信的内容。
蒋 豆 芽:(激动)好的,老李,来,赶紧上车!
1.13 进程通信——管道
隔壁老李:进程间通信就是在不同进程之间传递或交换信息。主要有三种方式:
- 管道。
- 系统IPC(消息队列,共享内存,信号量,信号)。
- 套接字。
我们先从管道开始讲起。
隔壁老李:前面已经提到,fork创建的父子进程之间不共享数据段和堆栈段,它们之间是通过管道进行通信的。
操作系统在内核中开辟一块缓冲区(称为管道)用于通信。管道是一种两个进程间同一时刻进行单向通信的机制。因为这种特性,管道又称为半双工管道,所以其使用是有一定的局限性的。半双工是指同一时刻数据只能由一个进程流向另一个进程(一端负责读,一端负责写);如果是全双工通信,需要建立两个管道。
管道分为无名管道和命名管道,无名管道只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件,管道本质是一种文件;命名管道可以允许无亲缘关系进程间的通信。
管道原型如下:
#include <unistd.h> int pipe(int fd[2]);
管道两端可分别用描述字fd[0]以及fd[1]来描述。注意管道的两端的任务是固定的,即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另 一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将发生错误。一般文件的 I/O 函数都可以用于管道,如close()、read()、write()等。
通信是指两个进程之间的信息交互,而pipe()函数创建的管道处于一个进程中间,单个进程中的管道几乎没有任何用处。因此一个进程在由 pipe()创建管道后,一般再使用fork() 建立一个子进程,然后通过管道实现父子进程间的通信。父子进程都有读端和写端,子进程的是从父进程复制过来的。
我们通过一个实例来看一下父子进程间的通信:
#include<unistd.h> #include<stdio.h> #include<stdlib.h> #include<string.h> #define INPUT 0 #define OUTPUT 1 int main(){ //创建管道 int fd[2]; pipe(fd); //创建子进程 pid_t pid = fork(); if (pid < 0){ printf("fork error!\n"); exit(-1); } else if (pid == 0){//执行子进程 printf("Child process is starting...\n"); //子进程向父进程写数据,关闭管道的读端 close(fd[INPUT]); write(fd[OUTPUT], "hello douya!", strlen("hello douya!")); exit(0); } else{//执行父进程 printf ("Parent process is starting......\n"); //父进程从管道读取子进程写的数据 ,关闭管道的写端 close(fd[OUTPUT]); char buf[255]; int output = read(fd[INPUT], buf, sizeof(buf)); printf("%d bytes of data from child process: %s\n", output, buf); } return 0; }
运行结果如下:
$ ./sharemat Parent process is starting...... Child process is starting... 13 bytes of data from child process: hello douya!
程序运行后,可以看到,我们成功实现了管道通信。特别强调的是,用管道通信时,先固定一端为读,然后再从另一端写,避免读写错误。
隔壁老李:上面我们讲了pipe无名管道,我们接下来讲讲命名管道FIFO。FIFO与管道几乎类似,所以FIFO也是一个字节流,从FIFO读取的顺序也是与被写入FIFO的顺序一致,容量是也有限的,也是可以确保写入不超过PIPE_BUF字节的操作是原子的。
FIFO的本质也是一个文件,但传递方向是可以双向的,它们两者之间的最大差别在于FIFO在文件系统中拥有一个名称,并且打开方式与打开一个普通文件是一样的(使用open),这样就能够将FIFO用于非相关进程之间的通信。
函数原型如下:
int mkfifo( const char* filename, mode_t mode ); //filename是你要创建的fifo管道的名字,操作系统是通过管道的路径名来区分不同的管道的 //mode是你要以何种权限来创建这个管道,本文中我们为了方便理解将它设置为"0777",即所有人都可以读写它
当创建了一个fifo管道之后,你的程序还需要打开它,使用"open()"函数,打开一个已经创建的管道;
int open( const char* pathname, int oflag ); /* pathname是你要打开的管道文件的路径。 oflag是你要以何种模式打开,这个参数有三个宏可供选择,分别是"O_RDONLY"(只读),"O_WRONLY"(只写),"O_NONBLOCK"(不阻塞)。但是他们三个不是互相独立的填入参数内的,其中"O_RDONLY"(只读)和"O_WRONLY"(只写)可以独立作为参数,而"O_NONBLOCK"(不阻塞)只能跟在其他两个参数的后面,作为附加条件,所以就有四种组合方式: 1)"O_RDONLY":以只读的方式打开一个管道文件,同时进程将会阻塞,直到有另一个进程以只写的方式打开这个管道文件。 2)"O_RDONLY | O_NONBLOCK":以只读的方式打开一个管道文件,但是进程不会阻塞。 3)"O_WRONLY":以只写的方式打开一个管道文件,同时进程将会阻塞,直到有另一个进程以只读的方式打开这个管道文件。 4)"O_WRONLY | O_NONBLOCK":以只写的方式打开一个管道文件,但是进程不会阻塞。 */
实例:我们来尝试写一个fifo的管道的进程1,我们先创建一个管道,然后以只读的方式打开它,等待进程2连接,当有进程链接上以后就会循环的读取管道中的数据,我们创建个"fifo_process1.c"文件:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #define FIFO_NAME "myfifo" int main(){ /* 先删除之前可能遗留的管道文件,然后再次创建它 */ unlink(FIFO_NAME); int result = mkfifo(FIFO_NAME, 0777 ); if( result != 0 ){ printf( "error:can't create a fifo.\n" ); return 1; } /* 以只读的方式打开管道文件 */ int fifo_fd = open( FIFO_NAME, O_RDONLY ); if( fifo_fd < 0 ){ printf( "error:can't open a fifo.\n" ); return 1; } char buffer[32]; int buffer_len; /* 循环从管到文件中读取数据 */ do{ memset( buffer, 0, 32 ); buffer_len = read( fifo_fd, buffer, 31 ); buffer[buffer_len] = '\0'; printf( "read:%s\n", buffer ); } while( memcmp( buffer, "close", 5 ) != 0 ); close( fifo_fd ); unlink( FIFO_NAME ); return 0; }
现在我们来写一个进程2,进程2会判断一下管道是否存在,如果存在则以只写的方式打开它,然后可以循环的向管道中写入数据,我们来创建一个"fifo_ process2.c":
#include <stdio.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #define FIFO_NAME "myfifo" int main(){ /* 判断一下管道文件是否存在,不存在就退出程序 */ int result = access( FIFO_NAME, F_OK ); if( result == -1 ){ printf( "error:can't find the fifo.\n" ); return 1; } /* 以只写的方式打开管到文件 */ int fifo_fd = open( FIFO_NAME, O_WRONLY ); if( fifo_fd < 0 ){ printf( "error:can't open a fifo.\n" ); return 1; } char buffer[32]; int buffer_len; /* 循环向管到文件中写入数据 */ do{ memset( buffer, 0, 32 ); printf( "Please input something:" ); scanf( "%s", buffer ); buffer_len = write( fifo_fd, buffer, strlen( buffer ) ); printf( "write:%s\n", buffer ); } while( memcmp( buffer, "close", 5 ) != 0 ); close( fifo_fd ); unlink( FIFO_NAME ); return 0; }
我们开启两个终端分别运行程序,结果如下,如此我们就成功实现了两个无亲缘关系的进程之间的通信了。
$ ./process1 read:hello!douya!
$ ./process2 Please input something:hello!douya! write:hello!douya!
1.14 进程通信——系统IPC
隔壁老李:系统IPC,有四种:消息队列,共享内存,信号量,信号。
我们先讲消息队列
消息队列是在一个系统内核中建立一个保存消息的队列,表现形式为消息链表。消息链表中节点的结构用msg声明。
有几个相关函数:
创建新消息队列或取得已存在的消息队列:
int msgget(key_t key , int msgflg); /* 参数中,key是一个端口号,也可以由ftok函数生成;msgflg如果等于IPC_CREAT,若没有队列,则创建并返回一个新标识符,若已存在则返回存在的标识符;msgflg如果等于IPC_EXCL,若没有队列,则返回-1,若已存在则返回0。 */
向队列读/写消息,函数原型如下:
msgrcv 从队列中取消息:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
msgsnd 将数据放到消息队列中:
int msgsnd(int msqid, canst void *msgp, size_t msgsz,int msgflg); /* 参数中,msqid是消息队列的标识码;msgp是指向消息缓冲区的指针。msgsz是消息的大小;msgtyp是从消息队列内读取的消息形态,如果值为0,则表示队列中的所有消息都会被读取。 */
设置消息队列属性,函数原型如下:
int msgctl(int msgqid, int cmd, struct msqid_ds *buf); /* msgctl系统调用对msgqid标识的消息队列执行cmd操作,系统定义了3种cmd操作:IPC_STAT、IPC_SET、IPC_RMID。 IPC_STAT用来获取消息队列对应的msqid_ds数据结构,并将其保存到buf指定的地址空间;IPC_SET用来设置消息队列的属性,要设置的属性存储在buf 中; IPC_RMID用来从内核中删除msqid标识的消息队列。 */
进程消息队列通信,需要两个进程,一个接受消息,一个发送消息,对应两个c文件,我们定义一个msgrecieve.c,用于消息接收,如下:
#include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/msg.h> #include <sys/ipc.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> struct msg_struc{ long msg_type;//标志位 char msg_text[512]; }; int main(void){ struct msg_struc data; //这里给0,返回队列的最早的一个消息 long int msgtype = 0; //打开消息队列 int msg = msgget((key_t)1234, 0777|IPC_CREAT); if(msg == -1){ //errno--number of last error fprintf(stderr, "The filure code is %d!\n",errno); exit(-1); } //接受消息队列的数据---以end结束 int running = 1; while(running){ //将结构体中数据通过msgrcv接收 if(msgrcv(msg, (void *)&data, BUFSIZ, msgtype, 0) == -1){ fprintf(stderr, "magcve error code %d!\n",errno); exit(-1); } printf("received string is:\n%s\n",data.msg_text); //以end结束 if(strncmp(data.msg_text, "end", 3) == 0){ running = 0; } } //断开消息队列的连接 if(msgctl(msg, IPC_RMID, 0) == -1){//msgctl删除消息队列 fprintf(stderr, "magctl error code %d!\n",errno); exit(-1); } exit(0); return 0; }
然后经过编译,直接运行。
$ gcc -Wall msgrecieve.c -o msgrecieve $ ./msgrecieve
这里进程一直等待消息。
然后我们定义一个msgsend.c,用于发送消息,如下:
#include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/msg.h> #include <sys/ipc.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> struct msg_struc{ long msg_type;//标志位 char msg_text[512];//发送数据缓冲区 }message; int main(void){ struct msg_struc data; //标识id //IPC_CREAT没有就创建及存取权限s int msg = msgget((key_t)1234, 0777|IPC_CREAT); if(msg == -1){ //errno--number of last error fprintf(stderr, "The filure code is %d!!!\n",errno); exit(-1); } int running = 1; while(running){ printf("Please enter data: \n"); //系统常量BUFSIZ是8192--定义在stdio.h中 char buffer[BUFSIZ]; // 标准输入向buffer获取BUFSIZ-1个字符 fgets(buffer, BUFSIZ, stdin); //标志置1,表示要通信了。其他数也可以 data.msg_type = 1; //将buffer的数据复制到结构体的msg_text中 stpcpy(data.msg_text, buffer); //发送数据到data结构体的数组中 if(msgsnd(msg, (void *)&data, 512, 0) == -1){ fprintf(stderr, "magsnd error!!!\n"); exit(-1); } //判断字符串是否相等,输入end结束 if(strncmp(data.msg_text, "end", 3) == 0){ running = 0;//退出while } sleep(1); } exit(0); }
打开另一个终端,编译、运行,然后我们发送消息:
Please enter data: hello Douya! Please enter data: how are you! Please enter data: do you finish your paper? Please enter data: haha i am a monster! Please enter data:
这个时候我们看到接收消息的终端显示出了消息:
received string is: hello Douya! received string is: how are you! received string is: do you finish your paper? received string is: haha i am a monster!
蒋 豆 芽:(晕)我的天,怎么这么多内容啊?
隔壁老李:进程的内容本来就多,没想到讲着讲着讲了这么多了,但是没办法,豆芽,内容再多,也要慢慢讲清楚,讲全面。你先拿笔记本记好,后面再慢慢复习吧。
隔壁老李:消息队列还有一种实现方式。那就是Posix消息队列。Posix消息队列可以认为是一个消息链表,某个进程往一个消息队列中写入消息之前,不需要另外某个进程在该队列上等待消息的达到,这一点与管道和FIFO相反。
Posix消息队列操作函数如下:
#include <mqueue.h> typedef int mqd_t; mqd_t mq_open(const char *name, int oflag, ... /* mode_t mode, struct mq_attr *attr */); //返回: 成功时为消息队列描述字,出错时为-1。 //功能: 创建一个新的消息队列或打开一个已存在的消息的队列。 int mq_close(mqd_t mqdes); //返回: 成功时为0,出错时为-1。 //功能: 关闭已打开的消息队列。 int mq_unlink(const char *name) //返回: 成功时为0,出错时为-1 //功能: 从系统中删除消息队列。 int mq_getattr(mqd_t mqd
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> - 本专刊适合于C/C++已经入门的学生或人士,有一定的编程基础。 - 本专刊适合于互联网C++软件开发、嵌入式软件求职的学生或人士。 - 本专刊囊括了C语言、C++、操作系统、计算机网络、嵌入式、算法与数据结构等一系列知识点的讲解,并且最后总结出了高频面试考点(附有答案)共近400道,知识点讲解全面。不仅如此,教程还讲解了简历制作、笔试面试准备、面试技巧等内容。 </p> <p> <br /> </p>