整理的Linux面经嵌入式相关知识点

自己在找工作的过程中,整理的LINUX系统嵌入式相关的知识点以及参考的其他一些相关博客文章

  • 大小端判断程序

首先,ARM存储器格式分为大端格式和小端格式;
 - 大端格式:字数据的高字节存储在低地址中,低字节存储在高地址中
 - 小端格式:字数据的低字节存放在低地址中,高字节存放在高地址中
判断程序:
 **一.共用体**

 union test
    {
        int a;
        char b;
    }
    int main(void)
    {
        union test t1;
        t1.a=1;
        if(t1.b==1)
            printf("小端格式");
        else
            printf("大端格式");
        return 0;
    }


**二.指针法**

int main(void)
    {
        int a=1;
        char *ptr=(char*)(&a);   //char型指针指向的char一个字节的地址空间
        if((*ptr) == 1)
            printf("小端格式");
        else
            printf("大端格式");
        return 0;
    }

 

  • int型指针和char型指针的区别:
  1.  - 本质上都是计算机里面的一个地址
  2.  - 默认指向空间占用的大小不同:int*指向空间需要4个字节,char*指针只需要一个指针
  3.  - 使用时的取值范围不同:用*取值时int*得到得值是int类型的范围,char*可以取到的值是char的范围
  4.  - 赋值时范围不同,用*p形式赋值时,如果是int*型的,会按照int来截取,如果是char*型的,按照char范围截取
  5. 比如int a, *pa=&a; char b, *pb = &b;

执行*pa = 0x12345678后,*pa的值就是0x12345678。
执行*pb =0x12345678后,*pb的值就会被截取,值为0x78。

  • IIC协议

物理层:
只要求两条总线线路,一条串行数据线SDA,一条串行时钟线SCL
每个连线到总线的器件都可以通过唯一的地址与其他器件通信,主从机角色和地址可配置
传输速率在标准模式下可以达到100Kb/s,快速模式下能达到400kb/s, 高速模式下能达到3.4Mb/s

  •  linux内核驱动加载过程,具体实现

Linux的驱动加载分两种情况:静态加载和动态加载
**静态加载:**就是把驱动程序直接编译进内核,然后内核在启动过程中由do_initcall()函数加载
在make menuconfig命令进行内核配置裁剪时,在窗口中可以选择是否编译进内核,还是放入/lib/modules/下的内核版本目录中,还是不选
**动态加载:**将动态驱动模块加载到内核中,加载驱动命令:insmod,modprobe
insmod与modprobe不同之处:insmod绝对路径/xx.ko, 而modprobe xx即可,不用加.o或.ko后缀,也不用加路径,重要的一点是modprobe同时会加载当前模块所依赖的其他模块
模块加载的时候(insmod, modprobe),**sys_init_module()**系统调用会调用**module_init**指定的函数,在module_init指定的函数过程:
首先进行**申请设备号**,其中包括动态分配和静态申请两种:

  1.  - **静态申请:** register_chrdev_region()函数
  2.  - **动态分配:** alloc_chrdev_region()函数

然后就是进行设备的注册,字符设备使用struct cdev来描述,字符设备的注册可分为3个步骤:

 

  1.  - **分配cdev:** 使用 struct cdev *cdev_alloc(void) 函数
  2.  - **初始化cdev:** 使用 void cdev_init(struct cdev *dev, const struct file_operations *fops) 函数
  3.  - **添加cdev:** 使用 int cdev_add(struct cdev *p, dev_t dev, unsigned count) 函数

加载完驱动模块后,可以在 cat /proc/devices 下面查看设备号
接着可以创建设备节点: mknod devicename c 2031 0(mknod 设备名称 设备类型 主设备号 次设备号)
创建设别节点后,用户进程可以通过 /dev/devicename 这个路径就可以访问到全局变量设备虚拟设备
也可以自动创建设备节点:
class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备

  • 主设备号与次设备号的区别

 - **主设备号:**用来表示与设备文件相连的驱动程序,反映的是设备类型
 - **次设备号:**被驱动程序用来辨别操作哪个设备,用来区分同类型的设备

  • 字符设备与块设备的区别

第一:字符设备是按字节来访问设备,而块设备只能以整块数据为单位来访问设备
第二:字符设备是按照字节流的方式有序访问,而块设备可以随机访问固定大小的数据片

  •  自旋锁底层实现机制

首先实现一个结构体用于自旋锁的使用


    typedef struct spinlock{
        volatile unsigned int slock;
    }spinlock_t;

接口实现
(1) 初始化接口:
    
    ·#define spin_lock_init(lock) \
    do{ \
        ((spinlock_t*)lock)->slock = 0x0; \
        }while(0)

