linux 相关知识

用户级程序主动发起

  1. 标准的系统调用 和自己定义的系统调用

  2. 编写驱动程序
    用户程序通过 /dev/目录下 open() —— read() —— write() —— ioctl() —— close()
    实质上ioctl, read, write本质上讲也是通过系统调用去完成的,只是这些调用已被内核进行了标准封装,统一定义.

3.使用proc 文件系统
它以文件系统作为使用界面,使应用程序可以以文件操作的方式安全、方便的获取系统当前运行的状态和其它一些内核数据信息.
proc文件系统多用于监视、管理和调试系统,我们使用的很多管理工具如ps,top等,都是利用proc来读取内核信息的
通过修改proc文件系统下的系统参数配置文件(/proc/sys),我们可以直接在运行时动态更改内核参数 echo 1 > /proc/sys/net/ip_v4/ip_forward

proc还为我们留有接口,允许我们在内核中创建新的条目从而与用户程序共享信息数据

  1. 使用虚拟文件系统
    libfs的例程序帮助不熟悉文件系统的用户封装了实现VFS的通用操作

  2. 使用内存映像
    Linux通过内存映像机制来提供用户程序对内存直接访问的能力。内存映像的意思是把内核中特定部分的内存空间映射到用户级程序的内存空间去。也就是说,用户空间和内核空间共享一块相同的内存.如X服务器需要对视频内存进行大量的数据交换。mmap完全是基于共享内存的观念了,也正因为此,它能提供额外的便利

内核主动发起的信息交互
1.从内核空间调用用户程序
比如modprobe程序帮助完成——最简单的情形是modprobe用内核传过来的模块名字作为参数调用insmod
2. 利用brk系统调用来导出内核数据
3. 使用信号
信号在内核里的用途主要集中在通知用户程序出现重大错误,强行杀死当前进程,这时内核通过发送SIGKILL信号通知进程终止,内核发送信号使用send_sign(pid,sig)例程,可以看到信号发送必须要事先知道进程序号(pid),
4. netlink 双向通知

本文由该问题引入到内核锁的讨论,归纳如下

为什么需要内核锁?

多核处理器下,会存在多个进程处于内核态的情况,而在内核态下,进程是可以访问所有内核数据的,因此要对共享数据进行保护,即互斥处理

有哪些内核锁机制?

(1)原子操作

atomic_t数据类型,atomic_inc(atomic_t *v)将v加1

原子操作比普通操作效率要低,因此必要时才使用,且不能与普通操作混合使用

如果是单核处理器,则原子操作与普通操作相同

(2)自旋锁

spinlock_t数据类型,spin_lock(&lock)和spin_unlock(&lock)是加锁和解锁

等待解锁的进程将反复检查锁是否释放,而不会进入睡眠状态(忙等待),所以常用于短期保护某段代码

同时,持有自旋锁的进程也不允许睡眠,不然会造成死锁——因为睡眠可能造成持有锁的进程被重新调度,而再次申请自己已持有的锁.

Question1. 在获得自旋锁后为什么不能够进行可能引起休眠的操作?举一例子spin_lock带来的死锁
spin lock的自动禁止抢占的
也就是说,A如果那到锁以后,内核的抢占暂时被禁止。然后它休眠了,切换到另一个进程B(注意,这不是抢占,是进程自己放弃CPU)。等到进程B想要获得这个锁时发生了死锁,尽管B的时间片会被用完,但由于内核抢占被禁止了,所以B不会被调度出去。更糟的情况是,如果A用spin_lock_irq方式来得到这个spin lock,那中断是被禁止的,时钟中断不会被响应,B的时间片根本不会被跟新。
这个例子前提是A和B发生在同一核上面。

Question2 举例spin_lock带来的死锁
进程A中调用了spin_lock(&lock)然后进入临界区,此时来了一个中断(interrupt),
该中断也运行在和进程A相同的CPU上,并且在该中断处理程序中恰巧也会spin_lock(&lock)
试图获取同一个锁。由于是在同一个CPU上被中断,进程A会被设置为TASK_INTERRUPT状态,
中断处理程序无法获得锁,会不停的忙等,由于进程A被设置为中断状态,schedule()进程调度就
无法再调度进程A运行,这样就导致了死锁!
但是如果该中断处理程序运行在不同的CPU上就不会触发死锁。 因为在不同的CPU上出现中断不会导致
进程A的状态被设为TASK_INTERRUPT,只是换出。当中断处理程序忙等被换出后,进程A还是有机会
获得CPU,执行并退出临界区。
所以在使用spin_lock时要明确知道该锁不会在中断处理程序中使用
所以在任何情况下使用spin_lock_irq都是安全的

