Linux驱动开发-第二章-Linux内核和内核编程

Linux内核和内核编程

内核的组成

Linux内核主要由进程调度(SCHED)、内存管理(MM)、虚拟文件系统(VFS)、网络接口(NET)和进程间通信(IPC)5个子系统组成:

]

1、进程调度
  • 进程调度控制系统中的多个进程对CPU的访问,使得多个进程能在CPU中“微观串行,宏观并行”地执行。进程调度处于系统的中心位置,内核中其他的子系统都依赖它,因为每个子系统都需要挂起或恢复进程。

  • 如下图所示,Linux的进程在几个状态间进行切换。在设备驱动编程中,当请求的资源不能得到满足时,驱动一般会调度其他进程执行,并使本进程进入睡眠状态,直到它请求的资源被释放,才会被唤醒而进入就绪状态。睡眠分成可中断的睡眠和不可中断的睡眠,两者的区别在于可中断的睡眠在收到信号的时候会醒。

在这里插入图片描述

  • 完全处于TASK_UNINTERRUPTIBLE状态的进程甚至都无法被“杀死”,所以Linux 2.6.26之后的内核也存在一种TASK_KILLABLE的状态,它等于“TASK_WAKEKILL|TASK_UNINTERRUPTIBLE”,可以响应致命信号。在Linux内核中,使用task_struct结构体来描述进程,该结构体中包含描述该进程内存资源、文件系统资源、文件资源、tty资源、信号处理等的指针。Linux的线程采用轻量级进程模型来实现,在用户空间通过pthread_create()API创建线程的时候,本质上内核只是创建了一个新的task_struct,并将新task_struct的所有资源指针都指向创建它的那个task_struct的资源指针。

  • 绝大多数进程(以及进程中的多个线程)是由用户空间的应用创建的,当它们存在底层资源和硬件访问的需求时,会通过系统调用进入内核空间。有时候,在内核编程中,如果需要几个并发执行的任务,可以启动内核线程,这些线程没有用户空间。启动内核线程的函数为:

    pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);
    
2、内存管理
  • 内存管理的主要作用是控制多个进程安全地共享主内存区域。当CPU提供内存管理单元(MMU)时,Linux内存管理对于每个进程完成从虚拟内存到物理内存的转换。Linux 2.6引入了对无MMU CPU的支持。

  • 如图下图所示,一般而言,32位处理器的Linux的每个进程享有4GB的内存空间,03GB属于用户空间,34GB属于内核空间,内核空间对常规内存、I/O设备内存以及高端内存有不同的处理方式。当然,内核空间和用户空间的具体界限是可以调整的,在内核配置选项Kernel Features→Memory split下,可以设置界限为2GB或者3GB。

在这里插入图片描述

  • 如下图所示,Linux内核的内存管理总体比较庞大,包含底层的Buddy算法,它用于管理每个页的占用情况,内核空间的slab以及用户空间的C库的二次管理。另外,内核也提供了页缓存的支持,用内存来缓存磁盘,per-BDI flusher线程用于刷回脏的页缓存到磁盘。Kswapd(交换进程)则是Linux中用于页面回收(包括file-backed的页和匿名页)的内核线程,它采用最近最少使用(LRU)算法进行内存回收。

在这里插入图片描述

3、虚拟文件系统
  • 如下图所示,Linux虚拟文件系统隐藏了各种硬件的具体细节,为所有设备提供了统一的接口。而且,它独立于各个具体的文件系统,是对各种文件系统的一个抽象。它为上层的应用程序提供了统一的vfs_read()、vfs_write()等接口,并调用具体底层文件系统或者设备驱动中实现的file_operations结构体的成员函数。

在这里插入图片描述

4、网络接口
  • 网络接口提供了对各种网络标准的存取和各种网络硬件的支持。如下图所示,在Linux中网络接口可分为网络协议和网络驱动程序,网络协议部分负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备通信,每一种可能的硬件设备都有相应的设备驱动程序。

在这里插入图片描述

5、进程间通信
  • 进程间通信支持进程之间的通信,Linux支持进程间的多种通信机制,包含信号量、共享内存、消息队列、管道、UNIX域套接字等,这些机制可协助多个进程、多资源的互斥访问、进程间的同步和消息传递。在实际的Linux应用中,人们更多地趋向于使用UNIX域套接字,而不是System V IPC中的消息队列等机制。Android内核则新增了Binder进程间通信方式。
总结、Linux内核组成部分之间的依赖关系

·进程调度与内存管理之间的关系:这两个子系统互相依赖。在多程序环境下,程序要运行,则必须为之创建进程,而创建进程的第一件事情,就是将程序和数据装入内存。

·进程间通信与内存管理的关系:进程间通信子系统要依赖内存管理支持共享内存通信机制,这种机制允许两个进程除了拥有自己的私有空间之外,还可以存取共同的内存区域。

