【嵌入式八股精华版】四、嵌入式Linux篇

完整版见以下专栏:

【嵌入式八股】一、语言篇https://www.nowcoder.com/creation/manager/columnDetail/mwQPeM

【嵌入式八股】二、计算机基础篇https://www.nowcoder.com/creation/manager/columnDetail/Mg5Lym

【嵌入式八股】三、硬件篇https://www.nowcoder.com/creation/manager/columnDetail/MRVDlM

【嵌入式八股】四、嵌入式Linux篇https://www.nowcoder.com/creation/manager/columnDetail/MQ2bb0

alt alt

四、嵌入式Linux

Linux应用

160.在用户态开发中程序跑飞,出现段错误等情况,你通过什么方式去定位?

运行态的错误怎么调试?

段错误就是指访问的内存超出了系统所给这个程序的内存空间

在用户态开发中,程序跑飞和段错误可能是由于代码中存在错误或非法操作导致的,可以通过以下几种方式进行定位和调试:

  1. 代码审查:寻找代码中的潜在问题和错误。发现代码中的逻辑错误、死锁等问题,提高代码质量。
  2. 使用调试器:使用gdb等调试器可以对程序进行单步调试、打印变量值等操作,快速定位代码错误的位置。在程序运行时出现异常时,可以使用调试器的core dump功能生成core文件,通过core文件定位程序出现问题的原因。
  3. 日志输出:在程序中加入日志输出语句,将关键的信息记录下来,便于后续分析。可以使用printf、syslog等方式输出日志,也可以使用专业的日志框架,如log4j等。
  4. 硬件调试工具:如果程序涉及硬件调用,可以使用硬件调试工具,如逻辑分析仪、示波器等,对硬件进行调试。
  5. 静态分析工具:使用静态分析工具对代码进行扫描,查找代码中的潜在问题,如空指针、内存泄漏、未初始化变量等。常用的静态分析工具包括Coverity、PVS-Studio等。
  6. 动态分析工具:使用动态分析工具对程序进行运行时监控,如检测内存泄漏、检测代码覆盖率等。常用的动态分析工具包括Valgrind、strace等。
  7. core dump文件分析:在程序发生崩溃时,可以通过core dump文件进行分析,查找出现问题的原因。可以使用gdb等调试器对core dump文件进行分析,找出导致程序崩溃的代码位置。
161.常考的Linux 命令

Linux管理文件和目录的命令

命令 功能 命令 功能
pwd 显示当前目录 ls 查看目录下的内容
cd 改变所在目录 cat 显示文件的内容
grep 在文件中查找某字符 cp 复制文件
touch 创建文件 mv 移动文件
rm 删除文件 rmdir 删除目录
vi 编辑文件 mkdir
find 文件搜索

有关磁盘空间的命令

命令 功能
mount 挂载文件系统
umount 卸载已挂载上的文件系统
df 检查各个硬盘分区和已挂上来的文件系统的磁盘空间
du 显示文件目录和大小
fsck 主要是检查和修复Linux文件系统

文件备份和压缩命令

在Linux中,常用的文件压缩工具有gzip、bzip2、zip。bzip2是最理想的压缩工具,它提供了最大限度的压缩。zip兼容性好,Windows也支持。

命令 功能
bzip2/bunzip2 扩展名为bz2的压缩/解压缩工具
gzip/gunzip 扩展名为gz的压缩/解压缩工具
zip/unzip 扩展名为zip的压缩/解压缩工具
tar 创建备份和归档

有关关机和查看系统信息的命令

命令 说明
shutdown 正常关机
reboot 重启计算机
ps 查看目前程序执行的情况
top 查看目前程序执行的情景和内存使用的情况
kill 终止一个进程
date 更改或查看目前日期
cal 显示月历及年历

管理使用者和设立权限的命令

命令 说明 命令 说明
chmod 用来改变权限 useradd 用来增加用户
su 用来修改用户 chown 改变文件的所有者
chgrp 改变文件所属用户组

线上查询的命令

命令 功能
man 查询和解释一个命令的使用方法,以及这个命令的说明事项
locate 定位文件和目录
whatis 寻找某个命令的含义

文件阅读的命令

命令 功能
head 查看文件的开头部分
tail 查看文件结尾的10行
less less是一个分页工具,它允许一页一页地(或一个屏幕一个屏幕地)查看信息
more more是一个分页工具,它允许一页一页地(或一个屏幕一个屏幕地)查看信息

网络操作命令

命令 功能 命令 功能
ftp 传送文件 telnet 远端登陆
bye 结束连线并结束程序 rlogin 远端登入
ping 检测主机 netstat 显示网络状态

其他命令

命令 功能 命令 功能
echo 显示一字串 passwd 修改密码
clear 清除显示器 lpr 打印
lpq 查看在打印队列中等待的作业 lprm 取消打印队列中的作业
162.Linux下查看内存使用情况的命令?

在 Linux 下查看内存使用情况的命令主要有以下几个:

  1. ps:可以查看进程的内存使用情况和其他信息。常用的命令选项有 -o(自定义输出格式)、-e(显示所有进程信息)等。ps -aux
  2. top:可以动态地查看系统资源使用情况,包括内存、CPU、进程等。在 top 命令中,按下“Shift + M”可以按内存使用量排序。
  3. free:可以查看系统内存使用情况,包括内存总量、空闲内存、已使用内存、缓存区内存、交换分区等。常用的命令选项有 -m(以 MB 为单位显示)、-g(以 GB 为单位显示)等。
  4. htop:是 top 命令的升级版,可以更直观地查看系统资源使用情况。在 htop 中,按下“F6”可以按内存使用量排序。
  5. vmstat:可以实时地查看系统内存使用情况和进程活动情况。常用的命令选项有 -s(以人类可读的格式显示内存使用情况)、-a(显示所有活动的进程信息)等。
  6. cat /proc/meminfo 显示一些关于系统内存使用情况的详细信息。
163.Linux下查看磁盘的命令?
  • df -hl:查看磁盘剩余空间
  • df -h:查看每个根路径的分区大小
  • du -sh [目录名]:返回该目录的大小
  • du -sm [文件夹]:返回该文件夹总M数
  • du -h [目录名]:查看指定文件夹下的所有文件大小(包含子文件夹)
164.VIM三大模式和最常用命令
  • 命令模式
//切换到输入模式
i  //进入插入状态(按下i ,并不会输入一个字符,而被当作一个命令insert)
   //再输入字符,会插入在光标前
a  //进入追加状态(再输入字符,会追加在光标后)
o  //进入新一行输入状态(再输入字符,会在新一行输入)
   
//移动光标
     k 上 
h 前       l 后 
     j 下
     
r  //取代光标处的字符
x  //删除当前光标所在处的字符。

//打开默认的模式
ctrl + f  //下翻页
ctrl + b  //上翻页 
gg        //跳到第一行
shift + g //跳到最后一行

yy           //复制一行
v + h/j/k/l  //选取字符串
y            //复制
p            //粘贴 

dd        //删除一整行
ndd       //3dd: 删除3行

u         //撤销,复原前一个动作
Ctrl+r    //反撤销,回退到修改前状态

i   //命令模式切换到编辑模式,直接按键盘上的i,出现INSERT 
:   //命令模式切换到底行模式(即可在最底一行输入命令)   
  • 编辑模式

    编辑模式我们可以在这个模式上输入一些文本。

Esc //切换到命令模式
  • 底行模式
:w  //写入 
:q  //退出 
:wq //写入并退出
:q! //不保存退出
    
/word  //搜索字符串 word
       // n: 查找下一个  shift + n: 查找上一个 

:set nu //显示行号    
:set nu! //隐藏行号   
:30   //光标跳到第30行
 
Backspace //取消底行字符后,自动切换到命令模式	
165.gcc编译4步骤

alt

  1. 预处理:在这一步,gcc会处理源代码文件中的宏定义、条件编译指令等预处理指令,并将它们替换成实际的代码

    gcc -E source_file.c -o preprocessed_file.i
    

    这个命令会将source_file.c文件进行预处理,并将结果保存到preprocessed_file.i文件中。

  2. 编译:在这一步,gcc会将预处理后的源代码翻译成汇编语言

    gcc -S preprocessed_file.i -o assembly_file.s
    

    这个命令会将preprocessed_file.i文件进行编译,并将结果保存到assembly_file.s文件中。

  3. 汇编:在这一步,gcc会将汇编代码翻译成机器码

    gcc -c assembly_file.s -o object_file.o
    

    这个命令会将assembly_file.s文件进行汇编,并将结果保存到object_file.o文件中。

  4. 链接:在这一步,gcc会将目标文件(object_file.o)与必要的库文件链接在一起,生成最终的可执行文件

    gcc object_file.o -o executable_file
    

    这个命令会将object_file.o文件进行链接,并将结果保存到executable_file文件中。

166.动态库和静态库

静态链接动态链接的区别

静态库

静态库是在程序编译链接时将库文件的代码和数据复制到可执行文件中,因此静态库文件会增加可执行文件的大小。(静态库在文件中静态展开,所以有多少文件就展开多少次,非常吃内存,100M展开100次,就是1G,但是这样的好处就是静态加载的速度快)静态库对于使用它的程序来说是独立的,即使在没有该库文件的情况下,程序也能正常运行。每次更新静态库都需要重新编译和链接程序。

