编辑
2024-09-09
学习记录
0
请注意,本文编写于 326 天前,最后修改于 263 天前,其中某些信息可能已经过时。

目录

Linux内核精讲
1.linux系统概括
2.Linux中断机制
3.进程管理
1.进程的运转方式:
2.如何进行创建一个新的进程
3.进程调度
4.进程的退出/销毁
5.进程间通信IPC
4.Linux操作系统的引导
5.Linux文件系统
6.Linux高速缓冲区
7.inode节点
8.Linux内存管理
9.Linux驱动
字符设备驱动
子系统驱动
MISC和platform驱动
linux项目-电子相框(Kindle系统构架)
u-boot移植项目
虚拟化
C++智能指针
Linux客户端与服务端应用
Linux进程管理
CFS调度器
操作系统一些命令

Linux内核精讲

1.linux系统概括

1.操作系统的结构

2.操作系统的工作方式:

  1. 把操作系统从用户态 切换到 内核态(用户应用程序 到 内核的流程)
  2. 实现操作系统的系统调用(操作系统服务层)
  3. 应用操作系统提供的底层函数,进行功能实现
    • 操作系统的驱动结构
  4. 退出后从内核态切换到用户态

3.操作系统内核中各级模块的相互关联

  1. Linux内核的整体模块:进程调度模块 、内存管理模块、文件系统模块、进程间通信模块、驱动管理模块
  2. 每个模块间的关系(互相调用)
    1. 内存管理和驱动管理模块 虚拟内存的缓存和回存机制
    2. VFS 虚拟文件系统 把硬件当成文件来进行使用

4.操作系统结构的独立性

  1. 管理层,只负责管理。
  2. 实现层,负责具体代码实现。

5.高低版本的内核之间的区别:

  1. 多的是内核驱动的种类,内核驱动的管理模式并没有巨大的改变(一段时间3个阶段的跳段 零散型 分层型 设备树)。
  2. 进程的调度算法发生了改变,进程的管理方式并没有巨大的改变。

2.Linux中断机制

分类:硬件中断和软件中断;

linux内核0.11版本: image.png

中断的工作流程(入栈 执行 出栈):

  1. CPU工作模式的转化(arm有7种工作模式,快中断、慢中断···)。
  2. 进行寄存器的拷贝和压栈,保存当前上下文。
  3. 中断异常向量表种找到对应的中断向量。
  4. 保存正常运行的函数返回地址。
  5. 调转到对应的中断服务函数上运行。
  6. 进行模式复原以及寄存器的复原。
  7. 跳转回正常工作的函数地址继续运行。

3.进程管理

1.进程的运转方式:

jiffies:系统滴答,CPU内部有一个RTC时钟,会在上电的时候调用mktime函数算出从1970年1月1日0时开始到当前开机点所过的秒数。给mktime函数传来的时间结构体是由初始化时从rtc或者cmos中读出的参数,转化为时间存入全局变量中,并且会为jiffies所用。

jiffies是系统的一个滴答,一个滴答是10ms。每个滴答引发一个中断:

  1. 首先进行jiffies自加;
  2. 调用do_timer函数:
    1. 计算用户/内核运行时间;
    2. 判断定时器链表中有没有事件满足,如果有就执行;
    3. 事件片调度,counter最大的进程先执行,执行完所有进程后进行新一轮的时间片分配(0.11版本内核是优先级时间片轮转调度算法);

image.png

2.如何进行创建一个新的进程

前置知识:

task_struct进程结构体(状态、时间片、优先级、tss结构体等;):

image.png

内存:

  1. LDT(局部描述符)段:代码段+数据段;
  2. TSS段:每个进程都有这样一个结构体,用于保存当前进程寄存器的值和结果;
  3. 堆栈:栈存放局部变量,向下生长;堆由用户分配释放;

上电进行一些初始化之后会执行init/main.c中的main函数,里面进行内存的拷贝(引导)、内存初始化、trap初始化、块设备驱动初始化、字符设备驱动初始化、调度初始化(初始化task链表)、软盘初始化,然后转移到用户态,因为此处不能被抢占,进程在内核态运行是不能被抢占的。然后创建0号进程,进程执行打开标准输入/输出/错误控制台,然后再创建1号进程打开/etc/rc文件,并执行shell(后面用另外一种方式又打开了一下避免没有被打开)。最后运行for(;;) pause();在没有其他进程运行时,运行0号进程。

进程的创建是系统的调用(中断): 给当前要创建的进程分配一个进程号find_empty_process()。然后就是对0号进程或者当前进程的task_struct和栈堆复制copy_process() image.png

3.进程调度

进程状态: image.png

调度代码(优先级时间片轮转调度算法): image.png

进程切换switch_to():

  1. 将需要切换的进程赋值给当前进程指针;
  2. 进行进程的上下文切换(切换tss段为新进程的值+新进程堆栈的值);

sleep_on() // 休眠任务链表,等待资源

wake_up() // 从不可中断状态变为运行状态