为什么用 spin_lock_irqsave ?
spin_lock_irqsave在进入临界区前,保存当前中断寄存器flag状态,关中断,进入临界区,在退出临界区时,把保存的中断状态写回到中断寄存器。
spin_lock_irqsave锁返回时,中断状态不会被改变,调用spin_lock_irqsave前是开中断返回就开中断。
spin_lock_irq锁返回时,永远都是开中断,即使spin_lock_irq前是关中断
可以在make menuconfig里面配置kernel

  1. 在单cpu,不可抢占内核中,自旋锁为空操作。
  2. 在单cpu,可抢占内核中,自旋锁实现为“禁止内核抢占”,并不实现“自旋”。(注意)
  3. 在多cpu,可抢占内核中,自旋锁实现为“禁止内核抢占” + “自旋”。

(3)信号量与互斥量

struct semaphore数据类型,down(struct semaphore * sem)和up(struct semaphore * sem)是占用和释放

struct mutex数据类型,mutex_lock(struct mutex *lock)和mutex_unlock(struct mutex *lock)是加锁和解锁

竞争信号量与互斥量时需要进行进程睡眠和唤醒,代价较高,所以不适于短期代码保护,适用于保护较长的临界区

互斥量与信号量的区别?

(1)互斥量用于线程的互斥,信号线用于线程的同步

这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别

互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的

同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源

(2)互斥量值只能为0/1,信号量值可以为非负整数

也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问

(3)互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到

GCC
1.
GNU GCC的基本功能包括:输出预处理后的C/C++源程序(展开头文件和替换宏)
输出C/C++源程序的汇编代码
输出二进制目标文件
生成静态库
生成可执行程序
转换文件格式

交叉编译器arm-elf-gcc GCC是编译的前端程序,编译成汇编代码,arm-elf-gcc根据输入文件的后缀来确定文件的类型,然后根据用户的编译选项(包括优化选项、调试信息选项等)将其编译成相应的汇编临时文件(后缀为.s);

汇编器 arm-elf-as arm-elf-as将汇编语言程序转换为ELF (Executable and Linking Format,执行时链接文件格式)格式的可重定位目标代码,这些目标代码同其它目标模块或函数库易于定位和链接,arm-elf-as将该汇编文件编译成目标文件(后缀为.o);

连接器arm-elf-ld arm-elf-ld根据链接定位文件Linkcmds中的代码区、数据区、BSS区和栈区等定位信息,将可重定位的目标模块链接成一个单一的、绝对定位的目标程序

库管理器arm-elf-ar arm-elf-ar将多个可重定位的目标模块归档为一个函数库文件

Linux设备模型

Kobject,kset,kypte这三个结构是设备模型中的下层架构。在sysfs中每一个目录都对应一个kobject.这些kobject都有自己的parent 因为大部份的同类设备都有相同的属性,因此将这个属性隔离开来,存放在ktype中
对于sysfs中的普通文件读写操作都是由kobject->ktype->sysfs_ops来完成的。

注册一个kobject不会产生事件,只有注册kset才会。

struct kset_uevent_ops uevent_ops =
{
.filter = kset_filter,
.name = kset_name,
.uevent = kset_uevent,
};
int kset_uevent(struct kset *kset, struct kobject *kobj,
struct kobj_uevent_env *env)
{
int i = 0;
printk("UEVENT: uevent. kobj %s.\n",kobj->name);

    while( i< env->envp_idx){
            printk("%s.\n",env->envp[i]);
            i++;
    }

    return 0;

}

kobject.kset,ktype.这三个结构联合起来一起构成了整个设备模型的基石.而bus.device.device_driver.则是基于kobject.kset.ktype之上的架构.在这里,总线,设备,驱动被有序的组和在一起

Platform总线是kernel中最近加入的一种虚拟总线
/sys/devices/platform下面.创建了一个名为“platform”的总线

在分析linux设备模型的时候,曾说过.调用device_add()会产生一个hotplug事件。

platform_driver_register()将驱动注册到总线上

这样,总线上有设备,又有驱动,就会进行设备与匹配的过程,调用的相应接口为:
bus ->match --- > bus->probe/driver->probe

系统调用的参数传递方式
系统调用是可以传递参数的.例如:int open(const char *pathname, int flags),那这些参数是如何传递的呢?系统调用采用寄存器来传值,这样,进入内核空间之后,取值非常方便.这几个寄存器依次是:ebx,ecx,edx,esi,edi,ebp.如果参数个数超过了6个,或者参数的大小大于32位,可以用传递参数地址的方法.陷入到内核空间之后,再从地址中去取值

中断处理程序
关中断时不能睡眠,睡眠就会睡死