静态库适合于程序的可移植性要求高、不需要经常更新的情况下使用

动态库

动态库在程序运行时被加载到内存中,因此它不会增加可执行文件的大小。(使用动态库会将动态库加载到内存,10个文件也只需要加载一次,然后这些文件用到库的时候临时去加载,速度慢一些,但是很省内存)程序在运行时可以调用动态库中的函数等内容。动态库可以被多个程序共享,因此可以节约系统资源,但需要确保操作系统中已经安装了动态库文件。每次更新动态库时,只需要替换动态库文件即可。

动态库适合于资源共享、需要经常更新的情况下使用。

  1. 静态链接是将库的代码完全复制到可执行文件中。而动态链接是只将库的引用复制到可执行文件中,程序运行时才会动态加载库函数。

  2. 静态链接的程序文件相对比较大,因为它包含了程序所需要的所有代码和库函数。而动态链接的程序文件相对较小,因为它只是包含了库函数的引用。

  3. 静态链接的程序运行速度要比动态链接的程序快,因为它不需要动态加载库函数,所有代码都已经包含在程序中。而动态链接的程序运行速度要稍慢一些,因为它需要动态加载库函数。

  4. 静态链接的程序安全性较高,因为它不依赖于外部库文件,不易受到外界的攻击。而动态链接的程序安全性相对较低,因为它依赖于外部库文件,这些库文件可能会受到攻击。

  5. 静态链接适合编写小型程序,对程序体积和速度要求高的场合;动态链接适合编写大型程序,对程序体积和速度要求不那么高,但对程序的灵活性和可维护性要求比较高的场合。

167.makefile基础规则

1 个规则 2 个函数 3 个自动变量

1 个规则:

目标:依赖条件
	(一个tab缩进)命令

两个函数

1. src = $(wildcard *.c)
匹配当前工作用户下的所有.c文件。将文件名组成列表,赋值给变量src。
找到当前目录下所有后缀为.c的文件,赋值给src

2. obj = $(patsubset %.c,%.o, $(src))
将参数3中,包含参数1的部分,替换成参数2
把src变量里所有后缀为.c的文件替换成.o

3. 加了clean部分
模拟执行clean部分

3个自动变量

$@ :在规则命令中,表示规则中的目标
$< :在规则命令中,表示规则中的第一个条件,如果将该变量用在模式规则中,它可以将依赖条件列表中的依赖依次取出,套用模式规则
$^ :在规则命令中,表示规则中的所有条件,组成一个列表,以空格隔开,如果这个列表中有重复项,则去重

终极形态:

alt

168.硬链接与软链接的区别?

什么是符号链接?

什么是硬链接?

软链接和硬链接都是在Unix/Linux文件系统中使用的链接(linking)概念。

**链接:**是给系统中已有的某个文件指定另外一个可用于访问它的名称,链接也可以指向目录。即使我们删除这个链接,也不会破坏原来的文件或目录。

ln file file.h 创建一个硬链接,创建硬链接后,文件的硬链接计数+1

硬链接是指多个文件名指向同一个物理文件。当创建硬链接时,不会在磁盘上创建新的数据块,而是将已有文件的索引节点(inode)复制一份,新文件名指向该索引节点。因此,多个硬链接文件实际上是同一个文件,它们在磁盘上占用的空间是相同的。硬链接只能针对文件,不能针对目录

ln -s file file.s 创建一个软链接,软链接就像windows下的快捷方式

软链接又称符号链接,是指一个文件名指向另一个文件名,而不是物理文件。创建软链接时,在磁盘上创建一个新的数据块,其中包含指向目标文件名的路径信息。因此,软链接实际上是一个文件,它的内容是目标文件的路径。软链接可以针对文件或目录。

与硬链接不同,软链接在磁盘上占用的空间比较小,但是因为需要额外的寻址操作,访问速度相对较慢。同时,当目标文件被删除或移动时,软链接会失效。

硬链接和软链接的功能都是让一个文件名指向另一个文件名,但是它们的实现方式和特性不同。

硬链接的主要特点是:

  • 它们实际上是同一个文件,占用磁盘空间相同。
  • 它们可以独立地被访问、重命名、删除等操作,而不会影响其他的链接文件。
  • 硬链接只能链接普通文件,不能链接目录。

软链接的主要特点是:

  • 它们是一个新的文件,占用磁盘空间较小。
  • 它们指向的是目标文件的路径,因此,当目标文件被删除或移动时,软链接会失效。
  • 软链接可以链接普通文件和目录。

因此,尽管硬链接和软链接的功能相同,但它们的实现方式和使用场景不同。硬链接主要用于共享文件,而软链接主要用于解决文件路径的问题

169.文件描述符

alt

在Linux中,文件描述符是一个非负整数,它是用于标识打开的文件或其他I/O对象的抽象句柄。每个进程都有一个文件描述符表,其中包含了当前打开的文件、管道、套接字和其他I/O对象的描述符。

Linux中,标准的I/O操作都是通过文件描述符来进行的。例如,当一个进程需要读取或写入一个文件时,它需要打开文件并获取一个文件描述符,然后通过该描述符进行I/O操作。当I/O操作完成后,进程会关闭该文件并释放该文件描述符。

文件描述符通常用于底层系统编程和网络编程,因为它们提供了对文件和I/O对象的低级别访问。在Linux中,文件描述符的值通常从0开始,其中0、1和2分别代表标准输入、标准输出和标准错误。其他的文件描述符则是由系统自动分配的。

170.目录项和inode

alt

在Linux中,文件系统中的每个文件和目录都有一个唯一的标识符,这个标识符可以通过目录项和inode来表示。

目录项(directory entry)是指存储在目录中的文件或子目录的名称和相应的inode号码。目录项也包含其他元数据,如文件的权限、拥有者和创建时间等。

而inode是指存储在文件系统中的每个文件或目录的元数据信息,包括文件类型、权限、拥有者、创建时间、修改时间、访问时间等信息。每个文件或目录都有一个唯一的inode号码,该号码可以在文件系统中唯一地标识该文件或目录。

在Linux中,通过目录项中的文件名可以找到对应文件的inode号码,然后根据inode号码来获取文件的元数据信息和数据内容。

所谓的删除文件,就是删除inode,但是数据其实还是在硬盘上,以后会覆盖掉。

171./proc目录下,以数字命名的目录表示什么?

在Linux系统中,/proc目录是一个虚拟文件系统,它提供了一种访问内核数据结构和系统信息的方式。/proc目录下的以数字命名的目录表示系统中正在运行的进程的PID(进程ID),每个数字目录对应一个正在运行的进程。例如,/proc/1234目录表示进程ID为1234的进程。

这些数字目录中包含了该进程的各种信息,包括进程的状态、命令行参数、文件句柄、内存映射、CPU时间、网络连接等等。这些信息以文件的形式存在于数字目录中,可以使用cat等命令读取和查看。

/proc目录下的数字目录提供了一种方便的方式来查看和监控进程的运行状态和系统的运行情况,对于系统管理和调试都非常有用。

172.进程的地址空间模型?

进程地址空间详解_小赵小赵福星高照~的博客-CSDN博客

alt

text segment 存储代码的区域。
data segment 存储初始化不为0的全局变量和静态变量、const型常量。
bss segment 存储未初始化的、初始化为0的全局变量和静态变量。
heap(堆) 用于动态开辟内存空间。
memory mapping space(内存映射区) mmap系统调用使用的空间,通常用于文件映射到内存或匿名映射(开辟大块空间),当malloc大于128k时(此处依赖于glibc的配置),也使用该区域。在进程创建时,会将程序用到的平台、动态链接库加载到该区域。
stack(栈) 存储函数参数、局部变量。
kernel space 存储内核代码。
173.分别介绍父进程、子进程、进程组、作业和会话

alt

  1. 父进程:当一个进程被创建时,它会由一个已存在的进程(即父进程)创建。父进程是新创建进程的直接控制者,它可以监视、管理和控制子进程的运行。父进程可以与子进程共享信息和资源,也可以在需要时向子进程发送信号或其他指令。
  2. 子进程:子进程是由父进程创建的进程,它拥有独立的内存空间和资源。子进程可以执行不同的任务,或在父进程的指导下完成某些操作。子进程可以继承父进程的一些属性和资源,如环境变量、文件描述符等。
  3. 进程组:进程组是由一个或多个相关联的进程组成的集合。每个进程组都有一个唯一的进程组ID(GID),并且每个进程都属于一个进程组。进程组可以方便地对多个进程进行管理和控制,比如向整个进程组发送信号、挂起或恢复进程组等操作。
  4. 作业:一个作业是由一个或多个进程组成的一组任务。通常,一个作业由一个前台进程组和一个或多个后台进程组组成。前台进程组通常是用户当前正在交互的任务,而后台进程组则是在后台运行的任务。作业可以被挂起、恢复、终止等操作。
  5. 会话:一个会话是一个或多个进程的集合,这些进程共享同一个控制终端。当用户登录到一个系统时,系统会自动创建一个会话,该会话通常包含一个或多个进程组。会话可以管理和控制与终端相关的操作,如控制终端的输入和输出、接受和发送信号等操作。