(2) 上锁接口

    static inline void spin_lock(spinlock_t *lock)
    {
        raw_spin_lock(&lock->slock);
    }

(3) 释放锁

    static inline spin_unlock(spinlock_t *lock);
    {
        raw_spin_unlock(&lock->slock);
    }

更底层的汇编实现

    raw_spin_lock:     //完成自旋锁的加锁功能
    mov  r1,#1         @1-->r1

    DSB
    take_again:

            LDREX     r2,[r0]            @把r0的内容赋给r2,同时置全局标志exclusive

            STREX     r3,r1,[r0]        @尝试将r1写入到锁里边,首先检查exclusive是否存在,如果存在则将r1-->r0,r3 = 0,并清除exclusive标志,否则1--->r3,结束

            TEQ         r3,#0

            BNE       take_again

            TEQ        r2,#0
    
            BNE       take_again

            MOV       pc,lr                @返回



    raw_spin_unlock:

        DSB

        MOV r1,#0

        STR  r1,[r0,#0]                  @为0,标示锁已释放

        DSB

        MOV  pc,lr

  • 有哪些线程同步机制


实现线程同步机制的方法:
 - **互斥锁:**两种状态:锁住状态和不加锁状态
 - **读写锁:**读模式下加锁状态,写模式下加锁状态,不加锁状态
 - **条件变量**条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两种动作:一个线程等待“条件变量的条件成立”而挂起;另一个线程使“条件成立”。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起
 - **临界区:**临界区的作用范围仅限于本进程。其他进程无法获取该锁

  • spinlock特点,内部实现

**spin lock的特点**
 - spin lock是一种死等的锁机制,当发生访问资源冲突的时候,当前的执行thread会不断的重新尝试直到获取进入临界区
 - 只允许一个thread进入,一次只能有一个thread获取并进入临界区
 - 执行时间短,持有自旋锁的时间一般不会超过两次上下文切换的时间
 - 可以在中断上下文执行

  • mutex和spinlock差别

如果代码需要睡眠,这往往发生在和用户空间同步时,使用信号量是唯一的选择

**信号量:**是一种睡眠锁,如果有一个任务想要获得已经被占用的信号量时,信号量会将这个进程放入一个等待队列,然后让其睡眠,当持有信号量的进程将信号释放后,处于等待队列中的任务将被唤醒,并让其获得信号量,信号量的初始值表示允许有几个任务同时访问该信号量保护的共享资源,初始值为1就变成互斥锁,如果释放后的信号量的值为非正数,表明有任务等待当前信号量,因此要唤醒等待该信号量的任务

**自旋锁:**自旋锁不会引起调用者睡眠,如果一个线程试图获得一个已经被持有的自旋锁,线程就会一直进行忙循环

理想情况是所有的锁都应该尽可能短的被持有,如果持有锁的时间较长,使用信号量

即信号量适合保持时间较长的情况,而自旋锁适合于保持时间非常短的情况而持有自旋锁的时间一般不会超过两次上下文切换的时间,一旦线程要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,就应该选择信号量

**自旋锁与互斥锁的区别在于不会导致睡眠,如果自旋锁被其他执行单元持有了,那么调用者就一直自旋在那循环的看持有者是否已经释放,而不睡眠。在持有时间短的情况下使用比互斥锁高效

/+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++/

 - 自旋锁不会引起调用者睡眠,如果一个线程试图获得一个已经被持有的自旋锁,线程就会一直进行忙循环;而信号量则引起调用者睡眠,如果有一个任务想要获得已经被占用的信号量时,信号量会将这个进程放入一个等待队列,然后让其睡眠,当持有信号量的进程将信号释放后,处于等待队列中的任务将被唤醒,并让其获得信号量
 - 自旋锁适合于保持时间非常短的情况,可以在任何上下文使用,信号量适合于保持时间较长的情况,只能在进程
 - 上下文使用;如果被保护的共享资源只在上下文访问,则可以以信号量来保护该进程资源,如果对共享资源的访问时间非常短,选择自旋锁,但如果保护的共享资源需要在中断上下文访问,就必须使用自旋锁

信号量根据计数值count的取值,可以将信号量分为二值信号量和计数信号量

- 二值信号量强制二者同一时刻只有一个运行
- 计数信号量,其允许一个时刻有一个或者多个进程同时使用,取决于count值。

  • 互斥量和信号量的区别

 - 互斥量用于线程的互斥,信号量用于线程的同步,这是互斥量和信号量之间的根本区别
 - 互斥量值只能为0/1,信号量值可以为非负整数,也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题,信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量时,也可以完成一个资源的互斥访问。
 - 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到

  • 区分互斥和同步

**互斥:**是指某一个资源同时只允许一个访问者对其进行访问,具有唯一性和排他性,但互斥无法限制访问者对资源的访问顺序,即访问是无序的
**同步:**是指在互斥的基础上,通过其他机制实现访问者对资源的有序访问,在大多数情况下,同步已经实现了互斥,特别是所有写入自愿的情况是互斥的,少数情况是指可以允许多个访问者同时访问资源

  •  数组指针和指针数组的区别

**指针数组:**array of pointers,即用于存储指针的数组,也就是数组元素都是指针
**数组指针:**a pointer to an array, 即指向数组的指针

其注意用法上的区别:

int \*a[4]      指针数组,表示:数组a中的元素都为int型指针  元素表示: \*a[i]---(其a[i]为地址值)
int (\*a)[4]    数组指针,表示:指向数组a的指针,     元素表示:(\*a)[i]

    

#include <iostream>
    
    using namespace std;
    
    int main()
    {
    
        int c[4]={1,2,3,4};
        int *a[4]; //指针数组
        int (*b)[4]; //数组指针
        
        b=&c;
        
        //将数组c中元素赋给数组a
        for(int i=0;i<4;i++)
        {
            a[i]=&c[i];
        }
        
        //输出看下结果
        cout<<*a[1]<<endl; //输出2就对
        cout<<(*b)[2]<<endl; //输出3就对
        
        return 0;
    }

 

  •  堆栈的区别

 - **栈:**由编译器自动释放内存,存放函数的参数值,局部变量,函数返回地址等
 - **堆:**一般由程序员分配释放内存,属于动态内存分配区域,malloc,realloc, calloc,并指明其大小

  • 函数调用参数传递时在栈中的实现

栈是从上到下生长(地址减小)的,压栈的操作使栈顶地址减小,弹出的操作使栈顶地址增大

相关寄存器:
 - esp: 堆栈(Stack)指针寄存器,指向堆栈顶部
 - ebp:基址指针寄存器,指向当前堆栈底部
 - eip:指令寄存器,指向下一条指令的地址
 - eax:累加寄存器,常用于函数返回值
 - edx:数据(DATA)寄存器

Program Stack

-------函数参数3---------|ebp+16

-------函数参数2---------|ebp+12

-------函数参数1---------|ebp+8

-------函数返回地址-------|ebp+4

--------------------

ebp-----上一个函数的ebp----|ebp

--------局部变量1----------|ebp-4

--------局部变量2----------|ebp-8

esp-----局部变量3----------|ebp-12

对于函数返回值:

小于4个字节的返回值由eax寄存器返回,eax寄存器本身只有4个字节,5到8个字节的返回值由eax和edx寄存器联合返回,eax寄存器返回低四个字节,edx返回高于4个字节的部分
对于大于8个字节的返回值:C语言会在函数返回时使用一个临时的栈上内存区域作为中转,结果返回值对象会被拷贝两次

 - 首先main函数在栈上开辟一片空间,将这块空间的一部分作为传递返回值的临时对象,这里称为temp
 - 将temp对象的地址作为隐藏参数传递给return_test函数
 - return_test函数将数据拷贝给temp对象,并将temp对象的地址用eax传出
 - return_test返回之后,main函数将eax指向的temp对象的内容拷贝给n

  •  ARM的工作模式
  1.  - **用户模式:**ARM处理器正常的程序执行状态
  2.  - **快速中断模式(FIQ):**处理高速中断,用于高速数据传输或通道处理
  3.  - **外部中断模式(IRQ):**用于普通的中断处理
  4.  - **管理模式:**操作系统使用的保护模式,系统复位后的默认模式
  5.  - **中止模式:**数据或指令预取中止时进入该模式
  6.  - **未定义模式:**处理未定义指令,用于支持硬件协处理器的软件仿真
  7.  - **系统模式:**运行特权级的操作系统任务
  •  快速中断(FIQ)和外部中断(IRQ)的区别

快速中断模式用于高速数据传输或通道处理,外部中断模式用于普通的中断处理

 - **执行速度FIQ比IRQ快**
 
ARM的FIQ模式提供了更多的banked寄存器,R8到R14还有SPSR,而IRQ模式就没有那么多,R8,R9,R10,R11,R12对应的banked寄存器就没有,这就意味着在ARM的IRQ模式下,中断处理程序自己要保存R8到R12这几个寄存器,然后退出中断处理程序要恢复这几个寄存器,而FIQ模式由于这几个寄存器都有banked寄存器,模式切换时CPU自动保存这些值到banked寄存器,退出FIQ模式自动恢复,所以这个过程FIQ和IRQ快

 - **FIQ比IRQ有更高的优先级,即IRQ可以被FIQ所中断,但FIQ不能被IRQ所中断,在处理FIQ时必须关闭中断**

FIQ的中断向量地址在0x0000001C,而IRQ的在0x00000018.(也有的在0xFFFF001C以及0xFFFF0018),18只能放一条指令,为了不与1C处的FIQ冲突,这个地方只能跳转,而FIQ不一样,1C后就没有任何中断向量表了,这样可以直接在1C处放FIQ的中断处理程序,由于跳转的范围限制,至少少了一条跳转指令

 - **IRQ和FIQ的响应延迟有区别**

IRQ的响应并不及时,从verilog仿真来看,IRQ会延迟几个指令周期才跳转到中断向量处,看起来像是在等预取的指令执行完。

## 进程死锁以及死锁的必要条件和解决方法

**死锁:**进程A占有资源R1,等待进程B占有的资源R2;进程B占有资源R2,等待进程A占有的资源R1,而且资源R1,R2只允许一个进程占有,即:不允许两个进程同时占用,结果两个进程都不能继续执行。

**产生死锁的必要条件**

 - **互斥条件**即某个资源在一段时间内只能由一个进程占有,不能同时被两个或两个以上的进程占有
 - **不可抢占条件**进程所获得的资源在未使用完毕之前,资源申请者不能强行从资源占有者手中夺取资源,只能由该资源的占有者进程自行释放
 - **占有且申请条件**进程至少已经占有一个资源,但又申请新的资源;由于该资源已被另外进程占有,此时该进程阻塞;但它在等待新资源之时,仍继续占用已占有的资源
 - **循环等待条件**形成一个进程循环等待环

  • 驱动里面为什么要有并发,互斥的控制?如何实现?

并发(concurrency)指的是多个执行单元同时,并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量,静态变量等)的访问则很容易导致竞态(race conditions)。
解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问就是指一个执行单元在访问共享资源的时候,其他的执行单元都被禁止访问。
访问共享资源的代码区域被称为临界区,临界区需要以某种互斥机制加以保护,中断屏蔽,原子操作,自旋锁和信号量都是linux设备驱动中采用的互斥途径

  •  malloc(), vmalloc()和kmalloc()的区别

 - kmalloc和vmalloc是分配内核的内存,malloc分配的是用户的内存
 - kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续,物理地址上不连续,malloc不保证任何东西
 - kmalloc分配的内存大小有限,是**32字节到128KB**,vmalloc和malloc能分配的大小相对较大
 - 内存只有在要被DMA访问的时候才需要物理上连续,此时用kmalloc
 - vmalloc比kmalloc要慢