在驱动程序中,通常使用request_irq()来注册中断处理程序
为了提高中断的响应速度,很多操作系统都把中断分成了两个部份,上半部份与下半部份.上半部份通常是响应中断,并把中断所得到的数据保存进下半部.耗时的操作一般都会留到下半部去处理.
接下来,我们看一下软中断的处理模型:

软中断
1.DECLARE_TASKLET(tasklet_test,tasklet_test_handle,0);
2.网络协议栈里专用软中断

同步

同步是linux内核中一种很重要的操作.它为内核提供了一种临界区和SMP系统中的数据保护机制
一:原子操作
void atomic_set(atomic_t *v, int i)
int atomic_read(atomic_t *v)
void clear_bit(nr, void *addr)
void change_bit(nr, void *addr)

二 信号量
在单核和多核上都有效,组成为一个整数变量,一个等待进程列表,两个方法up down.

三:自旋锁

内核还规定持有自旋锁的进程不能被抢占,这样做主要是出于系统性能考虑.如果一个持自旋锁的进程被调度出CPU.那其它需要这些锁的进程被调度进来之后,也只做 “自旋”工作.

    信号量/互斥体允许进程睡眠属于睡眠锁,自旋锁则不允许调用者睡眠,而是让其循环等待,所以有以下区别应用   

1)、信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因而自旋锁适合于保持时间非常短的情况  
2)、自旋锁可以用于中断,不能用于进程上下文(会引起死锁)。而信号量不允许使用在中断中,而可以用于进程上下文  
3)、自旋锁保持期间是抢占失效的,自旋锁被持有时,内核不能被抢占,而信号量和读写信号量保持期间是可以被抢占的  

另外需要注意的是
1)、信号量锁保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区,因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一进程企图获取本自旋锁,死锁就会发生。
2)、在你占用信号量的同时不能占用自旋锁,因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。

四 完成变量 补充原语 completion

在内核中经常有这样的需要,在一个进程中要等待某个进程完成某个事情.内核为其提供了一个简单的接口,其实它只是一个简化版的信号量而已。

Question 为什么要设计这样的信号量? 用MUTEX 不行吗?
在多核情况下,虽然信号量可以用于实现同步,但往往可能会出现一些不好的结果。例如:当进程A分配了一个临时信号量变量,把它初始化为关闭的MUTEX,并把其地址传递给进程B,然后在A之上调用down(),进程A打算一旦被唤醒就撤销给信号量。随后,运行在不同CPU上的进程B在同一个信号量上调用up()。然而,up()和down()的目前实现还允许这两个函数在同一个信号量上并发。因此,进程A可以被唤醒并撤销临时信号量,而进程B还在运行up()函数。结果up()可能试图访问一个不存在的数据结构。这样就会出现错误。为了防止发生这种错误就专门设计了completion机制专门用于同步。

避免上述方式出现问题在多核情况下,专门设计了completion用于同步。和semaphore的本质区别是如何使用等待队列中的自旋锁,complete 函数确保complete()和wait_for_completion()不会并发执行,信号量里面的自旋锁是保护并发down函数中的信号量数据结构。

wait_for_completion():等待条件的完成,

MUTEX初始化为1 为打开,MUTEX初始化为0 为关闭

信号量和互斥体之间的区别

概念上的区别:

  信号量:是进程间(线程间)同步用的,一个进程(线程)完成了某一个动作就通过信号量告诉别的进程(线程),别的进程(线程)再进行某些动作。有二值和多值信号量之分。  

 互斥锁:是线程间互斥用的,一个线程占用了某一个共享资源,那么别的线程就无法访问,直到这个线程离开,其他的线程才开始可以使用这个共享资源。可以把互斥锁看成二值信号量。    

上锁时:

 信号量: 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait阻塞,直到sem_post释放后value值加一。一句话,信号量的value>=0。  

 互斥锁: 只要被锁住,其他任何线程都不可以访问被保护的资源。如果没有锁,获得资源成功,否则进行阻塞等待资源可用。一句话,线程互斥锁的vlaue可以为负数。    

使用场所:

 信号量主要适用于进程间通信,当然,也可用于线程间通信。而互斥锁只能用于线程间通信。

抢占

  1. Linux内核(linux-2.6以前) 是不可抢占的,但支持用户抢占它的调度方法是:一个进程可以通过schedule()函数自愿地启动一次调度。非自愿的强制性调度只能发生在每次从系统调用返回的前夕以及每次从中断或异常处理返回到用户空间的前夕(这种强制性调度又叫用户抢占)。
    简而言之,用户抢占在以下情况时产生:

  从系统调返回用户空间。

  从中断处理程序返回用户空间。

会导致 优先级反转