174.fork函数

Linux中的fork()函数是一个创建新进程的系统调用。它会复制当前进程的一个副本,并且在新的进程中运行。这个新进程被称为子进程,而原始进程被称为父进程。父进程和子进程是通过进程ID来区分的。

当调用fork()函数时,操作系统会创建一个新的进程,并将所有的内存、寄存器和文件描述符等信息复制到这个新的进程中。父进程和子进程会在fork()函数的返回值上得到不同的结果。在父进程中,fork()会返回子进程的进程ID,而在子进程中,fork()会返回0。如果fork()返回-1,则表示创建新进程失败。

使用fork()函数可以实现多进程编程,这样可以在同一个程序中同时执行多个任务,从而提高程序的效率。例如,在Web服务器中,当有多个客户端请求时,可以通过fork()函数创建多个子进程来同时处理这些请求。

175.子进程从父进程继承的资源有哪些?

父子进程共享哪些内容

父进程fork出子进程,父进程中的变量和子进程中的变量有什么区别?>

父子进程相同:

​ 刚fork后。 data段、text段、堆、栈、环境变量、全局变量、宿主目录位置、进程工作目录位置、信号处理方式(0-3G的用户空间)

父子进程不同:

​ 进程id、返回值、各自的父进程、进程创建时间、闹钟、未决信号集。

父子进程共享:

​ 读时共享、写时复制。———————— 全局变量。

1.文件描述符(打开文件的结构体) 2. mmap映射区(进程间通信)。

176.exec函数族

alt

在Linux系统中,exec函数族是一组用于执行进程的系统调用函数,它们包括execl()execv()execle()execve()execlp()execvp()等函数。这些函数在执行时会替换当前进程的镜像,即用新的程序替代当前进程,从而实现执行新的程序

exec函数族的函数都具有以下特点:

  • 函数名中包含字母exec
  • 函数的第一个参数为要执行的程序或脚本的路径或名称。
  • 函数的第二个参数为一个字符数组(或可变参数),用于指定传递给程序的参数。
  • 函数返回值只有在出错时才会有值,一般为-1。

exec函数族中各个函数的不同之处在于传递参数的方式、参数的个数以及搜索可执行文件的方式等。比如,execl()函数使用可变参数列表的方式传递参数,execv()函数使用字符数组的方式传递参数,execlp()execvp()函数可以在系统环境变量PATH指定的目录中搜索可执行文件,而execle()execve()函数则可以通过指定环境变量来运行程序。

177.什么是孤儿进程,僵尸进程,守护进程?
  1. 孤儿进程

孤儿进程是指其父进程已经退出或结束,但子进程仍在运行的进程。在这种情况下,孤儿进程将成为系统进程(通常是init进程)的子进程。孤儿进程不会影响系统的正常运行,但它们可能会继续执行,并且可能会占用系统资源,直到它们完成执行或被强制终止。

  1. 僵尸进程

僵尸进程是指已经完成执行的进程,但它的状态信息仍然被保留在系统中,直到其父进程读取这些状态信息为止。(子进程死了,父进程没来得及收尸,就变僵尸了)僵尸进程不会再次运行,它们只是占用系统资源并占用进程表中的一个条目。如果系统中存在大量的僵尸进程,可能会导致进程表满,从而阻止新的进程启动。

  1. 守护进程

守护进程是一种在后台运行的进程,它通常是系统服务或其他长时间运行的任务。守护进程不依赖于任何终端或用户输入,通常在系统启动时自动启动,并在系统运行期间一直保持运行状态。守护进程通常不与用户交互,它们只执行特定的任务并定期向系统日志报告它们的状态。守护进程的一个常见例子是网络服务器。

178.wait函数

在Linux系统中,wait() 函数用于等待子进程结束并获取其退出状态。wait() 函数的原型如下:

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *status);

wait() 函数会阻塞父进程,直到有一个子进程结束。当一个子进程结束时,wait() 函数会获取该子进程的退出状态,并将其存储在 status 指向的内存空间中。如果 status 不为 NULL,那么wait() 函数会返回子进程的进程ID;如果 status 为 NULL,则不会获取子进程的退出状态。

需要注意的是,wait() 函数只会等待第一个结束的子进程,如果有多个子进程同时结束,那么其他子进程的退出状态将会被忽略。如果需要等待特定的子进程结束,可以使用 waitpid() 函数。

wait() 函数的返回值为子进程的进程ID,如果出现错误,则返回 -1。当 wait() 函数返回时,无论子进程是正常结束还是被信号杀死,都可以通过 status 指向的内存空间来获取子进程的退出状态。该状态包括子进程的退出码和一些其他信息,可以使用 WIFEXITED()WIFSIGNALED()WEXITSTATUS() 等宏来处理该状态。

179.如何清理僵尸进程?

僵尸进程的产生是因为父进程没有 wait() 子进程。所以如果我们自己写程序的话一定要在父进程中通过 wait() 来避免僵尸进程的产生。 当系统中出现了僵尸进程时,我们是无法通过 kill 命令把它清除掉的。但是我们可以杀死它的父进程, 让它变成孤儿进程,并进一步被系统中管理孤儿进程的进程收养并清理。

  1. 查看僵尸进程

在终端中运行命令ps aux | grep Z,该命令可以列出所有的僵尸进程。其中,ps命令用于列出所有进程的信息,aux选项表示列出所有用户的所有进程,grep Z选项表示只列出状态为“Z”的进程,即僵尸进程。

  1. 确认僵尸进程的父进程

记录下僵尸进程的PID(进程ID),然后运行命令ps -p <PID> -o ppid=来查看该进程的父进程PID。

  1. 结束僵尸进程的父进程

如果僵尸进程的父进程仍在运行,可以使用kill命令结束它。首先运行ps -p <PPID>来查看该进程的状态,如果其状态为“Z”,则表示它本身也是一个僵尸进程,需要结束其父进程。否则,使用命令kill <PPID>结束其父进程即可。

  1. 结束僵尸进程

如果僵尸进程的父进程已经不存在了,可以使用命令kill <PID>来结束僵尸进程。

180.管程是什么

使用信号量机制实现生产者消费者问题需要客户端代码进行很多控制,如对共享变量的访问、对信号量的操作等,这些控制代码可能比较复杂,容易出错,而且使得客户端代码难以维护和调试。

相比之下,管程把控制的代码独立出来,客户端代码只需要调用管程中提供的方法来实现对共享资源的访问和操作,使得客户端代码更加简单、可读性更强、更易于维护和调试。

Linux管程可以与内核进行直接交互,并利用内核提供的API和服务,实现自己的功能。它可以访问内核数据结构和函数,执行一些底层的操作,如文件系统、网络协议、设备驱动程序等。

在管程中,每个过程或方法都被定义为原子操作,执行时自动获得一个互斥锁(也称为管程锁),确保任何时刻只有一个线程可以进入管程并执行操作。当一个线程执行一个管程过程时,如果发现共享资源处于忙状态,它会自动阻塞等待,直到共享资源空闲并可以被访问。

管程还提供了条件变量,用于在等待某些条件满足时暂停线程并释放互斥锁。当条件满足时,其他线程会通知等待的线程并重新获得互斥锁,从而继续执行。

管程的使用可以避免死锁、饥饿和竞态条件等多线程并发编程中的问题。

181.网络字节序

在 Linux 网络编程中,网络字节序是指在网络上传输时使用的字节序,它是一种规范的字节序,确保不同计算机之间的数据传输的正确性。

在计算机内部,数据的表示可以使用两种字节序,即大端字节序和小端字节序。大端字节序是指数据的高位字节存放在内存的低地址中,而小端字节序是指数据的低位字节存放在内存的低地址中。

为了在网络上传输时保证数据的正确性,所有计算机都必须使用相同的字节序。在网络编程中,网络字节序被规定为大端字节序,无论计算机的实际字节序是大端还是小端,都必须将数据转换为网络字节序后再进行传输。

在 Linux 网络编程中,可以使用一些函数来进行字节序的转换,如htonl()、htons()、ntohl() 和 ntohs(),它们分别表示将一个 32 位整数从主机字节序转换为网络字节序、将一个 16 位整数从主机字节序转换为网络字节序、将一个 32 位整数从网络字节序转换为主机字节序、将一个 16 位整数从网络字节序转换为主机字节序。

182.请你来说一下socket编程中服务器端和客户端主要用到哪些函数?

请问你有没有基于做过socket的开发?具体网络层的操作该怎么做?

请你讲述一下Socket编程的send() recv() accept() socket()函数?

socket编程的流程?

alt

**基于TCP的socket **

服务器端程序

(1)创建一个socket,用函数socket()

(2)绑定IP地址、端口等信息到socket上,用函数bind()

(3)设置允许的最大连接数,用函数listen()

(4)接收客户端上来的连接,用函数accept()

(5)收发数据,用函数send()和recv(),或者read()和write()