因为vmalloc函数会建立新的页表,将不连续的物理内存映射成连续的虚拟内存,所以开销比较大
-----------------------------------------
**kmalloc和vmalloc**
这两个区别大体概括为:

  1.  - vmalloc分配的一般为高端内存,只有当内存不够的时候才分配低端内存,kmalloc从低端分配内存
  2.  - vmalloc分配的物理地址一般不连续,kmalloc分配的物理地址连续,两者分配的虚拟地址都是连续的
  3.  - vmalloc分配的一般为大块内存,kmalloc一般分配的是小块内存

kmalloc分配的内存处于3GB~high_memory之间,vmalloc分配的内存在VMALLOC_START~4GB之间,非连续的内存区
kmalloc分配内存基于slab分配器,Linux中为一些反复分配和释放的结构体预留了一些内存空间,使用内存池来管理,这种技术就是slab(后备高速缓存)

  • Linux系统中硬链接和软链接的区别

 - 软链接相当于WINDOWS中的快捷方式,如果打开并修改软链接,相应的文件也会随之改变,但是如果删除软链接,源文件并不会受到影响
 - 硬链接有点像引用和指针的结合,当打开并修改它时,相应的文件随之改变,但是所有这个文件的硬件内容也随之改变,这是因为所有的硬链接都拥有唯一的一个inode号,它们指向的是同一文件。
 - 软链接可以跨文件系统创建,也就是可以在某一个分区中创建到另外一个分区的软链接
 - 硬链接则只能在本文件系统中创建使用,因为inode是这个文件在当前分区中的索引值,是相对于这个分区的,不能跨越文件系统
 - 软链接可以连接任何文件或者文件夹,而硬链接则只能在文件之间创建,并且不能对目录进行创建

  • Linux设备驱动管理的中断