·虚拟文件系统与网络接口之间的关系:虚拟文件系统利用网络接口支持网络文件系统(NFS),也利用内存管理支持RAMDISK设备。

·内存管理与虚拟文件系统之间的关系:内存管理利用虚拟文件系统支持交换,交换进程定期由调度程序调度,这也是内存管理依赖于进程调度的原因。当一个进程存取的内存映射被换出时,内存管理向虚拟文件系统发出请求,同时,挂起当前正在运行的进程。除了这些依赖关系外,内核中的所有子系统还要依赖于一些共同的资源。这些资源包括所有子系统都用到的API,如分配和释放内存空间的函数、输出警告或错误消息的函数及系统提供的调试接口等。

内核空间与用户空间概述

  • 现代CPU内部往往实现了不同操作模式(级别),不同模式有不同功能,高层程序往往不能访问低级功能,而必须以某种方式切换到低级模式,存在用户空间和内核空间的区分。

  • ARM处理器的用户(usr)模式是大多数应用程序运行的模式,当处理器运行在用户模式下时,某些被保护的系统资源是不能访问的,而管理模式(svc)是操作系统使用的保护模式。ARM Linux的系统调用实现原理是采用swi软中断从用户(usr)模式陷入管理模式(svc)。

  • x86处理器包含4个不同的特权级,称为Ring 0~Ring 3。在Ring0下,可以执行特权级指令,对任何I/O设备都有访问权等,而Ring3则被限制很多操作。Linux系统可充分利用CPU的这一硬件特性,但它只使用了两级。在Linux系统中,内核可进行任何操作,而应用程序则被禁止对硬件的直接访问和对内存的未授权访问。例如,若使用x86处理器,则用户代码运行在特权级3,而系统内核代码则运行在特权级0。

  • 内核空间和用户空间这两个名词用来区分程序执行的两种不同状态,它们使用不同的地址空间。Linux只能通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。

C编程特性

1、零长度和变量长度数组

GNU C允许使用零长度数组,在定义变长对象的头结构时,这个特性非常有用。例如:

struct var_data {
    int len;
    char data[0];
};

char data[0]仅仅意味着程序中通过var_data结构体实例的data[index]成员可以访问len之后的第index个地址,它并没有为data[]数组分配内存,因此sizeof(struct var_data)=sizeof(int)。假设struct var_data的数据域就保存在struct var_data紧接着的内存区域中,则通过如下代码可以遍历这些数据:

struct var_data s;
...
for (i = 0; i < s.len; i++)
printf("%02x", s.data[i]);

GNU C中也可以使用1个变量定义数组,例如如下代码中定义的“double x[n]”:

int main (int argc, char *argv[])
{
    int i, n = argc;
    double x[n];
    for (i = 0; i < n; i++)
    x[i] = i;
    return 0;
}
2、case范围

GNU C支持case x…y这样的语法,区间[x,y]中的数都会满足这个case的条件,请看下面的代码:

switch (ch) {
case '0'... '9': c -= '0';
	break;
}

代码中的case’0’…'9’等价于标准C中的:
case ‘0’: case ‘1’: case ‘2’: case ‘3’: case ‘4’:case ‘5’: case ‘6’: case ‘7’: case ‘8’: case ‘9’:

3、语句表达式

GNU C把包含在括号中的复合语句看成是一个表达式,称为语句表达式,它可以出现在任何允许表达式的地方。我们可以在语句表达式中使用原本只能在复合语句中使用的循环、局部变量等,例如:

#define min_t(type,x,y) \
( { type _ _x =(x);type _ _y = (y); _ _x<_ _y
int ia, ib, mini;
float fa, fb, minf;
mini = min_t(int, ia, ib);
minf = min_t(float, fa, fb);

因为重新定义了__xx__y这两个局部变量,所以用上述方式定义的宏将不会有副作用。在标准C中,对应的如下宏则会产生副作用

#define min(x,y) ((x) < (y) (x) : (y))

代码min(++ia,++ib)会展开为((++ia)<(++ib)(++ia):(++ib)),传入宏的“参数”增加两次。

4、typeof关键字

typeof(x)语句可以获得x的类型,因此,可以借助typeof重新定义min这个宏:

#define min(x,y) ({ \
const typeof(x) _x = (x); \
const typeof(y) _y = (y); \
(void) (&_x == &_y); \
_x < _y _x : _y; })
5、可变参数宏

而在GNU C中,宏也可以接受可变数目的参数,例如:

#define pr_debug(fmt,arg...) \
printk(fmt,##arg)

使用“##”是为了处理arg不代表任何参数的情况,这时候,前面的逗号就变得多余了。使用“##”之后,GNU C预处理器会丢弃前面的逗号。

6、标号元素

标准C要求数组或结构体的初始化值必须以固定的顺序出现,在GNU C中,通过指定索引或结构体成员名,允许初始化值以任意顺序出现。指定数组索引的方法是在初始化值前添加“[INDEX]=”,当然也可以用“[FIRST…LAST]=”的形式指定一个范围。例如,下面的代码定义了一个数组,并把其中的所有元素赋值为0

unsigned char data[MAX] = { [0 ... MAX-1] = 0 };

下面的代码借助结构体成员名初始化结构体:

struct file_operations ext2_file_operations = {
    llseek: generic_file_llseek,
    read: generic_file_read,
    write: generic_file_write,
    ioctl: ext2_ioctl,
    mmap: generic_file_mmap,
    open: generic_file_open,
    release: ext2_release_file,
    fsync: ext2_sync_file,
};

但是,Linux 2.6推荐类似的代码应该尽量采用标准C的方式。

7、特殊属性声明

GNU C允许声明函数、变量和类型的特殊属性,以便手动优化代码和定制代码检查的方法。要指定一个声明的属性,只需要在声明后添加__attribute__((ATTRIBUTE))。其中ATTRIBUTE为属性说明,如果存在多个属性,则以逗号分隔。GNU C支持noreturn、format、section、aligned、packed等十多个属性。

  • noreturn作用于函数,表示函数永不返回,促使编译器优化代码,例如:
# define ATTRIB_NORET __attribute__((noreturn)) ....
asmlinkage NORET_TYPE void do_exit(long error_code) ATTRIB_NORET;
  • format属性也用于函数,表示该函数使用printf、scanf或strftime风格的参数,指定format属性可以让编译器根据格式串检查参数类型。例如
asmlinkage int printk(const char * fmt, ...) __attribute__ ((format (printf, 1, 2)));
  • unused属性作用于函数和变量,表示该函数或变量可能不会用到,这个属性可以避免编译器产生警告信息。
asmlinkage int unused_param_func(int __attribute__ ((unused)) param); 
  • aligned属性用于变量、结构体或联合体,指定变量、结构体或联合体的对齐方式,以字节为单位,例如:
struct example_struct {
    char a;
    int b;
    long c;
} __attribute__((aligned(4)));

表示该结构类型的变量以4字节对齐。

  • packed属性作用于变量和类型,用于变量或结构体成员时表示使用最小可能的对齐,用于枚举、结构体或联合体类型时表示该类型使用最小的内存。例如:
struct example_struct {
    char a;
    int b;
    long c __attribute__((packed));
};

编译器对结构体成员及变量对齐的目的是为了更快地访问结构体成员及变量占据的内存。例如,对于一个32位的整型变量,若以4字节方式存放(即低两位地址为00),则CPU在一个总线周期内就可以读取32位;否则,CPU需要两个总线周期才能读取32位。

8、内建函数
  • GNU C提供了大量内建函数,其中大部分是标准C库函数的GNU C编译器内建版本,例如memcpy()等,它们与对应的标准C库函数功能相同。

    ·用来处理变长参数列表;

    ·用来处理程序运行异常;

    ·程序的编译优化、性能优化;

    ·查看函数运行中的底层信息、堆栈信息等;

    ·C 标准库函数的内建版本。

  • 不属于库函数的其他内建函数的命名通常以__builtin开始,如下所示。
    ·内建函数 __builtin_return_address(LEVEL)返回当前函数或其调用者的返回地址,参数LEVEL指定调用栈的级数,如0表示当前函数的返回地址,1表示当前函数的调用者的返回地址。

    void func(void)
    {
        int *p;
        p = __builtin_frame_address(0);	 // 获取当前函数的栈帧地址
        printf("func frame:%p\n",p);   
        p = __builtin_frame_address(1);		// 获取调用者的栈帧地址
        printf("main frame:%p\n",p);
    }
    

    ·内建函数__builtin_constant_p(EXP)用于判断一个值是否为编译时常数,如果参数EXP的值是常数,函数返回1,否则返回0。例如,下面的代码可检测第1个参数是否为编译时常数以确定采用参数版本还是非参数版本:

    #define _dma_cache_sync(addr, sz, dir)        \
    do {                            \
        if (__builtin_constant_p(dir))          \
            __inline_dma_cache_sync(addr, sz, dir); \
        else                        \
            __arc_dma_cache_sync(addr, sz, dir);    \
    }                            \
    while (0)
    

    ·内建函数__builtin_expect(EXP,C)用于为编译器提供分支预测信息,其返回值是整数表达式EXP的值,C的值必须是编译时常数。Linux内核编程时常用的likely()和unlikely()底层调用的likely_notrace()、unlikely_notrace()就是基于__builtin_expect(EXP,C)实现的。

    #define likely_notrace(x) __builtin_expect(!!(x), 1)
    #define unlikely_notrace(x) __builtin_expect(!!(x), 0)
    

    ·若代码中出现分支,则即可能中断流水线,我们可以通过likely()和unlikely()暗示分支容易成立还是不容易成立,例如:

    if (likely(!IN_DEV_ROUTE_LOCALNET(in_dev)))
        if (ipv4_is_loopback(saddr))
            goto e_inval;
    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值