(6)关闭网络连接。

客户端程序:

(1)创建一个socket,用函数socket()

(2)设置要连接的对方的IP地址和端口等属性

(3)连接服务器,用函数connect()

(4)收发数据,用函数send()和recv(),或read()和write()

(5)关闭网络连接

基于UDP的socket

服务器端流程 (1)建立套接字文件描述符,使用函数socket(),生成套接字文件描述符。

(2)设置服务器地址和侦听端口,初始化要绑定的网络地址结构。

(3)绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地址类型变量进行绑定。

(4)接收客户端的数据,使用recvfrom()函数接收客户端的网络数据。

(5)向客户端发送数据,使用sendto()函数向服务器主机发送数据。

(6)关闭套接字, 使用close()函数释放资源。UDP协议的客户端流程。

客户端流程

(1)建立套接字文件描述符,socket()。

(2)设置服务器地址和端口,struct sockaddr。

(3)向服务器发送数据,sendto()。

(4)接收服务器的数据,recvfrom()。

(5)关闭套接字,close()。

基于TCP的socket代码如下:

client.c的作用是从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define MAXLINE 80
#define SERV_PORT 6666

int main(int argc, char *argv[])
{
	struct sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd, n;
    char *str;

	if (argc != 2) {
		fputs("usage: ./client message\n", stderr);
		exit(1);
	}
    str = argv[1];

	sockfd = socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
	servaddr.sin_port = htons(SERV_PORT);

	connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

	write(sockfd, str, strlen(str));

	n = read(sockfd, buf, MAXLINE);
	printf("Response from server:\n");
	write(STDOUT_FILENO, buf, n);
	close(sockfd);

	return 0;
}

server.c的作用是从客户端读字符,然后将每个字符转换为大写并回送给客户端。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAXLINE 80
#define SERV_PORT 6666

int main(void)
{
	struct sockaddr_in servaddr, cliaddr;
	socklen_t cliaddr_len;
	int listenfd, connfd;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int i, n;

	listenfd = socket(AF_INET, SOCK_STREAM, 0);

	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);

	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
	listen(listenfd, 20);

	printf("Accepting connections ...\n");
	while (1) {
		cliaddr_len = sizeof(cliaddr);
		connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
		n = read(connfd, buf, MAXLINE);
		printf("received from %s at PORT %d\n",
		inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
		ntohs(cliaddr.sin_port));
		for (i = 0; i < n; i++)
			buf[i] = toupper(buf[i]);
		write(connfd, buf, n);
		close(connfd);
	}
	return 0;
}
183.网络编程中设计并发服务器,使用多进程与多线程 ,请问有什么区别?

alt

  1. 多进程并发服务器:每个客户端请求都将创建一个新的进程,这个进程负责处理该客户端的请求。因为每个进程都是独立的,所以它们之间的内存空间是隔离的。这意味着在进程间共享数据需要使用进程间通信(IPC)技术,如管道、信号、共享内存、套接字等。由于进程切换的开销比较大,因此多进程并发服务器的性能通常比多线程并发服务器要差。
  2. 多线程并发服务器:在多线程并发服务器中,每个客户端请求都将创建一个新的线程,这个线程负责处理该客户端的请求。由于所有线程都属于同一个进程,它们共享同一个地址空间,可以轻松地共享数据,不需要进行进程间通信。由于线程切换的开销比进程切换的开销小得多,因此多线程并发服务器的性能通常比多进程并发服务器要好。但是,多线程编程需要注意线程安全问题,例如数据共享、竞态条件、死锁等。
184.什么是IO多路复用

IO多路复用是一种高效的IO操作方式,它可以同时监听多个文件描述符(socket)的可读、可写、异常等事件,并在有事件发生时通知应用程序进行处理。常见的IO多路复用机制有select、poll、epoll等。

在传统的IO模型中,每个文件描述符都需要对应一个线程来处理,这会导致系统资源的浪费和线程切换的开销。而使用IO多路复用机制,可以将多个文件描述符的IO事件集中到一个线程中处理,减少了系统调用和线程切换的次数,提高了系统的吞吐量和响应性能

例如,在一个聊天室服务器中,需要同时监听多个客户端连接的读写事件,如果每个客户端连接都对应一个线程来处理,会导致线程数过多,而使用IO多路复用机制,则可以将多个客户端连接的事件集中到一个线程中处理,减少了系统资源的浪费和线程切换的开销。

185.select是什么?

在 Linux 网络编程中,select 是一种系统调用,用于在多个文件描述符上等待数据可读、数据可写或出现异常情况。

select 函数会阻塞当前线程,直到指定的文件描述符上有数据可读、数据可写或出现异常情况。在返回之前,select 会修改文件描述符集合,指示哪些文件描述符上发生了事件。因此,通过在 select 调用之前设置文件描述符集合,程序可以监视多个文件描述符上的事件。

下面是 select 函数的原型和参数说明:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:要监视的最大文件描述符值加一。
  • readfds:指向可读文件描述符集合的指针。
  • writefds:指向可写文件描述符集合的指针。
  • exceptfds:指向出现异常情况的文件描述符集合的指针。
  • timeout:select 函数的超时时间,如果为 NULL,则一直阻塞直到有事件发生。

在函数返回后,可以使用下面的宏函数检查文件描述符集合中的事件:

  • FD_ISSET(fd, fdset):检查文件描述符 fd 是否在 fdset 集合中。
  • FD_SET(fd, fdset):将文件描述符 fd 加入到 fdset 集合中。
  • FD_CLR(fd, fdset):将文件描述符 fd 从 fdset 集合中移除。
  • FD_ZERO(fdset):清空 fdset 集合。

需要注意的是,select 函数的效率并不高,因为它需要轮询多个文件描述符,而且在文件描述符集合较大时性能会受到影响。因此,在需要同时监视大量文件描述符的场景下,通常会使用更高效的事件驱动框架,如 epoll 。

186.epoll是什么?

epoll是一种高效的I/O多路复用机制,是Linux特有的系统调用,相比于select和poll,它具有以下优点:

  1. 支持大量的并发连接:epoll可以处理上万个连接而不会受到系统资源的限制。
  2. 高效:在处理大量连接时,epoll的效率远高于select和poll。
  3. 内核与用户空间内存交互减少:在调用epoll_wait()时,它只需要向内核传递一次事件表的地址,而不需要每次调用都向内核传递,这减少了内核与用户空间之间的内存交互次数。
  4. 事件被唤醒时会立即返回:当epoll监听的文件描述符发生变化时,epoll_wait()会立即返回,而不像select和poll一样需要逐个遍历文件描述符,这减少了CPU的开销。
187.epool中et和lt的区别与实现原理

LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。

188.select、poll、epoll区别
  1. select:

select支持的文件描述符数量有限,一般为1024个

挨个检测满足条件的fd,需要自己添加业务逻辑(数组存放满足条件的fd,找的更快,不用把所有文件描述符轮一遍),提高了编码难度

  1. poll:

自带数组/链表结构,可以增加需要监听的文件描述符,突破监听上限受文件描述符限制

无法直接定位满足监听事件的文件描述符

select和poll的缺点在于,当文件描述符数量较多时,轮询的效率会降低,而且每次轮询时需要将整个数组或链表遍历一遍,效率不高。

  1. epoll:

epoll采用了事件通知的方式,当文件描述符就绪时,内核会向应用程序发送一个事件通知,应用程序只需要处理这些事件即可。还提供了两种工作模式:LT(Level Triggered)和ET(Edge Triggered)。LT模式是默认模式,当文件描述符就绪时会持续通知应用程序,直到应用程序将该文件描述符处理完毕;而ET模式只在文件描述符状态改变时通知应用程序,如果应用程序没有将该文件描述符处理完毕,下一次就不会再收到通知。

使用了红黑树的数据结构来存储需要监听的文件描述符,可以快速的插入、删除和查找文件描述符,因此效率较高。同时,epoll支持的文件描述符数量没有限制。

189.什么是线程池?

线程池是一种常见的多线程并发编程技术,它是一组线程的集合,这些线程预先创建并初始化,并被放入一个队列中等待任务。当有新任务到来时,线程池中的某个线程会被唤醒并处理该任务,任务处理完后,线程又会回到线程池中等待下一次任务。通过线程池技术,我们可以实现高效、可伸缩的并发处理,提高系统的并发处理能力,降低系统的开销和复杂度。

线程池的主要组成部分包括任务队列、线程池管理器和工作线程。任务队列用于存储所有需要处理的任务,线程池管理器用于管理线程池的创建、销毁和线程的调度等操作,工作线程则是线程池中的执行单位,它们从任务队列中取出任务并执行任务。线程池通常采用预创建线程的方式,通过线程复用的方式避免了线程频繁创建和销毁所带来的开销

Linux内核

190.Linux内核五大功能/子系统

什么是 Linux 内核?