4.进程的退出/销毁

  1. do_exit销毁函数释放进程的代码段和数据段和堆栈段;
  2. 关闭进程打开的所有文件,对当前的目录和i节点进行同步(文件操作);
  3. 如果当前要销毁的进程有子进程,那么就让1号进程作为新的父进程(init进程创建的);
  4. 如果当前进程是一个会话头进程,则会终止会话中的所有进程;
  5. 改变当前进程的运行状态,变成TASK ZOMBIE僵死状态,并且向其父进程发送SIGCHLD信号。
  6. 父进程在运行子进程的时候 一般都会运行wait waitpid这两个函数(父进程等待某个子进程终止),当父进程收到SIGCHLD信号时父进程会终止僵死状态的子进程;
  7. 首先父进程会把子进程的运行时间累加到自己的进程变量中;
  8. 把对应的子进程的进程描述结构体进行释放,置空任务数组中的空槽;

内核代码:

image.png

kill信号:

image.png

image.png

5.进程间通信IPC

  1. 管道,单向通信,是内存的数据,是看不见的,无法根据文件系统查看。
  2. 命名管道,单向通信,相当于一个文件,文件系统可以看到。
  3. 消息队列,属于内核的一个组件,进程A把消息提交给消息队列,通过内核通知把消息通知到进程B。
  4. 信号量,父子进程之间通信的,只作为一个flag,一个标识。
  5. 共享内存,两个进程共享一块内存,共同使用一个指针地址指向该块内存。

4.Linux操作系统的引导

BIOS/Bootloader:

由PC机的BIOS(0xFFFFO是BIOS存储的总线地址)把bootsect从在磁盘拿到了内存中的0x7c00地址,并且进行了一系列的硬件初始化和参数设置。

bootsect.s:磁盘引导块程序,在磁盘的第一个扇区中的程序(0磁道 0磁头 1扇区),被BIOS加载到0x7c00地址后,自己将自己剪切到0x9000地址处。

作用:首先将后续的setup.s代码从磁盘中加载到紧接着bootsect.s的地方 在显示屏上显示loading system 再将system(内核代码)模块加载到0x1000的地方最后跳转到setup.s中去运行。

setup.s:解析BIOS/BOOTLOADER传递来的参数(光标位置/显存大小/显示参数/硬盘参数/根文件系统),设置系统内核运行的LDT和IDT(中断描述符寄存器)和GDT全局描述符寄存器。设置中断控制芯片,进入保护模式运行,跳转到system模块的最前面的代码运行(head.s)。

head.s:加载内核运行时的各数据段寄存器,重新设置中断描述符表,开启内核正常运行时的协处理器资源,设置内存管理的分页机制,跳转到main.c开始运行。

main.c: image.png

上电进行一些初始化之后会执行init/main.c中的main函数,里面进行内存的拷贝(引导)、内存初始化、trap初始化、块设备驱动初始化、字符设备驱动初始化、调度初始化(初始化task链表)、软盘初始化,然后转移到用户态,因为此处不能被抢占,进程在内核态运行是不能被抢占的。然后创建0号进程(init()函数),进程执行打开标准输入/输出/错误控制台,然后再创建1号进程打开/etc/rc文件,并执行shell(后面用另外一种方式又打开了一下避免没有被打开)。最后运行for(;;) pause();在没有其他进程运行时,运行0号进程。

init()函数: image.png

操作系统的移植过程:

  1. 进行操作系统初始化的适配,让main能在板卡上跑起来(设置内存分配、硬件初始化用到的参数);
  2. 进程驱动的移植;

总结:

  1. machine desc结构体,用于linux做设备板子的识别结构体,这些结构体被限定在了内存的某一片区域。
  2. 并且通过UBO0T传过来的参数进行该结构体的配置(通过检索taglist(存放硬件参数)的方式来设置)。
  3. 并且在移植Linux的时候 也要对结构体的变量进行赋值。
  4. 并且在之后的启动或其他函数中对该结构体的变量进行调用。

硬件的信息存放在machine_desc结构体中。

3.x版本内核的引导过程从以下三个角度分析:

  1. 内核如何进行多平台的适配,在内核中是如何认识这些板子的?结构体machine_desc。
  2. 内核启动的整体流。
  3. 认识一种高效的编程结构(代码段)。

链接脚本:vmlinux.lds.S指定了代码段。

宏定义:#define MACHINE_START(_type, _name) xxxx,为了从代码段中获得硬件架构信息。

结构体machine_desc,用于Linux做设备板子的识别结构体,这些结构体被限定在了内存的某一块区域,并且通过UBOOT传过来的参数进行该结构体的配置,并且在移植Linux的时候,也要对结构体的变量进行赋值,并且在之后的启动或其他函数中对结构体的变量进行调用。

lookup_processor_type_data

__mmap_switched:将旧的地址转化为虚拟地址(代码重定义)。

setup_arch:创建CPU指令集描述结构体、从指定的内存中获取到该描述结构体、将获取到的GPU名字赋值给一个全局变量、使用当前函数进行UBOOT的taglist的参数解析、找到一个移植Linux时写的最适合的machine_desc结构体,并且返回。

static noinline int init_post(void):系统运行的第一个应用程序(根文件系统的挂接)。

5.Linux文件系统

概念:是磁盘管理的目录、是Linux中操作所有硬件设备的方式、系统的功能机制。

也是一个应用程序,Linux内核初始化之后,运行的第一个应用程序。