在Linux中,在核心态运行的任何操作都要优先于用户态进程,这就有可能导致优先级反转问题的出现 例如,一个低优先级的用户进程由于执行软/硬中断等原因而导致一个高优先级的任务得不到及时响应

  1. Linux内核(linux-2.6) 加入了内核抢占(preempt)机制。内核抢占指用户程序在执行系统调用期间可以被抢占,该进程暂时挂起,使新唤醒的高优先级进程能够运行
    这种抢占并非可以在内核中任意位置都能安全进行,比如在临界区中的代码就不能发生抢占。临界区是指同一时间内不可以有超过一个进程在其中执行的指令序列。在Linux内核中这些部分需要用自旋锁保护。

Question 那些情况内核不能被抢占 ?
禁止内核抢占的情况列出如下:

(1)内核执行中断处理例程时不允许内核抢占,中断返回时再执行内核抢占。

(2)当内核执行软中断或tasklet时,禁止内核抢占,软中断返回时再执行内核抢占。

(3)在临界区禁止内核抢占,临界区保护函数通过抢占计数宏控制抢占,计数大于0,表示禁止内核抢占。

内核的代码段正持有spinlock自旋锁、writelock/readlock读写锁等锁,处干这些锁的保护状态中。内核中的这些锁是为了在SMP系统中短时间内保证不同CPU上运行的进程并发执行的正确性。当持有这些锁时,内核不应该被抢占,否则由于抢占将导致其他CPU长期不能获得锁而死等。

3.Question 内核抢占的时间点是什么?
<1>.中断返回时再执行内核抢占
<2>.软中断返回,如系统调用
而且必须打开系统中断才能完成内核抢占

从用户空间到内核空间有两种触发手段
用户空间的应用程序,通过系统调用,进入内核空间
硬件通过触发信号,导致内核调用中断处理程序,进入内核空间

linux引导分析

第一阶段所用的boot loader被装载到RAM中并被执行,位于MBR中的主boot loader是一个512字节的镜像,这里的boot loader在大小上小于一个扇区的大小,也就是512字节
第二阶段boot loader位于内存中并被执行,当系统镜像被加载时,第二阶段的boot loader将把控制权转交给内核镜像
与此同时,内核开始自解压并初始化。在这个阶段,第二阶段的boot loader会检查系统的硬件,枚举那些附加的硬件设备,挂载根设备,之后加载需要的内核模块。完成之后,第一个用户空间程序(init)开始执行,更高层次的系统初始化开始

通过调用 start_kernel,会调用一系列初始化函数来设置中断,执行进一步的内存配置,并加载初始 RAM 磁盘。最后,要调用 kernel_thread(在 arch/i386/kernel/process.c 中)来启动 init 函数,这是第一个用户空间进程(user-space process)。最后,启动空任务,现在调度器就可以接管控制权了(在调用 cpu_idle 之后)。通过启用中断,抢占式的调度器就可以周期性地接管控制权,从而提供多任务处理能力。
在内核引导过程中,初始 RAM 磁盘(initrd)是由阶段 2 引导加载程序加载到内存中的,它会被复制到 RAM 中并挂载到系统上。这个 initrd 会作为 RAM 中的临时根文件系统使用,并允许内核在没有挂载任何物理磁盘的情况下完整地实现引导。由于与外围设备进行交互所需要的模块可能是 initrd 的一部分,因此内核可以非常小,但是仍然需要支持大量可能的硬件配置。在内核引导之后,就可以正式装备根文件系统了(通过 pivot_root):此时会将 initrd 根文件系统卸载掉,并挂载真正的根文件系统。
initrd 函数让我们可以创建一个小型的 Linux 内核,其中包括作为可加载模块编译的驱动程序。这些可加载的模块为内核提供了访问磁盘和磁盘上的文件系统的方法,并为其他硬件提供了驱动程序。由于根文件系统是磁盘上的一个文件系统,因此 initrd 函数会提供一种启动方法来获得对磁盘的访问,并挂载真正的根文件系统。在一个没有硬盘的嵌入式环境中,initrd 可以是最终的根文件系统,或者也可以通过网络文件系统(NFS)来挂载最终的根文件系统。
初始化系统 /sbin/init进程

Question 什么时候后不用考虑同步问题 ?
约束
1.中断处理程序,软中断和tasklet既不可以被抢占也不能被阻塞,最坏是被中断嵌套
2.软中断不和tasklet不能在一个给定的CPU上交错执行 ??、
3.同一个takslet不可能同时在几个CPU上执行
所以
1.中断处理程序和tasklet不必编写成可重入
2.仅被软中断和tasklet访问的每CPU变量(用于CPU之间复制数据结构)不需要同步
3.仅被一种takslet访问的数据结构不需要同步

中断中能不能睡眠?
中断处于中断上下文,而可以调度的进程处于进程上下文,在中断上下文中不能调度

posted @ 2015-09-22 14:36  fastwave2004  阅读(206)  评论(0编辑  收藏  举报