Linux内核是Linux操作系统的核心部分,负责管理和控制系统的各种硬件和软件资源。其五大功能包括:

  1. **进程管理:**Linux内核负责管理系统中所有的进程和线程,包括进程的创建、销毁、调度和同步等。它还负责为进程分配和管理系统资源,如CPU时间、内存、文件句柄等。
  2. **内存管理:**Linux内核负责管理系统中的内存资源,包括内存分配、释放和回收等。它还负责为进程提供虚拟内存管理功能,包括内存映射、分页、缓存和交换等。
  3. **文件系统管理:**Linux内核支持多种文件系统,包括常见的ext4、NTFS、FAT32等。它负责管理文件系统中的文件和目录,提供文件读写、权限管理、硬链接和软链接等功能。
  4. **设备驱动管理:**Linux内核支持多种硬件设备,如磁盘驱动器、网卡、USB设备等。它负责管理和控制这些设备,提供设备驱动程序和设备文件等接口,使应用程序可以访问和控制这些设备。
  5. **网络协议管理:**Linux内核支持多种网络协议,包括TCP/IP、UDP、ICMP等。它负责管理网络连接、数据传输和协议处理,提供网络套接字和套接字接口等接口,使应用程序可以访问和控制网络资源。
191.得到一个Linux Kernel的软件包后编译安装的过程。

编译和安装Linux内核的过程一般包括以下几个步骤:

1.安装前置软件包

2.下载并解压内核源码包

将下载的Linux内核软件包解压缩到一个目录中,例如:

$ tar xvf linux-x.y.z.tar.gz
$ cd linux-x.y.z

3.配置交叉编译工具链

vim ~/.bashrc
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihfexport PATH=$PATH:/home/用户名/100ask_firefly-rk3288/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin

4.配置内核

在Linux内核目录下执行 make menuconfig 命令,通过交互式的方式配置内核选项。在这个过程中,可以选择需要编译进内核的模块和驱动程序、修改内核的配置参数等等。完成配置后,将配置保存到 .config 文件中,例如:

$ make deconfig
$ make menuconfig
$ make savedefconfig

5.编译内核

执行 make 命令编译内核。编译时间可能会比较长,具体取决于计算机的性能和内核的复杂程度。可以通过指定 -j 选项使用多个CPU核心并行编译,例如:

$ make -j8

6.安装内核

编译完成后,执行 make modules_install 命令安装内核模块,然后执行 make install 命令将内核安装到系统中。这个过程会将内核文件复制到/boot目录下,并更新系统引导程序的配置文件。例如:

$ make modules_install
$ make install

7.重启系统

完成内核安装后,重启系统以应用新的内核。在引导时,选择新内核启动即可。

注意:在执行上述过程之前,应当备份重要的系统文件和数据,以避免意外情况发生导致数据丢失或系统不稳定。此外,对于生产环境的系统,建议在正式应用前进行充分的测试和验证。

192.Linux内核源码启动分析
  1. BIOS/UEFI阶段:计算机开机后首先会执行BIOS或UEFI程序,进行一些硬件初始化操作,如检查硬件配置信息、启动自检程序、加载操作系统引导程序等。
  2. Bootloader阶段:BIOS/UEFI会加载引导程序,比如GRUB等,这个引导程序会在屏幕上显示一个菜单,供用户选择要启动的操作系统,如果只有一个操作系统,那么该引导程序将自动启动内核。引导程序会根据用户选择或默认设置找到内核映像文件,加载到内存中。
  3. 内核启动自解压阶段:内核启动时,它首先会解压缩自身,然后进行一系列的初始化工作,如初始化CPU、内存管理、设备管理、文件系统等。其中,内存管理是最重要的一步,因为内核需要将系统中的所有可用内存映射到自己的地址空间中。
  4. 内核引导阶段:
  5. init进程启动阶段:当内核初始化完毕后,会启动init进程。在Linux系统中,init进程是用户空间中的第一个进程,它负责初始化系统环境,包括加载配置文件、启动系统服务等。
  6. 过渡到rootfs
193.为什么会有上下文这种概念?

在计算机系统中,上下文是指当前程序或进程执行的环境和状态,包括程序的执行位置、寄存器内容、堆栈信息、打开文件等。操作系统需要在多个进程之间进行快速的切换,这就需要在进程间保存和恢复上下文信息。

在一个计算机系统中,只有一个CPU,但是可能有多个进程或线程在同时运行。当操作系统需要将CPU从一个进程切换到另一个进程时,必须保存当前进程的上下文信息,并加载下一个进程的上下文信息,从而让它继续执行。这种切换称为上下文切换

上下文的概念也在操作系统的其他方面得到了应用,例如在中断处理中,需要保存当前执行的进程上下文,以便中断处理程序执行完毕后能够恢复到之前的状态。在多线程编程中,线程切换也需要保存和恢复上下文信息。

内核空间和用户空间是现代操作系统的两种工作模式,内核模块运行在内核空间,而用户态应用程序运行在用户空间。它们代表不同的级别,而对系统资源具有不同的访问权限。内核模块运行在最高级别(内核态),这个级下所有的操作都受系统信任,而应用程序运行在较低级别(用户态)。在这个级别,处理器控制着对硬件的直接访问以及对内存的非授权访问。内核态和用户态有自己的内存映射,即自己的地址空间。

其中,处理器总处于以下状态中的一种:

  • 内核态,运行于进程上下文,内核代表进程运行于内核空间;

  • 内核态,运行于中断上下文,内核代表硬件运行于内核空间;

  • 用户态,运行于用户空间。

系统的两种不同运行状态,才有了上下文的概念。用户空间的应用程序,如果想请求系统服务,比如操作某个物理设备,映射设备的地址到用户空间,必须通过系统调用来实现。通过系统调用,用户空间的应用程序就会进入内核空间,由内核代表该进程运行于内核空间,这就涉及到上下文的切换,用户空间和内核空间具有不同的地址映射,通用或专用的寄存器组,而用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行。

上下文概念的引入,保证了系统的并发性和可靠性,提高了系统的性能和效率。

194.什么是内核态和用户态?

Linux kernel和一般程序的区别是什么?

当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。

  • 从系统资源访问方面

在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令,操作系统的任何资源,包括硬件。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。 在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查,不能直接访问系统硬件,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中 I/O 许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。

  • 从出错危害性方面

对于以前的 DOS 操作系统来说,是没有内核空间、用户空间以及内核态、用户态这些概念的。可以认为所有的代码都是运行在内核态的,因而,用户编写的应用程序代码可以很容易的让操作系统崩溃掉。 对于 Linux 来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码(操作系统的代码要比应用程序的代码健壮很多)与应用程序代码。即便是单个应用程序出现错误,也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行。但是内核模块出错,有可能导致内核崩溃,只能重启系统。所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。

Linux使用Ring3级别运行用户态, RIng0作为内核态, Ring3状态不能访问RIng0的地址空间, 包括数据和代码,【看硬件相关ARM64部分】

Linux的4G地址空间, 前3G是用户空间, 后面1G是内核态的地址空间, 是共享的, 存放了整个内核的代码和内核模块以及内核所维护的数据.

195.用户空间与内核通信方式有哪些?

用户态->内核态

系统调用

open(),read(),write(), ioctl(),

内核态->用户态

API

get_user(x,ptr)/put_user(x,ptr):在内核中被调用,获取用户空间指定地址的数值并保存到内核变量x中。用于访问少量的数据。

Copy_from_user() / copy_to_user():主要应用于设备驱动读写函数中。适用于传输较大的数据。

信号 从内核空间向进程发送信号。用户程序出现重大错误,内核发送信号杀死相应进程。如SIGSEGV、SIGILL、SIGPIPE等。

文件 应该说这是一种比较笨拙的做法,不过确实可以这样用。当处于内核空间的时候,直接操作文件, 将想要传递的信息写入文件,然后用户空间可以读取这个文件便可以得到想要的数据了。下面是一个简单的测试程序,在内核态中,程序会向“/home/melody/str_from_kernel”文件中写入一条字符串,然后我们在用户态读取这个文件,就可以得到内核态传输过来的数据了。

内核态<->用户态

mmap共享内存 可以将内核空间的地址映射到用户空间。一方面可以在driver中修改Struct file_operations结构中的mmap函数指针来重新实现一个文件对应的映射操作。另一方面,也可以直接打开/dev/mem文件,把物理内存中的某一页映射到进程空间中的地址上。 其实,除了重写Struct file_operations中mmap函数,我们还可以重写其他的方法如ioctl等,来达到驱动内核空间和用户空间通信的方式。

虚拟文件系统

  1. sysfs文件系统+kobject 每个在内核中注册的kobject都对应着sysfs系统中的一个目录。可以通过读取根目录下的sys目录中的文件来获得相应的信息。

  2. proc文件系统。 和sysfs文件系统类似,也可以作为内核空间和用户空间交互的手段。/proc 文件系统是一种虚拟文件系统。与普通文件不同,这里的虚拟文件的内容都是动态创建的。使用/proc文件系统的方式很简单。调用create_proc_entry,返回一个 proc_dir_entry指针,然后去填充这个指针指向的结构就好了。

  3. debugfs文件系统

netlink netlink socket,用于用户态和内核态的通信。相比于其他的方式,netlink有几个好处:1.使用自定义一种协议完成数据交换,不需要添加一个文件等。2.可以支持多点传送。3.支持内核先发起会话。4.异步通信,支持缓存机制。