文件系统作用:

  1. 提供磁盘管理服务;
  2. glibc设备节点,就是mknod挂载硬件设备的驱动;
  3. 配置文件/etc/init.d/rcS,配置了开机运行什么软件,载入什么界面,执行什么命令;还有/sys/中开机要挂载的设备节点,eg: USB、光驱等。
  4. 应用程序shell命令(所有shell命令都在文件系统中);

Android就是Linux多了个文件系统,实际上是多了lib和framework,包括glibc、OpenGL图形化库、media_framework媒体框架库、虚拟机、架构代码(对多种服务进行封装)。

busybox文件系统:

new_init_action()

run_actions()

__setup_param()

UBOOT传入了很多的参数,tagglist,被解析为多个setup段,存放在.init.setup的代码段中,形式为CMD字符串和命定对应的处理函数。在两个函数obsolete_checksetup(处理early为0的)和do_early_param(处理early不为0的)中进行了所有存放在.init.setup代码段的命令执行。针对各种setup段的CMD进行全局变量的赋值。

内核启动文件系统后,文件系统的工作流程:

内核启动的第一个应用程序是根文件系统,根文件系统首先在init_main()函数设置了一些信号,即使用run_actions执行某些信号,如重启、关机等。内核一般调用文件系统的时候不传入参数,就会运行parse_inittab()函数来解析inittab。如果永和自己定义了就打开,如果没有配置就执行默认的配置(new_init_action()),这里做的事情就是创建init_action结构体节点,并且把inittab中所有的配置项解析的init_action节点形成一个init_action_list,每一个节点都是inittab的一个配置项,每个配置项都有自己的action和命令脚本。后面做的事情就是执行这些命令(有的执行一次 有的等待 有的一直运行...),最后根文件系统执行的就是shell一直循环等待用户的命令。

image.png

一个最小文件系统都需要什么东西:

  1. /dev/console
  2. run_init_process()linuxrc程序(init_main()函数)
  3. /etc/init.d/rcs--脚本
  4. shell命令所用到的函数--busybox
  5. busybox的响应函数运行,需要标准库支持,所以要有glibc

Linux中使用文件系统都分那几个部分

  1. 有关Linux中高速缓冲区的管理程序 buffer.c,文件系统一般跟高速缓冲区打交道,高速缓冲区再和硬盘。比如分页机制,一页4k。
  2. 文件系统的底层通用函数(对于硬盘的读写 分配 释放 目录节点管理inode 内存与磁盘的映射)
  3. 对文件数据进行读写操作的模块(VFS虚拟文件系统 硬件驱动和文件系统的关系 pipe 块设备的读取)
  4. 文件系统与其他程序的接口实现(fopen close create等)

文件系统的基本概念

磁盘中要有目录的映射,我们把磁盘分成盘片

每一个盘片都有一个文件系统的子系统(章节目录),由以下几部分组成:

  1. 引导块:用来引导设备的,可以为空,但要空出来位置
  2. 超级块:是该文件子系统的描述符(记录盘片的逻辑块位图/i节点位图的地址,通过设备号可以获得)
  3. i节点位图:每一位对应inode节点的使用情况
  4. 逻辑块位图:每一位对应一个逻辑块的使用情况
  5. inode节点:一个结构体,存储文件的索引点、属性、文件名、修改时间、对应的磁盘块(即目录与磁盘的桥接+属性)
  6. 逻辑块数据区:存储数据的

image.png

6.Linux高速缓冲区

高速缓冲区的管理要素

  1. 映射关系,和磁盘之间的映射关系
  2. 应用程序与高速缓冲区的交互API,fopen、read、write
  3. 高速缓冲区与磁盘的交互API(循环链表+哈希表+单链表)

高速缓冲区工作流程

高速缓冲区中存储着对应的块设备驱动的数据,当从块设备中读取数据的时候,OS首先会从高速缓冲区中进行检索,如果没有则从块设备中读出数据,如果有并且是最新的,就直接和该高速缓冲区进行数据交互。

高速缓冲区和磁盘块一一对应

缓冲区可以分为低区和高区,低区(buffer_head结构体)的某一块也对应高区的某一块,类似inode节点位图和i节点。

image.png

假如我们来写一个buffer.c

  1. 分配一个buffer_head, getblk()
  2. 设置该buffer_head指向空闲缓冲区的一个有效buffer_head, get_hash_table()
  3. 更新该buffer_head里的所有设置项
  4. 把该buffer_head从空闲中出链
  5. 算出该buffer_head对应的散列序号并把它加入到对应的散列项链表尾部

7.inode节点

文件与磁盘的映射结构

每一个盘片都有一个文件系统的子系统(章节目录),由以下几部分组成:

  1. 引导块:用来引导设备的,可以为空,但要空出来位置
  2. 超级块:是该文件子系统的描述符(记录盘片的逻辑块位图/i节点位图的地址,通过设备号可以获得)
  3. i节点位图:每一位对应inode节点的使用情况,1024 * 8 = 8192个(实际8191,0位不用)
  4. 逻辑块位图:每一位对应一个逻辑块的使用情况
  5. inode节点:一个结构体,存储文件的索引点、属性、文件名、修改时间、对应的磁盘块(即目录与磁盘的桥接+属性)
  6. 逻辑块数据区:存储数据的,是从第一个引导块开始计数的块号

