zoukankan      html  css  js  c++  java
  • Linux内核设计与实现 读书笔记

    第二章 Linux内核

    1 内核开发特点

    1)内核编译时不能访问C库;

    2)浮点数很难使用;

    3)内核只有一个定长堆栈;

    4)注意同步和并发。

    第三章进程管理

    1 current宏:查找当前运行进程的进程描述符。

    2 进程状态(5种)

    TASK_RUNNING 1)正在运行;2)在运行队列中等待执行。

    TASK_INTERRUPTIBLE:进程正在睡眠,可以被信号唤醒。

    TASK_UNINTERRUPTIBLE:进程正在睡眠,不会收到信号被唤醒。

    TASK_ZOMBIE:僵死态,进程已经结束,父进程未使用wait4()

    TASK_STOPPED

    3 进程上下文

    进程进入内核空间时,current宏依然有效,内核“代表进程执行”。

    4 进程创建

    1fork():拷贝当前进程创建一个子进程。

    2exec():读取可执行文件并载入地址空间开始运行。

    3)写时拷贝(copy-on-wrtie):推迟数据拷贝,在需要写入数据时,数据才会被复制。

    4vfork():不拷贝父进程的页表项,子进程作为父进程的一个线程在它的地址空间运行,父进程被阻塞直至子进程退出,子进程不能向地址块空间写入数据。

    5 线程

    Linux把所有的线程都当作进程来实现。

    6 内核线程:独立运行在内核中的标准进程。内核线程没有独立的地址空间,只能在内核空间中运行,创建内核线程用kernel_thread()

    7 进程终结

    1)释放资源;

    2)进入TASK_ZOMBIE

    3)等待wait4()

    第四章进程调度

    1 多任务系统

    非抢占式多任务:主动让步

    抢占式多任务(preemptive):时间片

    2 进程

    IO消耗型:常常阻塞

    处理器消耗型:执行代码

    3 动态优先级调度方法

    允许调度程序根据需要加减优先级。

    两组优先级范围:

    1nice值:-20+19,默认值为0nice值越大,优先级越低。

    2)实时优先级:099,任何实时进程优先级都高于普通进程。

    4 时间片

    默认时间片为20ms

    进程时间片用完——进程到期——所有进程都到期——重新计算时间片

    5 可执行列队(runqueue):每个处理器一个的可执行进程链表,还包含每个处理器的调度信息。

    cpu_rq(processor):返回给定处理器的可执行队列指针。

    this_rq():返回当前处理器的可执行队列。

    task_rq(task):返回给定任务所在的队列指针。

    this_rq_lock():锁住当前可执行列队。

    rq_unlock():释放给定列队上的锁。

    ★为了避免死锁,要锁住多个运行列队的代码,需要按同样的顺序获得这些锁。

    6 优先级数组

    每个运行列队有两个优先级数组:一个活跃的,一个过期的。

    优先级数组包含一个优先级位图,共有140个优先级用了532位长整形保存。

    活动数组:可执行队列上的进程还有时间片剩余。

    过期数组:可执行队列上的进程耗尽了时间片。

    ★进程从活动数组移动至过期数组时重新计算时间片。

    ■重新计算时间片,以静态优先级为基础计算。

    ■为了判断Linux进程类型,Linux记录了进程用于休眠和执行的时间。

    ■若进程交互性非常强,时间片用完后,会被再次放入活动数组。

    7 休眠

    休眠通过等待列队进程处理。

    等待列队:wait_queue_head_t

    静态创建等待列队:DECLARE_WAITQUEUE

    动态创建等待列队:init_waitqueue_head()

    加入等待列队:add_wait_queue()

    移出等待列队:remove_wait_queue()

    8 负载平衡

    保证可执行列队之间的负载处于均衡状态(用于SMP)。

    两种调用方法:1)当前可执行列队为空;2)系统定时器200ms一次。

    9 上下文切换

    1)将虚拟内存从上一个进程映射到新的进程中;

    2)将上个进程处理器的状态切换到新的进程,保存、恢复栈信息和寄存器信息。

    10 schedule()被调用的时间

    1)某个进程耗尽时间片时;

    2)优先级高的进程进入可执行态时;

    3)返回用户空间时;

    4)中断返回时。

    11 内核抢占

    只要没有持有锁,内核就可以进行抢占。preempt_count计数器记录内核锁的数量。

    内核抢占时机:

    1)从中断返回内核空间时;

    2)当内核代码再一次具有可抢占性的时候;

    3)内核任务显示调用shedule()

    4)内核任务阻塞。

    12 实时性

    内核实时调度策略:

    1SCHED_FIFO:简单,先进先出调度;

    2SCHED_RR:时间片轮转调度,耗尽时间片后不再执行。

    SCHED_NORMAL 是普通、非实时的调度策略。

    ■内核实时调度策略是基于静态优先级的:内核不为实时进程计算动态优先级,实时优先级范围:0-99

    ■动态非实时优先级范围:100-139(对应nice-20+19)。

    第五章系统调用

    1 应用程序通过软中断机制通知内核。

    2 参数验证

    指针:

    1)指针指向的内存区域属于用户空间;

    2)指针指向的内存区域属于本进程地址空间;

    3)如果读,内存标记为可读;如果写,内存标记为可写。

    3 copy_to_user()的三个参数:

    1)进程空间的目的地址内存;

    2)内核空间源地址;

    3)需要拷贝的数据长度(字节数)。

    copybit_from_user()和上面相反。

    第六章中断和中断处理程序

    1 上半部和下半部

    上半部:中断处理程序

    下半部:稍后完成的工作

    2 注册中断

    int request_irq(irq, *handle, irqflag, *devname, *devid)

    irq                中断号。

    handle                一个指针,指向中断处理程序。

    irqflag                标志位:SA_INTERRUPT 快速中断处理程序(禁止其他中断);SA_SHIRQ:可在多个中断处理程序间共享中断线。

    devname                与中断相关的设备的ASCII文本表示法。

    dev_id                用于共享中断线,当中断需要退出时,dev_id提供唯一的标志信息。从共享中断线的中断处理程序中删除指定程序。

    3 释放中断

    free_irq(unsigned int irq, void *dev_id)

    4 中断处理程序

    irqreturn_t intr_handle(int irq, void *dev_id, struct pt_regs *regs)

    regs:一个指向结构体的指针,包含中断处理之前的处理器寄存器和状态,regs使用越来越少,应该忽略。

    返回值irqreturn_t

    1IRQ_NONE:中断对应设备并不是注册函数期间指定的产生源;

    2IRQ_HANDLE:正常返回值。

    重入问题:Linux的中断处理程序无需考虑重入,当一个给定的中断处理程序正在执行,相应的中断会被屏蔽。

    5 中断上下文

    ★中断上下文不可以睡眠,不能从中断上下文中调用睡眠函数。

    中断栈:现在中断处理程序拥有自己的栈,每个处理器一个,大小为4KB

    6 中断控制

    local_irq_disable():禁止当前处理器上的本地中断;

    local_irq_enable():允许中断;

    local_irq_save(flag):禁止中断并保存系统状态;

    local_irq_restore(flag):恢复中断并恢复到原来的状态;

    disable_irq(unsigned int irq):禁止指定中断线;

    disable_irq_nosync():不等待当前中断处理完毕就禁止指定中断;

    enable_irq(unsigned int irq):允许指定中断;

    synchronize_irq():等待一个特定的中断处理程序的退出。

    ★如果调用了两次disable_irq(),需要对应调用两次enable_irq()才可以恢复。

    第七章下半部和推后执行的工作

    1 下半部

    1)缩短中断执行时间;

    2)下半部执行时允许响应所有的中断。

    2 下半部的类型

    1BH:早期实现,现在已经淘汰;

    2)任务列队(task queue);

    3)软中断(softirq):静态定义的下半部接口,共32个,可在所有处理上同时执行。

    4tasklet:基于软终端的动态下半部实现,不同类型的tasklet可在不同处理器同时执行,相同类型的tasklet不可以同时执行。

    5)工作列队(work queue):取代任务列队,对推后执行的工作排队。

    6)内核定时器:将操作推后到某个确定的时间段。

    3 软中断

    1)软中断最多有32个。

    2)一个软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序。

    3)软中断的执行时机:(1)从一个硬件中断代码返回时;(2)在ksoftirq内核线程中;(3)显示检查和执行处理的软中断代码中,如网络子系统。

    4)软中断保留给系统中对时间要求最严格和最重要的下半部使用:如网络和SCSI

    4 tasklet

    1tasklet通过软中断实现,本身也是软中断,有两种:HI_SOFTIRQ(优先级较高)和TASKLET_SOFTIRQ

    2)的状态有:0TASKLET_STATE_SCHED(已经被调度,正等待运行)和TASKLET_STATE_RUN(正在运行,应用于多核系统)。

    3tasklet的调度过程:

    1)检查tasklet的状态是否为SCHED——>返回;

    2)保存中断,禁止本地中断;

    3)将调度tasklet加入tasklet_vec或者tasklet_hi_vec表头;

    4)唤起TASKLET_SOFTIRQHI_SOFTIRQ中断——>do_softirq()

    5)恢复中断并恢复原状态。

    4)使用tasklet

    1)声明

    静态创建

    DECLARE_TASKLET

    DECLARE_TASKLET_DISABLED

    动态创建

    tasklet_init(t,tasklet_handle,dev)

    2handle

    void tasklet_handle(unsigned long data)

    ★由于tasklet靠软中断实现,所以tasklet执行中不能睡眠——>不能使用信号量或阻塞函数。

    tasklet运行时运行中断,需要做好保护工作。

    3)调度

    tasklet_schedule(&my_tasklet)        加入调度列队

    tasklet_disable()                禁止莫个tasklet,若正在执行则等待执行完毕再禁止

    tasklet_disable_nosync()        立即禁止(不太安全)

    tasklet_enable()                激活一个tasklet

    tasklet_kill()                        去掉列队的第一个tasklet

    4ksoftirq 辅助软中断内核线程

    内核不会立即处理重新触发的软中断,当大量软中断出现时,内核会唤起一组线程来处理这些负载。(nice=19

    5 工作队列(work queue

    1)工作队列可把工作推后,交由一个内核线程去执行。

    2)工作队列在进程上下文执行,允许重新调度甚至是睡眠,但无法访问用户空间。

    3)实现

    工作者线程 events/nn是处理器编号。

    worker_thread()函数:执行一个死循环并休眠,当有操作,线程被唤醒。

    4)使用工作队列

    1)声明

    静态创建

    DECLARE_WORK(name, void(*func)(void*), void *data)

    动态创建

    INIT_WORK(struct work_struct *work, void (*func)(void*), void *data)

    2)处理函数

    void work_handle(void *data)

    3)调度

    shedule_work(&work) 马上调度

    shedule_delayed_work(&work,delay) 延时调度

    4)刷新

    flush_scheduled_work(void)

    等待队列中所有对象都被执行后才返回。

    cancel_delayed_work(struct work_struct *work)

    取消延时的工作。

    5)创建新的工作列队

    可在默认的工作列队外创建新的进程:

    creat_work(const char *name)

    queue_work()        类似schedule_work(),针对自己的进程。

    queue_delayed_work()

    flush_workqueue()

    6 下半部机制的选择

    下半部                上下文                顺序执行要求

    软中断                中断                没有

    tasklet                中断                同类型不可以同时执行

    工作队列        进程                没有(和进程上下文一样被调度)

    1)易用性:工作列队>tasklet>软中断

    2)速度:软中断>tasklet>工作队列

    3)开销:工作队列>>tasklet、软中断

    7 下半部的锁机制

    1tasklet

    1)相同类型的tasklet不允许同时执行,无需同步。

    2)不同类型的tasklet需使用锁机制。

    2)软中断所有共享数据结构都需要合适的锁。

    3)进程上下文和下半部共享中断,上下文访问共享数据前,需禁止下半部并获取锁的使用权。

    4)中断上下文和下半部共享中断,需禁止中断并取得锁使用权。

    local_bh_disable() 禁止本地处理器的软中断和tasklet处理,不用禁止工作列队。

    local_bh_enable()

    第八章内核同步介绍

    1 临界区和竞争条件

    临界区:访问和操作共享数据的代码段。

    竞争条件:两个执行线程处于同一个临界区中。

    2 内核中造成并发的原因

    1)中断:任何时刻异步发生,打断当前执行的代码。

    2)软中断和tasklet:任何时刻唤醒或调度软中断、tasklet

    3)内核抢占(preempt)。

    4)睡眠及与用户空间的同步:唤醒调度程序,调度新进程执行。

    5)对称多处理器(SMP)。

    3 需要加速的代码

    1)中断安全代码;

    2SMP安全代码;

    3)抢占安全代码。

    ★给数据加锁而不是给代码加锁。

    4 编程需注意的问题:

    1)数据是否全局?除了当前线程,其他线程是否可以访问?

    2)数据是否在进程/中断上下文中共享?是否在两个不同中断中共享?

    3)进程在访问数据时可否被抢占?被调度的新进程是否会访问同一数据?

    4)当前进程是否会睡眠(阻塞)在某些资源上?共享数据处于何种状态?

    5)怎样防止数据失控?若在另一处理器上调度?

    5 预防死锁

    1)★加锁顺序是关键,使用嵌套锁必须以相同顺序获取锁;

    2)防止发生饥饿;

    3)不要重复请求同一个锁;

    4)复杂的加锁方案也可能造成死锁——简化设计;

    5)建议以获取锁相反的顺序来释放锁。

    第九章内核同步方法

    1 原子操作

    原子操作执行过程不被打断,原子操作接口分为整数操作接口和单独位操作接口。

    2 原子整数操作

    只有atomic_t类型可用于整数原子操作。

    atomic_t类型保证编译器不对相应的值进行优化。

    atomic_t类型可以屏蔽不同体系结构上实现原子类型的差异。

    atomic_t类型只能使用24位(现在已经没有这个限制)。

    定义:

    asm/atomic.h

    atomic_t v;

    atomic_t u = ATOMIC_INIT(0);

    操作:

    atomic_set(&v, 4)

    atomic_add(z, &v)

    atomic_sub(int i, atomic *v)

    atomic_inc(&v)

    atomic_dec(&v)

    atomic_read(&v) 转成整形

    atomic_dec_and_test(&v) 给定原子变量减1,若为0则返回真。

    3 原子位操作

    原子位操作对普通的内存地址操作,没有atomic_t类型。

    定义:

    asm/bitops.h

    操作:

    set_bit(int nr, void *addr)

    clear_bit(int nr, void *addr)

    change_bit(int nr, void *addr) 翻转

    test_and_set_bit() 设置并返回原先的值

    test_and_clear_bit()

    test_and_change()

    test_bit() 返回第nr

    非原子操作:

    __test_bit()

    4 自旋锁

    自旋锁最多只能被一个可执行线程持有。若一个线程试图获得一个被征用的自旋锁,线程会一直忙循环——选择——等待锁可用。

    缺点:由于自旋锁在等待时自旋(浪费处理器时间),因此自旋锁不应长时间持有。

    优点:线程不用睡眠,不用进行上下文切换。

    数据结构:

    结构相关代码:        asm/spinlock.h

    接口定义:        linux/spinlock.h

    声明:                spinlock_t mr_lock = SPIN_LOCK_UNLOCKED

    加锁:                spin_lock(&mr_lock)

    解锁:                spin_unlock(&mr_lock)

    自旋锁特点:

    自旋锁最多被一个线程持有,为SMP提供了并发保护机制。

    单处理器编程时并不会加入自旋锁,仅当做检测内核抢占的开关。

    如果禁止内核抢占,则自旋锁无效。

    自旋锁可用于中断处理程序中(信号量不可用于自旋锁,会自旋),中断中使用自旋锁首先应当关中断。

    自旋锁不可递归。

    自旋锁操作:

    spin_lock_irqsave(&mr_lock, flag) 保存中断当前状态,关中断,获取锁

    spin_unlock_irqstore(&mr_lock, flag) 恢复中断到加锁前状态并释放锁

    spin_lock_irq(&mr_lock)        关中断,获取锁

    spin_unlock_irq(&mr_lock) 关中断,释放锁

    因为很难搞清楚中断情况,推荐使用前者。

    其他操作:

    spin_lock_init()        动态初始化知道spinlock_t

    spin_trylock()                试图获取指定锁,若未获取则返回非0

    spin_is_locked()        如制定锁当前正在被获取,则返回非0

    4 读写自旋锁

    读写不同锁,多人和可并发持有读者锁,写锁只能被一个任务持有。

    用法:

    rwlock_t mr_rwlock = RW_LOCK_UNLOCKED;

    read_lock(&mr_rwlock)

    read_unlock(&mr_rwlock)

    write_lock(&mr_rwlock)

    write_unlock(&mr_rwlock)

    注意:

    1)不可将读锁“升级”为写锁,写锁会等待读锁。

    2)多个读者可以安全获得同一个读锁,也可递归获取读锁。

    5 信号量

    1)信号量的特点

    1Linux中的信号量是一种睡眠锁;

    2)信号量适用于锁会被长期占有的情况;

    3)由于信号量会睡眠,所以只能用于进程上下文中,中断上下文不支持调度;

    4)可以在持有信号量时睡眠;

    5)不可以在持有信号量时使用自旋锁;

    6)信号量允许任意数目的锁持有者,自旋锁一个时刻只允许一个任务持有;

    7)在声明时课指定信号量拥有的持有者数量。

    2)创建信号量

    定义:                asm/semaphore.h

    静态声明:        static DECLARE_SEMAPHORE_GENEIC(none count)

    互斥信号量声明:static DECLARE_MUTEX(name)

    动态创建:        sema_init(sem, count)

    动态创建互斥对象:init_MUTEX(sem) (注意大小写)

    3)使用信号量

    down()                信号计数减1,若为0或大于0,则获取信号量,若为负数,则任务等待。

    up()                信号计数加1,若等待队列不为空,唤醒等待任务。

    down_interruptible(struct semaphore*) 若信号量已被征用,则可进入中断睡眠状态。

    down_trylock()        试图获取信号量,若已被征用,则立刻返回非零值。

    6 -写信号量(都是互斥信号量)

    定义:                linux/rwsem.h

    静态声明:        static DECLARE_RWSEM(name)

    动态创建:        init_rwsem(struct rw_semaphore *sem)

    功能:                down_read()

    up_read()

    down_write()

    up_write()

    downgrade_writer() 动态将写锁转换为读锁

    7 自旋锁与信号量

    中断上下文中只能使用自旋锁。

    任务睡眠时只能使用信号量。

    需求                建议

    低开销加锁        优先使用自旋锁

    短期锁定        优先使用自旋锁

    长期加锁        优先使用信号量

    中断上下文加锁        使用自旋锁

    持有锁需要睡眠        使用信号量

    8 完成变量

    完成变量是同步两个任务的一种简单方法。

    定义:                linux/completion.h

    静态创建:        DECLARE_COMPLETION(mr_comp)

    动态创建:        init_completion()

    方法:                wait_for_completion(struct completion *) 等待指定的完成变量接受信号

    complete(struct completion*) 发信号唤醒等待任务

    9 禁止抢占

    内核代码使用自旋锁作为非抢占区域的标记。

    preempt_disable() 增加抢占计数值,从而禁止内核抢占

    preempt_enable() 减少抢占计数值,当减为0时检查和执行被挂起的任务

    preempt_enable_no_resched()

    preempt_count() 返回抢占计数

    get_cpu()

    put_cpu()

    10 顺序和屏障

    屏障(barrier):确保城乡运行顺序的指令。

    rmb()        阻止跨越屏障的转入动作发生重排序。

    read_barrier_depends() 阻止跨越屏障的具有数据依赖关系的载入动作重排序。

    wmb() 阻止跨越屏障的载入和存储动作发生重排序。

    mb() 阻止跨越屏障的载入和存储动作发生重排序。

    smp_rmb() SMP上提供rmb()功能,在UP上提供barrier()功能。

    smp_read_barrier_depends()

    smp_wmb()

    smp_mb()

    barrirer() 阻止编译器跨越屏障对载入或存储操作进行优化。

    第十章定时器和时间管理

    1 时钟中断的工作

    1)跟新系统运行时间;

    2)跟新实际时间;

    3)在SMP系统中均衡调度各处理器的运行列队;

    4)检查当前进程是否耗尽时间片,若耗尽则进行重新调度;

    5)运行超时的动态定时器;

    6)更新资源消耗和处理器时间的统计值。

    2 节拍率        HZ

    ARM         10010ms

    i386        10001ms

    x86-64

    提高HZ的优点:

    1)更高时钟中断解析度;

    2)提高时间驱动事件的准确性(平均误差5ms->0.5ms);

    3)内核定时器能够以更高的频度和更高准确度执行;

    4)依赖定时器的系统调用(如pollselect)能以更高精度运行;

    5)对资源消耗和系统运行时间的测量更准确;

    6)提供进程抢占的准确度。

    缺点:

    1)系统负担增加;

    2)中断占用处理器的时间增加。

    3 jifies

    jiffies全局变量用来记录系统自启动以来产生的节拍总数。

    定义:        linux/jiffies.h

    extern unsigned long volatile jiffies;

    extern u64 jiffies_64;

    jiffiesjifies_64的低32

    jiffies溢出回绕:

    #define time_after(unknown, known)        ((long)(known)-(long)(unknown)<0)

    #define time_before(unknown, known)        ((long)(unknown)-(long)(known)<0)

    4 时间中断处理程序

    流程:

    1)获得锁 xtime_lock

    2)需要时应答或重设系统时钟;

    3)周期性使用墙上时间(实际时间)更新实时时钟;

    4)调用架构无关的do_timer()函数;

    1)增加jiffies

    2)更新资源消耗的统计值;

    3)执行到期的动态定时器;

    4)执行schedule_tick()

    5)更新墙上时间->存有xtime中;

    6)计算平均负载。

    5 实际时间(墙上时间)

    定义;        kernel/timer.c

    struct timespec xtime;

    struct timespec {

    time_t tv_sec;        //秒,自1970.7.1开始的秒数

    long tv_nsec;        //纳秒,自上一秒开始的纳秒数

    }

    读写xtime需要用xtime_lock锁,是一个seqlock锁。

    用户空间获取墙上时间的接口:gettimeofday()

    内核空间获取墙上时间的接口:sys_gettimeofday() <-系统调用

    设置当前时间接口:settimeofday()

    6 定时器(动态定时器)

    定义:        linux/timer.h

    struct time_list my_timer

    初始化:

    init_timer(&my_timer);

    my_timer.expires = jiffies + delay;

    my_timer.data = 0;

    my_timer.function = my_function;

    处理函数格式:

    void my_function(unsigned long data)

    激活:        

    add_timer(&my_timer)

    修改定时时间:

    mod_timer(&my_timer, jiffies+delay)

    停止定时器:

    del_timer(&my_timer)

    del_timer_sync() <-用于SMP,不可用户中断上下文

    7 延迟执行

    1)忙等待

    unsigned long delay = jiffies + delays;

    while(time_before(jiffies, delay));

    2)代码等待时允许内核重新调度其他任务

    unsigned long delay = jiffies + delays;

    while(time_before(jiffies, delay))

    cond_resched();

    ★延迟执行在任何情况下都不应该在持有锁或禁止中断时发生。

    jiffies采用volatile型,每次循环都会从内存中读取。

    3)短延迟

    linux/delay.h

    void udelay(unsigned long usecs);

    void mdelay(unsigned long msecs);

    ★短延时是通过循环实现的。

    BogoMips不是为了表现机器性能,而是同udelaymdelay使用。

    4schedule_timeout()

    将需要延迟的任务睡眠至延迟时间耗尽(不能保证时间准确,因为会重新调度)。

    使用:

    set_current_state(TASK_INTERRUPTIBLE);

    schedule_timeout(X*HZ);

    使用场合为进程上下文且不能持有锁。

    第十一章内存管理

    1

    物理也是内存管理的基本单位。

    定义:        

    linux/mm.h

    struct page {

    page_flag_t        flags;                        //页的状态(脏、锁定)

    atomic_t        _count;                        //页的引用记录(0:内核没有引用,可用于新分配)

    atomic_t        _mapcount;

    unsigned long        private;                //页作为私有数据时使用

    struct address_space *mapping;                //页作为页缓存时使用

    pgoff_t                index;

    struct list_head lru;

    void                *virtual;                //虚拟地址

    }

    2 区(zone

    ZONE_DMA:包含的页用来执行DMA操作。

    ZONE_NORMAL:正常映射的页。

    ZONE_HIGHMEM:“高端内存”(并不能永久映射到内核地址空间)。

    3 获得页(获取页的接口)

    1sttuct page* alloc_pages(unsigned int gfp_mask, unsigned int order)

    分配2^x个连续物理页,并返回指向第一个页结构体的指针。

    2void* page_address(struct page * page)

    返回给定物理页的逻辑地址。

    3unsigned long __get_free_pages(unsigned int gft_mask, unsigned int order)

    alloc_page()相同,但直接返回第一页的物理地址。

    4struct page* alloc_page(gfp_mask)

       unsigned long __get_free_page()

    以上两个函数只分配一页。

    5unsigned long get_zeroed_page(gfp_mask)

    只分配一页,且内容填充0,返回逻辑地址指针。

    释放页:

    void __free_pages(struct page *page, unsigned int order);

    void free_pages(unsigned long addr, unsigned int order);

    void free_page(unsigned long addr);

    ★获取页后注意进行错误检查。

    4 kmalloc()

    定义:        linux/slab.h

    void* kmalloc(size_t, int flags)

    ★分配的内存在物理上是连续的。

    ★最大能分配128KB

    ★必须检查分配是否成功。

    kfree()kmalloc()对应

    void kfree(const void *ptr);

    5 gfp_mask标志

    该标志可以分为三类:

    1)行为修饰符:描述内核如何分配所需内存。

    2)区修饰符:从哪个区分配内存。

    3)类型:组合了行为和区修饰符,提供了常用的标志。

    类型标志:

    1GFP_ATOMIC                分配高级、优先,用于中断、下半部、锁中及其他不能睡眠的情况(分配成功率较低)。

    2GFP_NOIO                分配可阻塞,不会启动磁盘IO

    3GFP_NOFS                分配可阻塞,可能启动磁盘IO,不会启动文件系统。

    4GFP_KERNEL                最常使用,可阻塞。睡眠安全时用于进程上下文(没有锁的情况)。

    5GFP_USER                常规分配方式,可阻塞,用于为用户空间分配内存。

    6GFP_HIGHUSER        ZONE_HIGHMEM分配可阻塞,用于为用户空间分配内存。

    7GFP_DMA                ZONE_DMA分配内存

    6 vmallc()

    分配内存,虚拟地址连续,物理地址不一定连续(类似malloc)。

    vmalloc()为了把物理上不连续的页转换为虚拟地址连续的页,需要专门建立页表项。

    通过vmalloc()获得的页必须一个一个进行映射,导致TLB抖动增大,因此仅在不得已情况下调用,且可能睡眠,不能用于中断上下文。

    7 slab分配器

    slab分配器把不同对象划分为不同的高速缓存组,如进程描述符组、索引节点组、kmalloc组等等。

    高速缓存又被划分为不同的slbaslab由一个或多个物理连续的页组成(一般一页)。

    slab的状态分为:满、部分满和空。

    高速缓存的数据结构 kmem_cache_t保存着三个链表:slabs_fullslabs_partialslabs_empty

    8 slab分配器接口

    1)创建

    kmem_cache_t * kmem_cache_creat(const char* name, size_t size, size_t align, unsigned long flags,

    void (*ctor)(void*, kmem_cache_t *, unsigned long),        (构造)

    void (*dtor)(void*, kmem_cache_t *, unsigned long))        (析构)

    2)销毁

    int kmem_cache_destroy(kmem_cache_t *cachep)

    3)从高速缓冲中获取对象

    void* kmem_cache_alloc(kmem_chache_t *cachep, int flags)

    4)释放一个对象

    void kmem_cache_free(kmem_cache_t *cachep, void *objp)

    9 高端内存

    X86中,高于896MB的内存是高端内存,不能永久映射到内核空间。

    永久映射:映射一个给定的page结构体到内核地址空间

    void* kmap(struct page *page) 低端内存:返回虚拟地址;高端内存:建立永久映射再返回。

    void kunmap(struct page *page)

    临时映射(原子映射):当上下文不能睡眠时,可使用临时映射(可用于中断)

    void* kmap_atomic(struct page *page enum km_type type)

    void kunmap_atomic(void *kvaddr, enum km_type type)

    第十二章虚拟文件系统

    1 Unix文件系统

    四要素:文件、目录、索引节点(inode)、安装点(mount point

    索引结点:存储访问权限、大小、拥有者、创建时间等信息。

    超级块:存储文件系统的控制信息。

    2 VFS对象

    1)超级块对象:代表一个已安装的文件系统。

    2)索引节点对象:代表一个文件。

    3)目录项对象:代表一个目录项,是路径的一个组成部分。

    4)文件对象:代表由进程打开的文件。

    VFS将目录作为文件来处理,目录项不同于目录。

    3 超级块对象

    各种文件系统必须实现超级块,用于存储指定文件系统信息。

    超级块通常存放于磁盘的特定扇区中。

    对于sysfs等非磁盘文件系统,超级块存在内存中。

    4 索引节点对象

    索引节点对象包含内核在操作文件或目录时需要的全部信息。

    没有索引节点的文件系统,内核需要建立这些信息。

    一个索引节点代表文件系统中的一个文件(包括设备、管道等特殊文件)。

    5 目录项对象(dentry

    路径中的每个组成部分都由一个索引节点对象表示。

    目录项:每个目录项代表一个目录中的一个组成部分。

    eg /bin/vi /”“bin”“vi”是三个目录项

    目录项不保存在磁盘上,VFS根据路径现场创建。

    目录项的状态:

    1)被使用:对应一个有效的inode,且对象存在使用者;

    2)未使用:对应一个有效的inodeVFS当前未使用;

    3)负,没有对应的有效索引节点。

    目录项缓存:

    1)“被使用的”目录项链表;

    2)“最近被使用”的双向链表;

    3)散列表及散列函数。

    6 文件对象

    表示进程已打开的文件,是打开文件在内存中的表示。

    文件对象在内存中,也没有对应的磁盘数据。

    第十三章IO

    1 各种设备

    块设备:随机访问固定大小数据片的设备。

    字符设备:按字符流方式有序被访问,和块设备的区别在于随机访问。

    扇区:物理设备的最小可寻址单元(物理属性)。

    块:文件系统的最小逻辑可寻址单元。

    2 IO调度程序

    内核会针对IO操作进行合并、排序预操作。

    Linux电梯调度算法(2.4内核中的旧算法):

    1)相邻合并;

    2)驻留时间过长的请求放入队列尾部;

    3)保证扇区的磁盘访问顺序;

    4)不合适的请求插入位置被放入列队尾部。

    新的调度算法:

    1)最后期限IO调度,每个请求都有一个超时时间,读默认500ms,写默认5s

    2)预测IO调度,保持良好的读响应,同时提供良好的全局吞吐率。

    3)完全公正的排队IO调度程序(CFQ),IO进程列队采用时间片轮转。

    4)空操作IO调度程序,不进行排序,只进行合并,适用于闪存等无需“寻道”的介质。

    第十四章进程地址空间

    1 内存区域可包含的内存对象

    1)可执行文件代码的内存映射,代码段(text section);

    2)可执行文件已初始化全局变量的内存映射,数据段(data section);

    3)未初始化全局变量(bss段的零页);

    4)用于进程用户空间栈的零页的内存映射;

    5)每一个诸如C库或动态连接程序等共享库的代码段、数据段和bss段;

    6)任何内存映射文件;

    7)任何匿名的内存映射,如malloc()分配的内存;

    2 内存描述符

    1)内核用mmapmm_rb两个结构体描述内存对象。

    mmap:链表,利于简单、高效地遍历所有元素。

    mm_rb:红黑树,适合搜索指定元素。

    2)内核用内存描述符(mm_strcut)表示进程的地址空间。

    3current->mm指向当前进程的内存描述符。

    4)内核线程:内核线程没有进程地址空间,也没有相关的内存描述符。新内核线程直接使用前一个进程的内存描述符。

    3 内存区域

    vm_area_struct 结构体:描述指定地址空间内存连续区间上的一个独立内存范围。

    VMA标志:标志内存区域所包含的页面的行为和信息。

    eg. C库在物理内存中仅占用一份空间(空间不可写),不需为每个进程保存一个C库。

    4 内存区域接口

    find_vma():指定地址空间搜索第一个vm_end大于addr的内存区域。

    find_vma_prev():返回第一个小于addrVMA

    find_vma_intersection():返回第一个和指定地址区间相交的VMA

    mmap()\do_mmap():将一个新的地址区间加入到进程地址空间中。

    unsigned long do_mmap(struct file *file, unsgined long addr, unsigned long len,

    unsigned long prot, unsigned long flag, unsigned long offset)

    file:指定文件,NULL为匿名映射,否则为文件映射。

    offset:文件的偏移。

    len:长度。

    addr:可选参数,指定空闲区域的起始地址。

    prot:指定内存区域中页面的访问权限

    1PROT_READ                对应VM_READ

    2PROT_WRITE                对应VM_WRITE

    3PROT_EXEX                对应VM_EXEC

    4PROT_NONE                页不可访问

    flag:指定VMA标志

    1MAP_SHARED                映射可以被共享

    2MAP_PRIVATE        映射不可以被共享

    3MAP_FIXED                新区间必须开始于指定地址addr

    4MAP_ANONYMOUS        映射是匿名的

    5MAP_GROWSDOWN        VM_GROWSDOWN

    6MAP_DENYWRITE        VM_DENYWRITE

    7MAP_EXECUTABLE        VM_EXECUTABLE

    8MAP_LOCKED                VM_LOCKED

    9MAP_NORESERVE        不需为映射保留空间

    10MAP_POPULATE        填充页表

    11MAP_NONBLOCK        IO操作上不阻塞

    调用流程:

    mmap()->mmap2()->do_mmap()(内核系统调用)

    5 页表项

    虚拟地址——>物理地址:查询页表

    三级页表机制:

    PGD:一级页表,全局目录

    PMD:二级页表

    PTE:最后一级页表

    mm->PGD->PMD->PTE->物理地址

    TLB:缓冲虚拟地址到物理地址的信息。

    第十五章页高速缓存和页回写

    1 页高速缓存

    1Linux内核的一种主要磁盘缓存,减少对磁盘的IO操作(将磁盘数据缓存至主存中);

    2)页高速缓存缓存的是页面,所有的页IO操作都通过也高速缓存。

    2 页回写

    脏数据:页高速缓存比后台存储数据新的数据。

    脏数据被写回:

    1)空闲内存低于一个阈值,脏数据写回以释放内存。

    2)脏页在内存中驻留超过一个特定的阈值。

    pdflush内核线程:负责定时将脏数据刷入磁盘。pdflush线程可以有多个处理不同的设备列队。

    第十六章模块

    1 构建模块

    将模块放在内核外:

    makefile

    obj-m := fishing.o

    fishing-objs := fishing-main.o fishing-line.o

    make -C /kernel/source/location SUBDIRS = $PWD modules

    2 模块参数

    module_param(name, type, perm)

    name:用户可见的参数名

    type:参数类型

    permsysfs文件系统下的文件权限

    3 导出符号表

    导出的内核函数可被模块调用。

    EXPORT_SYMBOL()

    EXPORT_SYMBOL_GPL()

    第十七章 kobjectsysfs

    1 统一设备模型

    kobject:包含引用计数,父子关系和对象名称等基本对象道具,以统一方式提供这些功能。

    ktype:定义了一些kobject相关默认特性,如析构、sysfs行为等。

    kset

    1)嵌入的kobject作为kobject组的基类;

    2kset将相关的kobject集合在一起,相关的kobject以独立目录与sysfs中。

    subsystem:包含kset的集合,是sysfs的根目录映射。

    2 kobject使用方法

    初始化:kobject_init(struct kobject *kobj)

    设置名称:kobject_set_name(struct kobject *kobj, const char* fmt,...)

    增加引用计数:struct kobject* kobjec_get(struct kobject *kobj)

    减少引用计数:void kobject_put(struct kobject *kobj)

    3 sysfs

    kobject导入sysfs

    int kobject_add(struct kobject *kobj)

    kobject_register()函数包含kobject_init()kobject_add()两个函数的功能。

    删除一个kobject对应文件目录:

    void kobject_del(struct kobject *kobj)

    kobject_unregister包含kobject_del()kobject_put()功能

    创建新属性:

    int sysfs_create_file(struct kobject *kobj, const struct attribute *attr)

    创建符号链接:

    int sysfs_create_link(struct kobject *kobj, struct kobject *target, char* name)

    删除新属性:

    void sysfs_remove_file(struct kobject *kobj, const struct attribute *attr)

    void sysfs_remove_link(struct kobject *kobj, char* name)

    第十八章调试

    1 printk

    ★可在进程、中断上下文中调用printk

    ★可在持有锁时调用printk

    ★可在SMP上调用printk

    记录等级:

    0        KERN_EMERG        紧急情况

    1        KERN_ALERT        需立即注意的错误

    2        KERN_CRIT        临界情况

    3        KERN_ERR        错误

    4        KERN_WARNING        警告

    5        KERN_NOTICE        普通,可能需要注意

    6        KERN_INFO        非正式信息

    7        KERN_DEBUG        调试信息

    默认的等级是KERN_WARNING

    记录缓冲区(LOG_BUF_LEN):

    CONFIG_LOG_BUF_SHIFT 可调整缓冲区大小

    守护进程:

    klogd,从记录缓冲区中获取内核消息。

    syslogd,将消息保存于内核日志中。

    kallsysm

    CONFIG_KALLSYMS 可载入内核镜像对应的内存地址符号名称,使内核稍大,但便于调试。

    2 引发BUG

    可以打印oops信息的函数:

    BUG()

    BUG_ON(bad_thing)

    打印oops并挂起整个系统:

    panic()

    回朔栈信息:

    dump_stack()

    3 GDB

    1gdb vmlinux /proc/kcore

    2p global_variable 打印一个变量

    3dissable function 反汇编一个函数

    4 时间重复限制

    static unsigned long pre_jiffy = jiffies;

    if(time_after(jiffies, prev_jiffy + 2*Hz)){

    prev_jiffy = jiffies;

    printk("XXXX");

    }

    5 次数重复限制

    static unsigned long limit = 0;

    if(linux < 5) {

    limit++;

    printk("xxx");

    }

    第十九章可移植性

    1 字长规则

    1ANSI C标准规定,一个char长度是8位;

    2Linux当前体系中,int都是32位的;

    3Linux当前系统中,short都是16位的;

    4long的长度可能是32位或64位;

    5)不应该假设 sizeof(int) == sizeof(long)

    6)不应该假设指针和int长度相等。

    2 不透明类型

    pid_tatomic_t ——> int

    dev_tgid_tuid_t

    1)不要假设该类型长度;

    2)不要将该类型转化为C标准类型;

    3)编程时要保证类型实际存储空间和格式发生变化后代码不受影响。

    第二十章其他

    1 Linux的编程风格

    1)缩进:用制表符(tab)每次缩进8个字符;

    2)括号:左括号紧跟在语句的最后,与语句同一行。右括号新起一行;

    3)每行代码长度:尽可能保证每行代码不超过80个字符;

    4)命名规范:不允许混合大小写;

    5)函数:代码应不超过两屏,局部变量不超过十个。

    6)注释:bug以“FIXME”开头;

    7)尽量少用typedef

    8)在源码中尽量不要用ifdef

    2 创建补丁

    1)创建整个内核patch

    diff -urN linux-origin/ linux-change/ > my_patch

    2)创建指定文件的patch

    diff -u linux-origin/some/file linux-change/some/file > my_patch_file

    3)在自己代码目录下执行patch

    patch -p1 < ../my_patch

    附录A:链表

    1 链表结构体

    struct list_head{

    struct list_head *next;

    struct list_head *prev;

    };

    Linux的链表可以从任何一个节点开始遍历,因此每个独立节点都可看做是链表头。

    单独的list_head无意义,需要嵌入自己的结构体中。

    eg.         struct my_struct{

    struct list_head list;

    unsigned long dong;

    void *cat;

    };

    初始化:

    eg.        struct my_struct *p;

    p->dog = 0;

    p->cat = NULL:

    INIT_LIST_HEAD(&p->list);

    静态创建:

    struct my_struct mine = {

    .list = LIST_HEAD_INIT(mine.list);        //INIT_LIST_HEAD

    .dog = 0;

    .cat = NULL;

    };

    直接声明和初始化静态链表:

    static LIST_HEAD(fox);

    2 操作链表<linux/list.h>

    1)增加节点(head后插入)

    list_add(struct list_head *new, struct list_head *head)

    2)把节点增加到链表尾(head节点前插入)

    list_add_tail(struct list_head *new, struct list_head *head)

    3)从链表删除一个节点

    list_del(struct list_head *entry)

    不会释放entry和占用内存。

    4)删除一个节点并对其重新初始化

    list_del_init(struct list_head *entry)

    再次初始化entry

    5)把节点从一个链表移到另一个链表(head后)

    list_move(struct list_head *list, struct list_head *head)

    6)把节点从一个链表移到另一个链表末尾

    list_move_tail(struct list_head *list, struct list_head *head)

    7)检查链表是否为空

    list_empty(struct list_head *head)

    8)合并两链表(list插入head后)

    list_splice(struct list_head *list, struct list_head *head)

    9)合并,并重新初始化list指向的链表

    list_splice_init(struct list_head *list, struct list_head *head)

    10)遍历链表

    list_for_each()

    eg.        struct list_head *p;

    list_for_each(p, list){

    //p指向某元素

    }

    11)取得包含list_head的结构体

    list_entry()

    参数:

    1)指向给定链表元素的指针;

    2)嵌入了链表结构体类型的引用;

    3)结构体中链表成员的名称。

    eg.        struct list_head *p;

    struct my_struct *my;

    list_for_each(p, &mine->list)

    {

    my = list_entry(p, struct my_struct, list);

    }

  • 相关阅读:
    Tampermonkey 油猴脚本开发 入门
    k8s 上安装 lamp 环境
    centOS 7.9 k8s 安装 和 基本命令
    力扣59-螺旋矩阵 II
    力扣54-螺旋矩阵
    力扣705-设计哈希集合
    力扣706-设计哈希映射
    scrapy参数-COOKIES_ENABLED
    优雅降级、渐进增强
    e-cahr的地图组件封装(浙江省为例)
  • 原文地址:https://www.cnblogs.com/sinaxyz/p/2648038.html
Copyright © 2011-2022 走看看