196.应用程序中open()在linux中执行过程中是如何从用户空间到内核空间?

Linux字符设备中的两个重要结构体(file、inode) - GreenHand# - 博客园 (cnblogs.com)

3.1 linux驱动之linux中file, cdev, inode之间的关系_哔哩哔哩_bilibili

用户空间使用open()系统调用函数打开一个字符设备时( int fd = open("dev/demo", O_RDWR) )大致有以下过程:

  1. 在虚拟文件系统VFS中的查找对应与字符设备对应 struct inode节点
  2. 遍历字符设备列表(chardevs数组),根据inode节点中的 cdev_t设备号找到cdev对象
  3. 创建struct file对象(系统采用一个数组来管理一个进程中的多个被打开的设备,每个文件描述符作为数组下标标识了一个设备对象)
  4. 初始化struct file对象,将 struct file对象中的 file_operations成员指向 struct cdev对象中的 file_operations成员)
  5. 回调file->fops->open函数,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。
197.Linux中断为什么要区分上半部和下半部?

在Linux中,中断通常被划分为上半部(也称为快速路径)和下半部(也称为慢速路径),主要是为了提高系统的性能和可靠性。

具体来说,当内核响应一个中断时,它首先会执行上半部的中断处理程序,该程序通常是一些轻量级的操作,如保存寄存器、更新硬件状态等。上半部的中断处理程序通常需要在短时间内完成,以便尽快响应其他中断和系统调用。

下半部的中断处理程序则是一些较重的操作,如磁盘I/O、网络协议栈等。这些操作通常需要较长时间才能完成,如果将它们放在中断处理程序中,会导致中断响应时间过长,影响系统的性能和可靠性。

因此,在Linux中,内核将中断处理程序分成上半部和下半部,上半部的中断处理程序通常在中断上下文中执行,而下半部的中断处理程序则延迟到后续的软中断或工作队列中执行,以避免中断响应时间过长,提高系统的性能和可靠性。

198.中断下半部一般如何实现?

Linux驱动中断下半部的三种方法-linux运维-PHP中文网

中断下半部一般可以通过软中断、tasklet、工作队列来实现。

  1. softirq:软中断是效率最高的一种方式;在中断上下文运行,不可睡眠;可并发执行,函数要求可重入;需要静态定义;在do_softirq()中执行,执行时机是硬中断返回后、以及系统负荷小时运行。

    软中断使用的几个要点

    • 一个软中断不会抢占另外一个软中断。
    • 惟一可以抢占软中断的是中断处理程序。
    • 其他的软中断可以在其他处理器上同时执行。
  2. tasklet:实际上是利用软中断实现的,所以继承了软中断的特性,尤其注意不可睡眠;由于是在softirq上重封装,所以易用性大大优于softirq,而效率上略微降低;可动态申请;同一时间相同的tasklet不会并发执行,不需要考虑重入。

  3. workqueue:工作队列是通过工作在内核线程实现的,运行在进程上下文,可以睡眠;响应不如软中断及时。

199.Linux内核有哪些同步方式?

共享资源防冲突使用有什么手段?

原子操作

不被打断,放在硬件驱动最底层,只有一个进程能用,释放后其他进程才能用

中断屏蔽

进程中一段程序中不想被中断打断(可能这段程序和中断程序都会操作一个设备),可用中断屏蔽,时间短,最好一个函数内使用

  • local_irq_disable(); //屏蔽所有中断
  • local_irq_save(flags); //屏蔽所有中断,可恢复
  • disable_irq(int irq); //屏蔽指定中断号的中断

自旋锁

一个线程操作一个设备,另一个线程也操作这个设备,那就得一直等第一个用完,但是等待时间不长

时间短,最好一个函数内使用,while循环等待释放锁,一个进程用完下一个进程用。不能睡眠,可以中断中使用

  • spin_lock(&slock);
  • spin_try_lock(); //为避免很卡
  • spin_lock_irqsave(&db->lock, flags); //自旋锁 + 中断屏蔽,防锁状态变换时进入中断,浪费时间

互斥体

一个线程操作一个设备,另一个线程也操作这个设备,那得等第一个用完,但中间可以睡眠,去做其他事。不能中断中使用。

也叫互斥锁,互斥量,会阻塞睡眠,可长时间,不能用于中断中,cpu去处理其他进程了,等下一次轮过来再检测好了没

  • mutex_init(&lock); //初始化互斥锁
  • mutex_lock(&lock); //上锁 (无法获得,则阻塞睡眠)
  • mutex_unlock(&lock); //解锁

信号量

【操作系统,这个底层实现应该是靠记录型信号量】

变量+1-1,和互斥体差不多,资源数不为1时可以做资源的计数,而互斥体不行

可以同时给多个进程用,但是计数到了就不能让其他进程再用了,可以睡眠

读写锁

如视频会议

摄像头,网卡,GPU往内存中写,要求写写互斥

读写也要互斥,要不每个人读的可能不一样,

如果想要收到的都是美颜后的,那要写优先(顺序锁),如果追求速度,那要读优先(RCU)

读读可以不互斥,各自读各自的就行

读写优先级相同,

无法保证读优先。写饥饿。

顺序锁

写优先

把负担丢个读者(重读,冲突判断)

读拷贝更新RCU

主要是读写互斥,读优先

写时先拷贝,合适时再更新,让读者优先,不用等待,读的时候直接指针指向更新好的。

中原互旋号,顺序读写更新

200.自旋锁和信号量可以睡眠吗?为什么?

自旋锁不能睡眠,信号量可以。 原因 自旋锁自旋锁禁止处理器抢占;而信号量不禁止处理器抢占。 基于这个原因,如果自旋锁在锁住以后进入睡眠,由于不能进行处理器抢占,其他系统进程将都不能获得CPU而运行,因此不能唤醒睡眠的自旋锁,因此系统将不响应任何操作。而信号量在临界区睡眠后,其他进程可以用抢占的方式继续运行,从而可以实现内存拷贝等功能而使得睡眠的信号量程序由于获得了等待的资源而被唤醒,从而恢复了正常的代码运行。自旋锁的睡眠的情况包含考虑多核CPU和中断的因素。自旋锁睡眠时,只是当前CPU的睡眠以及当前CPU的禁止处理器抢占,所以,如果存在多个CPU,那么其他活动的CPU可以继续运行使操作系统功能正常,并有可能完成相应工作而唤醒睡眠了的自旋锁,从而没有造成系统死机;自旋锁睡眠时,如果允许中断处理,那么中断的代码是可以正常运行的,但是中断通常不会唤醒睡眠的自旋锁,因此系统仍然运行不正常。

201.自旋锁和信号量可以用于中断中吗?

信号量不能用于中断中,因为信号量会引起睡眠,中断不能睡眠。 **自旋锁可以用于中断。**在获取锁之前一定要先禁止本地中断(也就是本CPU中断,对于多核SOC来说会有多个CPU核),否则可能导致锁死现象的发生。

202.内存资源管控

图片上传失败,请重新上传

  • 缓冲缓存一般系统就弄好了
  • DMA
  • SWAP
  • 内存池,频繁申请释放,CPU不断操作,浪费时间,单独划出一片区域让别人随便用就行
  • 内存泄漏
  • 环形缓存区,如摄像头写,网卡/LCD读,也就是生产者消费者问题,环形缓存区大小与读取速度有关,可以做到不用等
  • 映射MMU
  • mmap,不需要频繁从内核拷到应用,指向同一块内存区域就行

图片上传失败,请重新上传

  • 通过MMU,将内存和其他物理设备地址映射到应用层和内核层,页表将对应关系写在一起
  • 应用层:代码段、数据段、堆、栈
  • 内核层:静态映射、动态映射
  • 内核直接映射,直接对应内存中的空间
  • 内核动态映射,可以把硬件地址映射过来
  • 共享内存,让两进程指向同一内存空间,进程与进程间传数据最高效的方式
  • 文件内存映射,让两进程指向同一存储空间,可以是任意地方
共享内存和文件内存映射都属于内存共享机制,但它们的实现方式和应用场景有所不同。

共享内存是一种内存共享方式,通过申请一块共享内存区域,多个进程之间可以共享该内存区域中的数据,从而实现进程间的通信。共享内存通常用于大量数据交换场景,比如传输图像、音频等大文件数据。共享内存的优点是速度快、延迟低,但由于操作系统没有提供文件系统的保护机制,因此需要应用程序自行处理同步和互斥的问题。

文件内存映射是一种以文件为基础的内存共享方式,通过将文件映射到进程的虚拟内存空间中,实现对文件的读写操作,多个进程之间可以共享该内存映射区域。文件内存映射通常用于处理文件数据、数据库等应用场景。文件内存映射的优点是易于使用、具有强大的保护机制,可以使用标准文件操作函数进行读写操作,且不需要自行实现同步和互斥问题,但由于需要经过文件系统的逻辑处理,因此速度相对较慢。

两缓,DS池漏环MMs

203.什么是MMU?为什么需要MMU?