Linux中断分为两个半部:上半部(tophalf)和下半部(bottom half),上半部的功能是“登记中断”,当一个中断发生时,它进行相应的硬件读写后就把中断例程的下半部挂到该设备的下半部执行队列中去,上半部执行的速度会很快,上半部与下半部最大的不同就是上半部是不可中断的,下半部是可中断的,下半部几乎做了中断处理程序的所有事情。

下半部的实现机制主要有tasklet和工作队列

  •  静态链接库和动态链接库的区别

------------------------1静态链接库的优点--------------------------
 (1)代码装载速度快,执行速度略比动态链接库快; 
 (2)只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱等问题。 
--------------------------2动态链接库的优点------------------ 
 (1)更加节省内存并减少页面交换;
 (2) DLL文件与EXE文件独立,只要输出接口不变(即名称、参数、返回值类型和调用约定不变),更换DLL文件不会对    EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性;
 (3)不同编程语言编写的程序只要按照函数调用约定就可以调用同一个DLL函数;
 (4)适用于大规模的软件开发,使开发过程独立、耦合度小,便于不同开发者和开发组织之间进行开发和测试。
--------------------------3不足之处--------------------------
 (1)使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费;
 (2)使用动态链接库的应用程序不是自完备的,它依赖的DLL模块也要存在,如果使用载入时动态链接,程序启动时发现DLL不存在,系统将终止程序并给出错误信息。而使用运行时动态链接,系统不会终止,但由于DLL中的导出函数不可用,程序会加载失败;速度比静态链接慢。当某个模块更新后,如果新模块与旧的模块不兼容,那么那些需要该模块才能运行的软件,统统撕掉。这在早期Windows中很常见。

  • 驱动模块与模块之间的通信(函数调用)

