操作系统,揭开钢琴的盖子-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++已经入门的学生或人士,有一定的编程基础。

本教程适合于互联网嵌入式软件求职的学生或人士。

img

故事背景

img

蒋 豆 芽:小名豆芽,芳龄十八,蜀中人氏。卑微小硕一枚,科研领域苟延残喘,研究的是如何炒好一盘豆芽。与大多数人一样,学习道路永无止境,间歇性踌躇满志,持续性混吃等死。会点编程,对了,是面对对象的那种。不知不觉研二到找工作的时候了,同时还在忙论文,豆芽都秃了,不过豆芽也没头发啊。

隔壁老李:大名老李,蒋豆芽的好朋友,技术高手,代码女神。给了蒋豆芽不少的人生指导意见。

导 师:蒋豆芽的老板,研究的课题是每天对豆芽嘘寒问暖。

img

故事引入

img

隔壁老李:话不多说,直接进入进程通信的内容。

蒋 豆 芽:(激动)好的,老李,来,赶紧上车!

img

1.13 进程通信——管道

img

隔壁老李:进程间通信就是在不同进程之间传递或交换信息。主要有三种方式:

  1. 管道
  2. 系统IPC(消息队列,共享内存,信号量,信号)。
  3. 套接字

我们先从管道开始讲起。

隔壁老李:前面已经提到,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() 建立一个子进程,然后通过管道实现父子进程间的通信父子进程都有读端和写端,子进程的是从父进程复制过来的

img

我们通过一个实例来看一下父子进程间的通信:

#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!
img

1.14 进程通信——系统IPC

img

隔壁老李:系统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>

全部评论
感觉系统IPC部分讲的没有条理。存在管道通信了,为什么还会出现消息队列,为什么会出现共享内存等问题应该解释清楚。 管道中第一个进程必须等待数据被读取,这种方法使得进程间通信效率低下,出现了消息队列。 针对消息队列拷贝数据耗时,以及存放和读取数据的速度差别大的问题,出现了共享内存。 为了解决多进程竞争共享内存的情况,可以使用信号量。
7 回复 分享
发布于 2021-08-16 17:13
看完后脑子晕晕的😴
1 回复 分享
发布于 2023-05-12 09:20 韩国
1.13中,“因为这种单向性,管道又称为半双工管道,所以其使用是有一定的局限性的。半双工是指数据只能由一个进程流向另一个进程(一个管道负责读,一个管道负责写)”,有两个疑问:1.只能单向通信的管道应该不是半双工而是单工?2.括号中的内容,应该是“管道一端负责读,另一端负责写”?
点赞 回复 分享
发布于 2021-07-29 08:55

相关推荐

码客明:其实东西都是那一套,但是不同的方向会自己造轮子,然后方便汇报。一个同样的工具不同的方向做好几个工具,然后大同小异,汇报的时候说根据我们的团队和业务情况开发一个适合我们平台的xx工具。
点赞 评论 收藏
分享
牛客50327486...:腾讯官方:我们没有人机对局
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务