MMU(Memory Management Unit)是一种硬件设备,主要用于实现虚拟内存管理。它的作用是将进程所使用的虚拟地址转换成对应的物理地址,并进行内存保护

在没有MMU的系统中,所有进程共享同一块物理内存,因此进程间需要通过约定好的内存地址来进行通信,容易导致地址冲突和安全问题。而有了MMU之后,每个进程都有自己的虚拟地址空间,不会互相干扰。MMU还可以根据进程的访问权限,对虚拟地址空间进行访问控制和内存保护。

此外,MMU还可以通过虚拟地址和物理地址的映射关系,实现了虚拟内存技术,使得进程能够访问大于物理内存的虚拟地址空间,从而提高了内存利用率和系统性能

204.线程是否具有相同的堆栈?

在同一进程中的线程共享相同的虚拟地址空间,因此它们共享进程的堆和内存映射区域。但是,每个线程都有其自己的栈空间,线程之间不共享栈空间。因此,每个线程都有自己的堆栈。这是因为线程栈中存储了线程执行的函数调用、局部变量、返回地址等线程相关的信息,不同线程的这些信息不同,如果共用一个栈就会出现互相干扰的情况,导致程序出错。因此,为了保证线程之间的独立性和安全性,每个线程都需要独立的栈空间。

205.内核程序中申请内存使用什么函数?

图片上传失败,请重新上传

内核中的内存申请:kmalloc、vmalloc、kzalloc、kcalloc、devm_kzalloc() 、get_free_pages

  • kmalloc
void *kmalloc(size_t size, gfp_t flags)

kmalloc是内核中最常用的一种内存分配方式,它通过调用kmem_cache_alloc函数来实现。kmalloc一次最多能申请的内存大小由include/linux/Kmalloc_size.h的内容来决定,在默认的2.6.18内核版本中,kmalloc一次最多能申请大小是128KB字节的连续物理内存

对于kmalloc()申请的内存,需要使用kfree()函数来释放;

备注:kmalloc是基于slab机制实现的;

  • vmalloc
void *vmalloc(unsigned long size)

在某些场合中,对内存区的请求不是很频繁,较高的内存访问时间也可以接受,这是就可以分配一段线性连续,物理不连续的地址,带来的好处是一次可以分配较大块的内存。 vmalloc对一次能分配的内存大小没有明确限制。出于性能考虑,应谨慎使用vmalloc函数。在测试过程中,最大能一次分配1GB的空间。注意:vmalloc()和vfree()可以睡眠,因此不能在中断上下文调用。

对于vmalloc()申请的内存,需要使用vfree()函数来释放;

备注:vmalloc是基于slab机制实现的;

  • kzalloc

kzalloc()函数功能同kmalloc()。区别:内存分配成功后清零。

  • kcalloc

kcalloc()函数为数组分配内存,大小n*size,并对分配的内存清零。该函数的最终实现类似kmalloc()函数。

每次使用kcalloc()后,都要有对应的内存释放函数kfree()

  • **devm_kzalloc() **

devm_kzalloc() 和kzalloc()一样都是内核内存分配函数,但是devm_kzalloc()是跟设备有关的,当设备被detached或者驱动卸载时,内存会被自动释放。使用函数devm_kfree()释放。而kzalloc() 必须手动释放(配对使用kfree()),但如果工程师检查不仔细,则有可能造成内存泄漏,devm_kzalloc 有在统一设备模型的设备树记录空间,有自动释放的第二道防线,更安全。

  • __get_free_page()

get_zeroed_page()__get_free_page():用于分配一页物理内存,前者会将页清零,后者不会。这两个函数使用的是伙伴分配器。

  • alloc_page()

alloc_page():和__get_free_page()类似,用于分配一页物理内存,但是返回的是一个页描述符指针,需要使用page_address()函数将其转换为物理地址。

  • get_free_pages

_get_free_pages()函数是页面分配器提供给调用者的最底层的内存分配函数,它申请的内存也是连续的物理内存,同样位于物理内存映射区;它是基于buddy机制实现的;在使用buddy机制实现的物理内存管理系统中,最小的分配粒度(单位)也是以页为单位的;在__get_free_pages()内部通过调用alloc_pages()来分配物理内存页; __get_free_page()函数分配的是连续的物理内存,处理的是连续的物理地址,但是返回的也是虚拟地址(线性地址);如果想要得到正确的物理地址,也需要使用virt_to_phys()可进行转换;

对于__get_free_pages()函数申请的内存,需要使用__free_pages()函数来释放;

备注:__get_free_pages是基于buddy机制实现的;

  • dma_alloc_coherent
void *dma_alloc_coherent(struct device *dev, size_t size,ma_addr_t *dma_handle, gfp_t gfp)

DMA是一种硬件机制,允许外围设备和主存之间直接传输IO数据,而不需要CPU的参与,使用DMA机制能大幅提高与设备通信的吞吐量。DMA操作中,涉及到CPU高速缓存和对应的内存数据一致性的问题, 必须保证两者的数据一致,在x86_64体系结构中,硬件已经很好的解决了这个问题, dma_alloc_coherent和get_free_pages函数实现差别不大,前者实际是调用alloc_pages函数来分配内 存,因此一次分配内存的大小限制和后者一样。__get_free_pages分配的内存同样可以用于DMA操作。 测试结果证明,dma_alloc_coherent函数一次能分配的最大内存也为4M。

  • ioremap
void * ioremap (unsigned long offset, unsigned long size)

ioremap是一种更直接的内存“分配”方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段物理地址映射到内核地址空间。ioremap用到的物理地址空间都是事先确定的,和上面的几种内存分配方式并不太一样,并不是分配一段新的物理内存。ioremap多用于设备驱动,可以让CPU直接访问外部设备的IO空间。ioremap能映射的内存由原有的物理内存空间决定,所以没有进行测试。

  • kmem_cache_alloc()

kmem_cache_alloc()kmem_cache_zalloc():用于分配一个内核高速缓存中已经预先分配好的内存块,这个函数使用slab分配器实现,可以快速高效地分配内存。

Linux驱动

206.编译的两种方法
  • 编译成模块

固定的makefile,只需修改Linux源码路径,交叉编译路径

  • 编译进内核

放到内核的驱动目录下、写个Kconfig、再make menuconfig中对应选上生成.config配置文件(make的时候就会根据.config编译进去了)make之前写个makefile,并修改上一级makefile。然后make,生成.bin文件,做成镜像。

207.insmod,rmmod一个驱动模块,会执行模块中的哪个函数?在设计上要注意哪些问题?

当使用 insmod 命令将一个驱动模块插入内核时,模块中的 _init 函数将会被调用。这个函数用于初始化模块并向内核注册模块所提供的设备驱动程序。

当使用 rmmod 命令从内核中移除一个驱动模块时,模块中的 _exit 函数将会被调用。这个函数用于清理和卸载模块。

要注意的就是,尽量使在 init函数中出现的资源申请及使用,都要有对应的释放操作在exit中,即init申请,eixt释放。

208.设备驱动的分类

字符设备有哪些?和块设备有什么区别?

(按共性分类方便管理)

1.字符设备驱动 字符设备指那些必须按字节流传输,以串行顺序依次进行访问的设备。它们是我们日常最常见的驱动了,像鼠标、键盘、打印机、触摸屏,还有点灯以及I2C、SPI、音视频都属于字符设备驱动。

字符设备不经过系统快速缓冲。

2.块设备驱动 就是存储器设备的驱动,比如 EMMC、NAND、SD 卡和 U 盘等存储设备,因为这些存储设备的特点是以存储块为基础,按块随机访问,可以用任意顺序进行访问,以块为单位进行操作,因此叫做块设备。数据的读写只能以块(通常是512B)的倍数进行。与字符设备不同,块设备并不支持基于字符的寻址。

块设备经过设备缓冲

3.网络设备驱动 就是网络驱动,不管是有线的还是无线的,都属于网络设备驱动的范畴。按TCP/IP协议栈传输

网络设备面向数据包的接受和发送而设计,它并不对应文件系统的节点

注意: 块设备和网络设备驱动要比字符设备驱动复杂,就是因为其复杂所以半导体厂商一般都编写好了,大多数情况下都是直接可以使用的。

一个设备可以属于多种设备驱动类型,比如USB WIFI,其使用 USB 接口,属于字符设备,但是其又能上网,所以也属于网络设备驱动。

209.字符设备框架

如何写⼀个字符设备驱动?

分配

  • 注册设备号,register_chrdev_region(devno, LED_NUM, "myled");为了让内核知道这个设备是合法的,将构造的设备号注册到内核中,表明该设备号已经被占用,如果有其他驱动随后要注册该设备号,将会失败。找到设备,以及让应用内核和硬件能对应起来,主次设备号来分类,哪一类的哪一个设备,内核里面以及分配了一些设备号,自己用不能设置太小。也可以动态分配。

设置

  • 初始化字符设备,cdev_init(&cdev, & led_fops);
  • 实现设备的文件操作,file_operation

注册

  • 添加字符设备到系统散列表中,cdev_add(&cdev, devno, LED_NUM);
210.设备驱动程序中如何注册一个字符设备?分别解释一下它的几个参数的含义。