扇区:是一个长度为512B的数据块

在不同的文件系统中,扇区和盘块对应关系是不同的,0.11内核是两个扇区对应一个盘块,最大的文件系统为:1024B * 1024 * 8 = 8M;8M * 8 = 64M,当前linux最大支持64M的块设备大小

不管读取什么磁盘上的资源,都是先getblk(获取该资源对应的设备和块号的高速缓冲区),然后再bread(确认有效数据的高速缓冲区),最后进行区域内存的拷贝从从bh(buffer_head)的b_data数据区域拷贝到要用到数据的内存中(buffe(hd read(char * source char * desbuf uint size))

source --> bh->b_data

desbuf --> 硬盘

_bmap(struct m_inode * inode,int block,int create): 对文件进行磁盘映射,也就是给m_inode结构体中的unsigned short i_zone[9] inode节点的磁盘块映射。i_zone前7个存放的是逻辑块号(7K内存),第八个是一次间接块号(前面是指针,这里就是指针的指针),指向一个存放512(1个逻辑块号是1k,short类型是2B,所以有512个)个逻辑块号的数组。第九个是二次间接块号(指针的指针的指针),指向512个一次间接块号。

从磁盘读写数据 信息 inode 等的流程:

  1. 找到指定的dev
  2. 通过dev找到设备的super_block
  3. 通过super_block返回值sb中的信息计算要读的块号
  4. 调用bread将其读取或写入到高速缓冲区
  5. 读: 将高速缓冲区的b_data读到你要读的内存地址放高速缓冲区, 写: 将要写入的数据写入高速缓冲区的b_data,并设置dirt修改标志位,等待系统的sys_sync进行写盘,释放高速缓冲区

super.c作用:

  1. 对设备的超级块进行操作(获取get_super 读取read_supper 释放put_supper)
  2. 因为超级块是设备文件系统的映射(代码中的类),超级块的操作关系到设备的文件系统操作 文件系统的加载/卸载 sys_mount sys_umount
  3. 根文件系统的加载( / )mount_root

namei.c作用:根据输入字符进行文件目录的建立、删除、打开 节点建立 删除。

namei.c目的:文件系统中的文件操作(打开、权限、属性)、了解文件系统的命令操作(如chmod chown mknod)、c语言中内存区域的检索和管理。

链接文件在内核中就是给已存在的文件,添加一个dir_entry:

  1. 找到已存在文件的Inode
  2. 在制定的文件路径中创建一个新的dir_entry
  3. 把新的dir_entry映射到老的inode上去

8.Linux内存管理

学习目的:

  1. Linux内存的管理机制(分段 分页)
  2. 虚拟内存和物理内存的映射方式
  3. 内存与磁盘的交互(分页机制 缺页重读机制 用时拷贝机制)
  4. 应用程序如何高效使用内存和高级程序的设计方法

Linux内存使用情况(用户内存和内核内存分开):

内核内存高速缓冲区(包括提供给BIOS的和显存)[虚拟盘]主内存(用户内存)

内存管理名词:

  1. 逻辑地址:程序员看到的地址,Linux操作系统分配给每一个进程的独立的地址。
  2. 线性地址:总线地址(=逻辑地址+段基地址)
  3. 物理地址:物理内存的地址,CPU总线的直接地址

虚拟内存的好处:

  1. 安全
  2. 能够提供给进程比物理内存大的多的多的内存空间
  3. 能够有效管理物理内存,并把零散的内存也映射给完整的虚拟内存

虚拟内存映射到物理内存的方式:

  1. 分页:包括页目录表(一级页表)和页表(二级页表),实现从线性地址到物理地址的转换。
  2. 分段:通过GDT(全局描述符,一个操作系统一张,在setup.S中创建),每个段有一个段基地址,实现从逻辑地址到线性地址的转换。

GDT从底向上依次为:null->代码段(内核的)->数据段(内核的)->系统段->状态段tss0(任务0)->局部表ldt0(任务0)->状态段tss1(任务1)->局部表ldt1(任务1)->状态段tss2->局部表ldt2......

分段: image.png

分页:

image.png image.png

内存管理主要实现了两个重要方式:

  1. 分页机制:缺页重读,在页目录和页表表项结构中,10位用于页目录寻址,10位用于页表寻址,其余12位的长度供其他权限使用,其中最低位P位是存在位,当P=1的时候该项可用,当目录表项或二级页表表项的P=0时,表示该项无效,通过这种机制实现了我的程序不用全部加载到内存,当用到的时候引发缺页中断再写进内存。倒数第二位R/W表示读还是写,倒数第三位U/S表示用户能用还是super权限能用。倒数第六位是Accessed位表示允不允许访问。倒数第7位表示Dirt位,如果置写了那么会执行同步操作

  2. 内存读写权限:用时拷贝。在A进程fork() B进程后,只是把A的虚拟内存copy给B,但此时A和B共用一段物理内存,并且把当前的共享内存设置为只读内存。一旦有A或B对这块内存进行写操作时,就会引发页面出错异常page_fault。在该异常的中断服务函数中,就会首先取消共享内存的操作,并且给写进程复制一个新的物理页面(写多少复制多少),此时A和B就各自有一块要写的内存,然后设置该内存为可读写状态,然后返回重新进行刚才的写操作。

以上内存分段分页机制是在主内存中,也就是用户内存,在内核内存是不进行分段分页机制的。内核内存结构:

向量表
vmalloc(连续的堆空间)
DMA+常规区域映射区(比如kmalloc也是在这里分配,以页为单位,所以内存可能不连续)
高端内存映射区(上面不够了从这里借)
内核模块(xx.ko)

slab 内存池:适合用在分配大量小对象的情景下(使用buddy算法管理)。创建kmem_cache_create();

ioremap: 物理地址映射为虚拟地址,建立了一个虚拟的页目录 页表等,把指定的物理地址放入建立好的页表关系中。

iounmap: 销毁掉ioremap建立的映射关系。IO读写中专门做寄存器读写的函数:readb readw readl writeb writel,前面函数都是带内存屏蔽的,也就是这行代码等待前面代码执行完再执行(编译乱序情况下),不带内存屏蔽的是readb_relaxed

mmap: 做了内存和设备的关联,将用户空间的一段内存与设备关联,当用户访问这段内存的时候就会自动转化为对这段物理内存的访问。

munmap: 取消mmap做的内存和设备的关联。

程序在硬盘到执行做的处理:

  1. 在运行程序之前,首先从硬盘中把程序的头通过高速缓冲区读到内存(通过fork函数)
  2. fork-->copy_process(拷贝到栈空间中)-->创建ldt和tss
  3. 程序运行之前要传入参数和环境变量,创建参数和环境变量的存储页面(在代码段上面)(使用copy_strings()函数),设置新函数的sp,重新调整代码段

9.Linux驱动

字符设备驱动

字符设备驱动程序调用过程:用户app->Linux系统调用->虚拟文件系统->具体硬件操作。

写的.c驱动程序添加到sourceinsight中,写.c驱动程序的时候就可以基于内核提供的一些函数,编写代码会有代码补充什么的比较方便。与.c驱动文件在一块的还有个Makefile编译文件,一般结构为:

makefile
KERN_DIR = (内核根目录) all: make -C $(KERN_DIR) M='pwd' modules clean: make -C $(KERN_DIR) M='pwd' modules clean rm -rf modules.order #生成char_drv_1.o文件 m表示不加入内核中 如果是y则表示加入内核中 obj-m += char_drv_1.o

Makefile文件在编译时,会通过根目录下的.config文件传入参数,编译的时候会生成auto.conf和autoconf.h文件,auto.conf是给编译用的表示要将那些程序编译到内核镜像中,autoconf.h是给c程序用的, 里面其实添加的宏定义。

写驱动的方法:

  1. 直接用file_operations写驱动程序,然后register_chrdev注册,参照http://blog.futurezxt.work/post/28 的代码1 2 3。缺点是一个major只能对应一个fops。(2.x版本的内核)
  2. 3.x版本的内核是每个major对应很多cdev,每个cdev对应minor~minor+size对应一个fops。
    1. 注册cdev: char_dev_struct;
    2. 分配cdev:register_chrdev_region(分配第2 3 4个的时候用)和alloc_chrdev_region(分配第一个的时候用)
    3. cdev_init()对cdev结构体进行清空,设置cdev设备树对象,并和fops绑定。
    4. cdev_add()把cdev加到所属major的region中

SMP概念:SMP是Symmetric Multi Processing的简称,意为对称多处理系统,内有许多紧耦合多处理器,这种系统的最大特点就是共享所有资源。在这种技术的支持下,一个服务器系统可以同时运行多个处理器,并共享内存和其他的主机资源。一个CPU的多个核可能运行着不同的进程,各种进程之间通信是一个需要解决的复杂问题,有以下几种解决方式:

  1. 内存屏蔽 barrier
  2. 中断屏蔽,注册一个中断request_irq(); 取消free_irq(); 定义一个工作队列struct work_struct kindlemem_work();(是把所有的中断下半部放在一个线程中。还有线程中断threaded_irq是把每个中断下半部都作为一个线程)。
  3. 原子操作:对整形临界区的保护(原理是在操作时对总线进行监控),相关函数:atomic_t atomic_a = ATOMIC_INIT(int i) atomic_set(atomic_a, int i) atomic_a = atomic_read(atomic_a) atomic_add() atomic_sub() atomic_inc() atomic_dec() atomic_inc_and_test() atomic_dec_and_test() atomic_sub_and_test() atomic_add_return() set_bit() clear_bit() change_bit() test_bit() test_and_set_bit()。缺点:只能对int做操作。
  4. 自旋锁spin_lock,定义spinlock_t lock; 初始化spin_lock_init(spinlock_t *lock); 锁定/获取锁spin_lock(spinlock_t *lock); 释放锁 spin_unlock(spinlock_t *lock); 锁之间不允许被调度出去,所以要关开中断。
  5. 自旋锁衍生 读写自旋锁,读写锁优点是可以有多个进程去读,只能有一个进程去写。定义rwlock_t my_rwlock; 初始化rwlock_init(my_rwlock); 读锁定read_lock(); 读解锁read_unlock(); 写锁定write_lock(); 写解锁write_unlock(); 尝试获得write_trylock()
  6. 读写自旋锁衍生 顺序自旋锁,优点:多个进程读的时候也能写,判断读完数据有没有变化,如果有变化重读。定义seqlock_t *my_seqlock; 初始化seqlock_init() 获得顺序锁write_seqlock() 读开始read_seqbegin() 重读函数read_seqretry()
  7. RCU 读复制更新,专门用于链表操作,保护链表结构体中的结构体对象安全与不发生竞态。读锁定rcu_read_lock(); 读解锁rcu_read_unlock(); 替换list_replace_rcu(); 更新synchronize_rcu();(阻塞函数) 释放复制的节点kfree();
  8. 互斥体,互斥体定义struct mutex kindle_mutex; 初始化mutex_init(); 获取mutex_lock(); 释放mutex_unlock();。与锁的区别是允许有进程切换和临界区较大的时候。
  9. 互斥锁mutex_lock,
  10. 完成量,一个执行单元执行完之后另外一个才能执行。定义struct completion kindlemem_completion;也允许上下文切换,等待的任务放在等待队列中。初始化init_completion(); 等待完成量wait_for_completion() 唤醒第一个节点completion(); 唤醒等待队列所有节点completion_all();
  11. 信号量,多用于PV操作(生产资源消费资源)同步,定义struct semaphore kindlemem_semaphore; 初始化sema_init() 获取down() down_interruptible() 释放up()

DMB: 数据内存屏蔽:保证所有DMB之前的内存访问行为完成。

DSB:数据同步屏蔽:保证DSB之前的所有指令完成操作。

ISB:指令同步屏蔽:刷新CACHE。

子系统驱动

设计思想:同类型的子系统开发一个核心层代码,以输入子系统为例,主要涉及input_dev和input_handler。

image.png

不同鼠标键盘的驱动向核心层注册input_dev和input_handler,最终用户调用read/write一级一级的找到目标硬件设备的驱动。

image.png

输入子系统驱动

image.png

定时器使用方法

  1. 初始化定时器init_timer(struct timer_list xx_timer)
  2. 设置定时器的中断函数xx_timer.function=(void *)xx_timer_handle
  3. 把定时器提交给系统定时器链表add_timer(&xx_timer)
  4. 触发定时器mod_timer(&xx_timer,jiffies+HZ/100)

MISC和platform驱动

MISC(多种多样的设备驱动)子系统,是Linux预留的分层结构,在写驱动时如果没有对应的核心层时所使用的,主设备号为10,从设备号可自定义。

image.png

image.png

对于platform驱动在platform_device中对用到的资源进行描述(IO/中断/mem/DMA),指定个名字,与platform_driver指定的名字匹配后执行platform_driver的probe函数,进行资源初始化给定fops等。

LCD驱动:核心层fmmem.c

触摸屏驱动:核心层input.c(input_dev和input_handler),事件驱动。优化:

  1. 按一个点跳动很大,进行延迟优化,比如按键的去抖动,机械会产生震动。
  2. 当等待ADC转化的时候,(ADC还没有转化完)但是此时笔已经拍起来了。ADC转化完进行判断,如果此时笔已经抬起来了,丢弃这个点并且转化到等待笔抬起来状态。
  3. 多次测量取平均,进行精度优化,在ADC的完成中断中 多次触发ADC中断,进行多次测量取平均值。
  4. 去除在平稳运行中的毛刺点,用数字滤波技术(过滤功能和纠正功能),比如差分滤波、中值滤波

linux项目-电子相框(Kindle系统构架)

  1. 硬件设计

    1. 由客户和系统架构师提出硬件参数需求(CPU 内存 硬盘 外设)
    2. 硬件原理图工程师设计原理图、Layout工程师设计PCB、硬件选型需要和嵌入式工程师沟通,嵌入式工程师首先和客户沟通,如果客户不懂行凭借自己的经验、公板的demo、评价任务选择。
  2. 系统构建

    1. hello world测试看能不能跑(串口能不能用 cpu能不能 能跑几个等,如果有问题找硬件工程师)(bist测试)
    2. 进行BOOTLOADER的移植,一般用uboot,让uboot提供下载和正常的启动过程。
    3. 操作系统的移植(选一个合适的Linux内核版本):系统级别的移植(CPU的适配 内核裁剪(不需要的架构、用不到的驱动等) 分区 兼容性 节能性)。
    4. 驱动的移植:不仅要考虑硬件,还要考虑操作系统版本(找到芯片的裸板驱动demo,了解相关Linux版本的对应芯片的驱动框架,然后将裸板demo中的硬件操作加入框架中)。
  3. 底层高级驱动设计

    1. 设计所有需要使用的硬件驱动,将原芯片demo移植到当前linux内核版本中。主要是熟悉对应芯片的驱动框架,读懂demo怎么控制的硬件。
  4. 应用层设计

    1. 应用层库系统构建,如freetype tslib mjpeg socket qt(都是基于根文件系统)
    2. 库函数编程:通用方法
    3. 应用程序系统的构架:分层分离设计、顶层设计、模块设计

一、需求确定:

  1. 显示公司L0G0和显示开机画面
  2. 读取配置文件,并且按要求加载配置的需求进程
  3. 人机交互界面的及对话框的设计(前端)
  4. 电子书的操作
  5. 菜单 进行书签
  6. 后台,数据存储方式控制处理 通信

二、设计框架

  1. 人机交互进程:
    1. tslib线程:得到触摸上报数据,并上报给事件监听线程
    2. 按键线程:得到按键上报数据,并上报给事件监听线程
    3. 事件监听线程:得到底层驱动读写线程的数据后,进行事件的封装
    4. 事件上报线程:把事件上报给主控线程(添加事件队列的方式)
    5. 主控线程:把事件队列里面的事件用socket发给后台控制进程
  2. 后台控制进程:
    1. SOCKET监听线程:接收其他进程发来的事件
    2. 主页线程:控制当前LCD显示什么
    3. 向上线程:准备向上点击后的显示数据,并准备DMA
    4. 设置线程:
    5. 存了菜单的图片数据,和多种设置存储线程
    6. 主控线程:进行事件解析并分发调用其它各种线程
  3. 网络传输进程
  4. 文件管理存储进程等等都可以添加

内核信号项目-写一个自己的信号

也是一种中断,所以在system_call.s中会出现。

do_signal不是接受信号然后判断信号值,在中断中执行对应的函数,而是将对应的信号处理程序句柄插入到用户的堆栈中(具体的函数实现代码在文件系统中)。

image.png

u-boot移植项目

uboot负责设置系统时钟、关闭中断、设置到svc32模式(超级保护模式,复位/软件中断进入)、初始化硬件、进行uboot代码的重定义、给内核进行参数配置(放到tagglist中)、做内核代码的重定义、启动内核。(如果bootloader比较大,做重定位到SDRAM)

在source insight中add代码,尤其arm架构中的只add和单板相关的代码:

image.png

uboot使用过程:

  1. 根据自己单板的配置文件执行make xxx_config,会生成config.mk给makefile编译使用,生成config.h给c语言使用。
  2. 然后编译make (依赖u-boot.lds文件生成u-boot.bin)

执行的时候程序先执行start.S:

  1. 定义中断异常向量表
  2. set the cpu to svc32 mode
  3. turn off the watchdog
  4. mask all IROs
  5. 设置时钟分频系数
  6. cpu_init_crit()刷新v3/v4cache v4TLB、失能MMU,跳转lowlevel_init()
  7. 设置了栈call_board_init_f() 运行nand flash的初始化操作
  8. 把bootloader拷贝到SDRAM上(全部的bootloader)

移植过程:

  1. 先找一个同型号或者型号相差不大的Demo
  2. 比如移植smdk2440,先找smdk2410那里出现了grep "smdk2410" * -nR:
bash
arch/arm/include/asm/mach-types.h:1645:# define machine_is_smdk2410() (machine_arch_type == MACH_TYPE_SMDK2410) arch/arm/include/asm/mach-types.h:1647:# define machine_is_smdk2410() (0) board/samsung/smdk2410/Makefile:8:obj-y := smdk2410.o boards.cfg:72:Active arm arm920t s3c24x0 samsung - smdk2410 David Müller <d.mueller@elsoft.ch>

有以下文件:mach-types.h:1645、mach-types.h:1647、Makefile:8、boards.cfg:72。

  1. boards.cfg是板子配置表(名字、arch、cpu、soc等),复制粘贴一个目标板卡的,其他文件修改类似。最后配置一下make smdk2440 然后编译make。大概看看错误,下面从头开始修改代码。

  2. start.s 修改设置svc32模式(cpsr寄存器定义),看门狗寄存器位,失能中断,时钟分频,总线模式,启动ICACHE,SDRAM。

虚拟化

通过虚拟化技术可以将一个物理计算机分割为多个虚拟计算机,提高硬件的利用率。

目前全球排名前5的虚拟化软件公司: VMware、微软、红帽(RedHat) 、Oracle等

虚拟机由虚拟化层、虚拟层和管理层三部分组成。

C++智能指针

作用:动态内存管理,动态分配对象之后自动释放资源,防止内存泄漏。

  1. unique_ptr定义在<memory>中的一个智能指针,对对象有独有权,两个unique_ptr不能指向一个对象,不能进行复制操作只能进行移动操作。
  2. shared_ptr是C++标准库中的一个智能指针,共享对一个控制块的访问权限,通过引用计数表示有几个指针指向该对象,计数为0的时候,控制块删除内存资源和自身。
  3. weak_prt定义在<memory>中的一个智能指针,协助shared_ptr,解决shared_ptr指针循环引用问题。

unique_ptr和shared_ptr两个智能指针的区别:

  1. 所有权管理方式:unique_ptr独占式,某一个指针拥有所有权,shared_ptr是共享式,可以好几个指针共同指向某块内存进行控制。
  2. 内存管理方式不一样:unique_ptr所有权转移方式,shared_ptr使用引用计数的方式。
  3. 性能:shared_ptr在一个堆上引用计数,构造和赋值比unique_ptr慢,也涉及额外的线程的开销。

Linux客户端与服务端应用

服务器端使用API函数流程:

socket()-->bind()-->listen()-->accept() -->recv ()-->close()

客户端流程使用API函数流程:

socket()-->connect()-->send ()-->close()

select模型:可以同时监听多个文件描述符上的IO事件,从而实现高效地处理并发IO操作。IO多路复用select函数,通过将需要监听的文件描述符以位图的形式传递给select函数,并设置超时时间,当有任何一个文件描述符上的IO事件发生时,select函数返回,开发者可以遍历所有文件描述符来处理就绪事件。可用于网络编程、高并发服务器、IO密集型应用等。

Linux进程管理

ARM64处理器内核地址空间布局

image.png

ARM64处理器异常级别

image.png

支持虚拟化的异常切换

image.png

同步异常:执行指令时生成的异常。

异步异常:中断IRQ、快速中断FIQ、系统错误SE。

异常处理:

  1. 保存处理器状态到寄存器SPSP_EL1(程序状态寄存器,Saved program status register)
  2. 保存返回地址在寄存器ELR_EL1(异常链接寄存器)中。
  3. 把处理器状态的DAIF(debug system irq frq)这4个异常掩码位都设置为1。
  4. 如果是同步异常或系统错误异常,将异常类别存放到ESR_EL1寄存器中。
  5. 如果是同步异常,把错误地址保存在寄存器FAR_EL1(错误地址寄存器)。
  6. 如果处理器处于用户模式(异常级别0),那么把异常级别提升到1。
  7. 根据向量基准地址寄存器VBAR_EL1、异常类型和生成异常的异常级别计算出异常向量虚拟地址,执行向量。

CFS调度器

Linux内核作为一个通用操作系统,包括实时进程、交互式进程、批处理进程。

  • 交互式进程:与人机交互的进程,比如和鼠标、触摸屏、键盘等相关的应用。vi编辑器、vim编辑器、gedit编辑器。此类进程系统响应时间越块越好,否则用户就会报怨系统卡顿状态。
  • 批处理进程:可能会占用比较多的系统资源,例如编译代码等。
  • 实时进程:有些应用对整体时延有严格要求,例如现在非常火的VR设备。

操作系统一些命令

shell
# 显示file文件的前10行 head -n 10 file # 显示file文件的后10行 tail -n 10 file # 合并文件 cat file1 file2 > file3 # 显示大文件时用more,一次显示两行 more -2 file # 显示当前登录用户的所有信息 who -a # 显示当前操作系统的信息 uname -a # 寻求帮助,用于查看命令/函数/文件的帮助信息 man # 改变文件所有权(chown/chgrp) chown [] 用户名 文件名称 # 更改文件所有者 chown -R 用户名 文件名 # 更改文件属于的组 chgrp 用户名 文件名 # 修改文件权限 chmod 777 文件名/文件夹 # 创建软链接(快捷方式) ln 源文件 新文件 # 查看文件的i_node,如果两个文件是软链接关系的话,i_node值一样 ls -li 文件名 # 输入输出重定向(</>) # '>'将程序的标准输出或标准错误输出重定向到指定的文件或设备 # '<'将标准输入流从键盘改为来自其他文件或其他命令的输出 # ubuntu软件包管理 apt-get # 查看软件安装位置 dpkg -L 名称 # which 名称 # 查看系统已经安装的包 dpkg --get-selections # 硬盘发展IDE/SCSI/SATA # linux文件系统常用的3种类型Ext2/Ext3/Ext4/XFS/JFS # swap交换分区,类似虚拟内存,当内存不足时,把一部分硬盘虚拟为内存使用 # 挂载文件系统 mount [-t 文件系统类型] [-o 挂载选项,如读写权限访问控制等] 设备文件名 挂载点 # fsck检查及修改文件系统 fsck -C -t 文件系统名 # 在磁盘创建文件系统 mkfs --help # 建立磁盘分区表 fdisk # 压缩工具gzip(直接将原文件替换) gzip 文件名 gzip -r 目录 # 解压 gzip -d 文件名 # 打包压缩工具tar tar -cvf 生成的文件名.tar file1 file2 目录1 目录2 # 解压 tar -xf 生成的文件名.tar -C 保存的目录 # 其他压缩工具:bzip2/zip # 显示当前用户执行过的命令历史记录 history # 管理用户账号,比如修改账号名 usermod -l 新名字 原名字 # 查看用户信息: id命令 id 用户名 # 用户之间切换: su命令 su 用户名 # 监视进程 ps命令,显示当前所有在线用户和运行状态等 ps -aux # 显示某一个用户的进程 ps -u 用户名 # 即使跟踪进程:top命令 top # 向进程发送信号 kill向进程发送终止操作 kill PID号 # 调整进程的优先级nice/renice # 使用ps命令查看进程nice值并降序排列 ps axo pid,comm,nice --sort=-nice # eth和 ens 区别:一个物理网卡,一个是虚拟网络会话。 # 启动和关闭指定网卡 ifconfig 网卡名 up/down # 配置IP地址 ifconfig 网卡名 地址 # 报告CPU统计数据: iostat命令,监控系统磁盘IO活动情况,读写速率等。 # I/O监控:iotop命令,内核输出的I/O使用情况,每个进程线程的带宽 # 报告CPU统计数据: mpstat命令 # 虚拟内存统计,使用情况、进程活动、磁盘I/O和CPU使用率 vmstat -a

本文作者:zzw

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 License 许可协议。转载请注明出处!