如模块1调用模块2的功能函数过程:

首先加载模块2:

 - insmod 模块2.ko
 - 内核为模块2分配空间,然后将模块的代码和数据装入分配内存中
 - 内核发现符号表中有函数1,函数2可以导出,于是将其内存地址记录在**内核符号表**中

导出:EXPORT_SYMBOL(add_intergar);
导出后可以在 /proc/kallsyms下查看
    
    root# cat /proc/kallsyms | grep add_integar

    __ksymtab_add_integar 表明内核符号add_integar已导出

输出信息可以在 /var/log/syslog里面查看

    root# tail -n 10 /var/log/syslog

加载模块1:

 - insmod命令给模块分配空间,然后将模块的代码和数据装在内存中。
 - 内核在模块1的符号表中发现一些未解析的函数,于是模块1会通过内核符号表,查找相应的函数,并将函数地址填到模块1的符号表中

  • 函数可重入

一个函数被重入,表示这个函数没有执行完成,由于外部因素或内部调用,又一次进入该函数执行,一个函数要被重入,有两种情况:

 - 多个线程同时执行这个函数
 - 函数自身调用自身

一个函数可重入,具有以下几个特点:

  1.  - 不适用静态或者全局的非const变量
  2.  - 不返回任何静态或者全局的非const变量的指针
  3.  - 仅依赖于调用方提供的参数
  4.  - 不依赖任何单个资源的锁
  5.  - 不调用任何不可重入的函数

 

  •  Linux系统中的系统调用

从用户态切换到内核态的两种方式:系统调用和中断方式
实现系统调用一般是汇编指令:

    int $0x80

128号中断引发软中断进入内核空间
在实际执行中断向量表中的第0x80号元素所对应的函数之前,CPU首先还要进行栈的切换,用户态和内核态使用的是不同的栈,两者各自负责各自的函数调用,在应用程序调用0x80号中断时,程序的执行流程是从用户态切换到内核态,从中断处理函数返回时,程序的当前栈还要从内核栈切换回用户态
所谓当前栈,指的是ESP的值所在的栈空间,此外,寄存器SS的值还应该指向当前栈所在的页
当前栈切换到内核栈:
 - 保存当前ESP, SS的值
 - 将ESP,SS的值设置为内核栈的相应值

将当前栈由内核栈切换为用户栈:

 - 恢复原来ESP, SS的值
 - 用户态的ESP和SS的值保存在内核栈上

先保存系统调用号的值,再SWI软中断进入内核系统,即Entry_common.S中的ENTRY<VECTORY_SWI>中取出系统调用号,再根据系统调用号去查找系统调用表(calls.S文件中的sys_call_table)中找到相应的系统调用的内核处理函数

参考的一些博客文章:

linux驱动面试题2018(面试题整理,含答案)

linux驱动工程面试必问知识点

Linux 驱动面试题总结

你知道底层自旋锁是如何实现的吗

Linux中的spinlock和mutex区别

gdb调试(线程和正在运行中的程序)

gdb调试当前运行的程序

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章