在设备驱动程序中注册一个字符设备,可以使用 register_chrdev 函数。以下是该函数的原型:

int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);

该函数有三个参数:

  1. major:设备的主设备号,用于唯一标识设备。如果该参数为 0,则表示将自动分配主设备号。
  2. name:设备的名称,用于在 /dev 目录下创建设备文件。该参数应该是一个以 \0 结尾的字符串。
  3. fops:一个指向文件操作结构体的指针,用于指定设备的操作函数。文件操作结构体包括一组函数指针,用于实现设备的读、写、打开、关闭等操作。

其中,设备的主设备号用于唯一标识设备。如果需要支持多个设备,则可以使用次设备号来标识不同的设备。设备的名称用于在 /dev 目录下创建设备文件,设备文件名通常以设备名称命名。文件操作结构体包括一组函数指针,用于实现设备的读、写、打开、关闭等操作。需要注意的是,这些函数在调用时必须是原子的,以避免竞态条件和并发问题。

211./dev/下面的设备文件是怎么创建出来的?

/dev 目录下的设备文件是由系统内核动态创建的,主要有以下两种方法:

  1. 自动创建

当设备驱动程序使用device_create()、class_create()或者 register_chrdev()register_blkdev() 函数进行设备注册时,系统内核会自动创建相应的设备文件。内核会分配主设备号,然后在 /dev 目录下创建设备文件,设备文件的名称通常与设备名称相同。

  1. 手动创建

用户也可以手动创建设备文件,方法是使用 mknod 命令。mknod 命令用于创建字符设备、块设备和管道等特殊文件,其语法为:

mknod /dev/device_name c major_number minor_number   # 创建字符设备文件
mknod /dev/device_name b major_number minor_number   # 创建块设备文件
mknod /dev/pipe_name p                              # 创建命名管道文件

其中,device_name 是设备文件的名称,major_number 是设备的主设备号,minor_number 是设备的次设备号。对于命名管道文件,只需要指定文件名即可。

212.总线设备驱动模型和字符设备有什么区别?

总线设备驱动模型和字符设备是两种不同的驱动模型,主要用于处理不同类型的硬件设备。

  1. 总线设备驱动模型

总线设备驱动模型是一种用于处理复杂硬件设备的驱动模型,它可以支持多个设备连接到同一个总线上,同时提供了一个统一的接口,使得驱动程序可以处理不同类型的设备。总线设备通常是通过总线控制器与主机连接,例如 PCI 总线、USB 总线等。在总线设备驱动模型中,驱动程序需要实现一些特定的函数,例如 probe()remove() 函数,以便在设备连接到总线上时进行初始化和在设备从总线上移除时进行清理操作。

  1. 字符设备

字符设备是一种比较简单的设备类型,通常用于处理流数据,例如串口、键盘、鼠标等。字符设备驱动程序通常只需要实现几个基本的函数,例如 open()close()read()write(),以提供对设备的访问。字符设备驱动程序还需要将设备注册到系统中,并创建相应的设备文件,以便用户空间程序可以使用 open() 等系统调用打开设备并进行读写操作。

Linux移植

213.什么是交叉编译?为什么需要交叉编译?

交叉编译是指将源代码从一种计算机架构编译为另一种计算机架构的过程,在一个操作系统上编译针对另一个操作系统或硬件平台的程序。

需要进行交叉编译的原因通常是:

  1. 目标平台和开发平台不同:在开发软件时,开发者可能需要将软件运行在一个与其开发机器不同的目标平台上,如编写针对嵌入式设备的应用程序时,开发者通常需要在 PC 上编译,然后将其部署到嵌入式设备中。
  2. 硬件架构不同:在不同的硬件架构之间进行编译时需要进行交叉编译。例如,将 ARM 架构的应用程序编译为 x86 架构的应用程序。
  3. 系统库不同:不同的操作系统有不同的系统库,编译程序时需要使用适当的系统库。交叉编译可以使开发者在开发机器上使用开发者熟悉的库,在目标平台上使用目标平台的库。

交叉编译需要考虑多种因素,例如处理器架构、操作系统、编译器版本和编译选项等。因此,需要仔细配置编译工具链,以确保生成的可执行文件或库能够在目标平台上运行。

214.Uboot的启动流程

uboot启动过程中做了那些事?

arch级的初始化

  • 关闭中断,设置svc模式

  • 禁用MMU、TLB

  • 关键寄存器的设置,包括时钟、看门狗的寄存器

板级的初始化

  • C语言执行环境初始化(堆栈环境的设置)

  • 前置板级初始化(重定位前),包括时钟、定时器、环境变量、串口、内存的初始化(到C语言了,前面都是汇编)

  • 进行代码重定向

代码重定向之后的板级初始化

  • 后置板级初始化(重定位后)、串口(进一步初始化)、看门狗、中断、定时器、网卡等等的初始化。
  • 内核映象和根文件系统映象重定位(从 Flash上读到SDRAM空间中)
  • 启动内核或进入命令行状态,等待终端输入命令以及对命令进行处理,为内核设置启动参数
  • 跳转到Linux内核所在的地址运行(直接修改PC寄存器的值为Linux内核所在的地址)

转到Linux内核所在的地址运行现在发展出了新形式,非传统方式启动内核。将内核作为根文件系统的一个文件,通过/boot/extlinux/extlinux.conf配置内核启动路径(原理:内存加载ramdisk放内核运行,因为现在内核是应用层的一个文件),方便更换内核,甚至板子自己编译内核,自己替换

215.uboot和内核如何完成参数传递?

在启动 Linux 内核的过程中,U-Boot 和内核之间需要进行参数传递,这些参数通常包括内核的启动参数、设备树文件、根文件系统等信息。U-Boot 和内核之间的参数传递可以通过以下几种方式实现:

  1. 命令行参数:U-Boot 可以通过环境变量 bootargs 设置 Linux 内核的启动参数,如串口波特率、根文件系统、IP 地址等。这些参数会被传递给内核作为启动参数,内核启动后可以通过 /proc/cmdline 文件读取这些参数。
  2. 设备树文件:U-Boot 可以加载设备树文件,并将设备树地址和大小传递给内核。内核在启动时会将设备树文件加载到内存中,并用于初始化硬件和设备驱动。U-Boot 通过 bootm 命令的 -d 参数指定设备树文件的地址,内核启动后可以通过 of_flat_dt 系列函数来访问设备树信息。
  3. 内存映像地址:U-Boot 可以将 Linux 内核的内存映像地址传递给内核,以便内核启动时能够正确加载和执行。这通常通过 bootm 命令的 -s 参数来指定。
  4. 其他参数:除了上述参数之外,U-Boot 还可以通过其他方式向内核传递参数,如通过寄存器或者内存地址来传递参数。这些方式需要在 U-Boot 和内核中进行相应的设置和处理。
216.为什么uboot要关掉中断、看门狗、caches、MMU?

启动引导过程中(在基本硬件初始化和重定位时)安全第一,速度第二。专心初始化,保证板子成功起来

关中断和看门狗防止异常打断

关数据caches,防止指令取址异常

在进行这些准备工作之前,U-Boot 通常会关掉中断、看门狗、caches和MMU,以确保系统处于一个可控制的状态。

  1. 关掉中断:中断是一种异步事件,它可能会在任何时刻发生,中断处理程序会打断当前的执行流程。在启动时关闭中断可以防止不可预料的中断事件影响启动过程的稳定性。
  2. 关掉看门狗:看门狗是一种硬件定时器,它会在一段时间内定时检查系统是否出现故障,如果系统出现故障则会进行复位。在启动时关闭看门狗可以避免系统在准备工作中耗时过长被看门狗复位。
  3. 关掉caches:处理器的caches是一种高速缓存,用于加速访问内存。在启动时关闭caches可以确保所有的内存访问都经过物理地址,而不是通过缓存地址,从而避免访问到脏数据。
  4. 关掉MMU:MMU是一种硬件单元,用于将虚拟地址映射到物理地址。在启动时关闭MMU可以确保系统访问的是物理地址,而不是虚拟地址,这可以避免由于地址映射错误导致的不可预料的行为。
#嵌入式##八股#
【嵌入式八股】精华版 文章被收录于专栏

【嵌入式八股】精华版

全部评论
1
点赞 回复 分享
发布于 09-10 19:54 山东
2
点赞 回复 分享
发布于 09-10 19:55 山东
3
点赞 回复 分享
发布于 09-10 19:55 山东
4
点赞 回复 分享
发布于 09-10 19:55 山东
5
点赞 回复 分享
发布于 09-10 19:55 山东
6
点赞 回复 分享
发布于 09-10 19:55 山东
7
点赞 回复 分享
发布于 09-10 19:55 山东
8
点赞 回复 分享
发布于 09-10 19:55 山东
9
点赞 回复 分享
发布于 09-10 19:55 山东
10
点赞 回复 分享
发布于 09-10 19:55 山东

相关推荐

10-25 00:32
香梨想要offer:感觉考研以后好好学 后面能乱杀,目前这简历有点难
点赞 评论 收藏
分享
评论
4
15
分享
牛客网
牛客企业服务