linux内核设计与实现的读书笔记
- 格式:doc
- 大小:16.19 KB
- 文档页数:4
Linux内核console设备实现详解【转】本⽂基于Linux-4.141.earlyconearly console,顾名思义,他表⽰的就是早期的console设备,主要⽤于在系统启动阶段的内核打印的输出,由于linux内核实际设备驱动模型还没有加载完成,所以早期的启动信息需要⼀个特殊的console⽤于输出log。
在系统初始化时通过cmdline参数来解析,代码如下:./init/main.c:static int __init do_early_param(char *param, char *val,const char *unused, void *arg){const struct obs_kernel_param *p;for (p = __setup_start; p < __setup_end; p++) {if ((p->early && parameq(param, p->str)) ||(strcmp(param, "console") == 0 &&strcmp(p->str, "earlycon") == 0)) {if (p->setup_func(val) != 0)pr_warn("Malformed early option '%s'\n", param);}}/* We accept everything at this stage. */return 0;}void __init parse_early_options(char *cmdline){parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,do_early_param);}整个流程如下,系统启动阶段会读取cmdline,并且解析cmdline参数名字为earlycon的参数,然后执⾏do_early_param操作,其中的关键是调⽤p->setup_func,这个也就是earlycon驱动实现的内容,param_setup_earlycon函数:⾸先来看内核实现的earlycon驱动:./drivers/tty/serial/earlycon.c:static int __init param_setup_earlycon(char *buf){int err;* Just 'earlycon' is a valid param for devicetree earlycons;* don't generate a warning from parse_early_params() in that case*/if (!buf || !buf[0]) {if (IS_ENABLED(CONFIG_ACPI_SPCR_TABLE)) {earlycon_init_is_deferred = true;return 0;} else if (!buf) {return early_init_dt_scan_chosen_stdout();}}err = setup_earlycon(buf);if (err == -ENOENT || err == -EALREADY)return 0;return err;}early_param("earlycon", param_setup_earlycon);上⾯的代码会创建⼀个如下结构体,⽤于和cmdline中的参数进⾏匹配:struct obs_kernel_param {const char *str;int (*setup_func)(char *);int early;};然后我们来看关键的setup实现param_setup_earlycon->setup_earlycon:./drivers/tty/serial/earlycon.c:int __init setup_earlycon(char *buf){const struct earlycon_id **p_match;if (!buf || !buf[0])return -EINVAL;if (early_con.flags & CON_ENABLED)return -EALREADY;for (p_match = __earlycon_table; p_match < __earlycon_table_end;p_match++) {const struct earlycon_id *match = *p_match;size_t len = strlen(match->name);if (strncmp(buf, match->name, len))continue;if (buf[len]) {if (buf[len] != ',')continue;buf += len + 1;} elsebuf = NULL;return register_earlycon(buf, match);}return -ENOENT;}最后我们来看关键的setup实现setup_earlycon->register_earlycon:static int __init register_earlycon(char *buf, const struct earlycon_id *match) {int err;struct uart_port *port = &early_console_dev.port;/* On parsing error, pass the options buf to the setup function */if (buf && !parse_options(&early_console_dev, buf))buf = NULL;spin_lock_init(&port->lock);port->uartclk = BASE_BAUD * 16;if (port->mapbase)port->membase = earlycon_map(port->mapbase, 64);earlycon_init(&early_console_dev, match->name);err = match->setup(&early_console_dev, buf);if (err < 0)return err;if (!early_console_dev.con->write)return -ENODEV;register_console(early_console_dev.con);return 0;}最终会调⽤register_console注册printk输出对应的console。
Linux内核:RCU机制与使⽤Linux 内核:RCU机制与使⽤背景学习Linux源码的时候,发现很多熟悉的数据结构多了__rcu后缀,因此了解了⼀下这些内容。
介绍RCU(Read-Copy Update)是数据同步的⼀种⽅式,在当前的Linux内核中发挥着重要的作⽤。
RCU主要针对的数据对象是链表,⽬的是提⾼遍历读取数据的效率,为了达到⽬的使⽤RCU机制读取数据的时候不对链表进⾏耗时的加锁操作。
这样在同⼀时间可以有多个线程同时读取该链表,并且允许⼀个线程对链表进⾏修改(修改的时候,需要加锁)。
RCU适⽤于需要频繁的读取数据,⽽相应修改数据并不多的情景,例如在⽂件系统中,经常需要查找定位⽬录,⽽对⽬录的修改相对来说并不多,这就是RCU发挥作⽤的最佳场景。
RCU(Read-Copy Update),是 Linux 中⽐较重要的⼀种同步机制。
顾名思义就是“读,拷贝更新”,再直⽩点是“随意读,但更新数据的时候,需要先复制⼀份副本,在副本上完成修改,再⼀次性地替换旧数据”。
这是 Linux 内核实现的⼀种针对“读多写少”的共享数据的同步机制。
RCU机制解决了什么在RCU的实现过程中,我们主要解决以下问题:1、在读取过程中,另外⼀个线程删除了⼀个节点。
删除线程可以把这个节点从链表中移除,但它不能直接销毁这个节点,必须等到所有的读取线程读取完成以后,才进⾏销毁操作。
RCU中把这个过程称为宽限期(Grace period)。
2、在读取过程中,另外⼀个线程插⼊了⼀个新节点,⽽读线程读到了这个节点,那么需要保证读到的这个节点是完整的。
这⾥涉及到了发布-订阅机制(Publish-Subscribe Mechanism)。
3、保证读取链表的完整性。
新增或者删除⼀个节点,不⾄于导致遍历⼀个链表从中间断开。
但是RCU并不保证⼀定能读到新增的节点或者不读到要被删除的节点。
RCU(Read-Copy Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。
Linuxkernel驱动相关抽象概念及其实现之“bus,device,driver”bus,device,driver三个很重要的概念贯穿Linux内核驱动架构,特转载⼀篇博⽂:内核的开发者将总线,设备,驱动这三者⽤软件思想抽象了出来,巧妙的建⽴了其间的关系,使之更形象化。
结合前⾯所学的知识,总的来说其三者间的关系为bus有两条链表,分别⽤于挂接设备和驱动,指定了其⾃⾝bus的device或者driver最后都会分别连接到对应bus的这两条链表上,⽽总线⼜有其始端,为bus_kset,⼀个driver可以对应于⼏个设备,因此driver同样有其设备链表,⽤于挂接可以操作的设备,其⾃⾝也有bus挂接点,⽤于将⾃⾝挂接到对应bus(每个driver只属于⼀条总线),⽽对于device,⼀个设备只属于⼀条总线,只能有⼀个driver与其对应,因此对于device,都是单⼀的,⼀个driver挂接点,⼀个bus挂接点,device与bus相同的是都有始端,device为devices_kset,因此device的注册同时会出现在对应的bus⽬录和device总⽬录下。
好了,下⾯就以源码为例分别分析⼀下bus,device,driver的注册过程。
⼀、bus的注册bus的注册⽐较简单,⾸先来看⼀下bus的结构:1struct bus_type {2const char *name; //名字3struct bus_attribute *bus_attrs; //bus属性集4struct device_attribute *dev_attrs; //device属性集5struct driver_attribute *drv_attrs; //driver属性集6int (*match)(struct device *dev, struct device_driver *drv);7int (*uevent)(struct device *dev, struct kobj_uevent_env *env);8int (*probe)(struct device *dev);9int (*remove)(struct device *dev);10void (*shutdown)(struct device *dev);11int (*suspend)(struct device *dev, pm_message_t state);12int (*resume)(struct device *dev);13const struct dev_pm_ops *pm;14struct bus_type_private *p; //bus的私有成员15 };16//其中重点看⼀下私有成员结构体:17struct bus_type_private {18struct kset subsys; //bus内嵌的kset,代表其⾃⾝19struct kset *drivers_kset;20struct kset *devices_kset;21struct klist klist_devices; //包含devices链表及其操作函数22struct klist klist_drivers; //driver链表及其操作函数23struct blocking_notifier_head bus_notifier;24 unsigned int drivers_autoprobe:1; //匹配成功⾃动初始化标志25struct bus_type *bus;26 };⽆论是bus,driver,还是device其本⾝特征都放在私有成员⾥,其注册时,都会申请并填充这个结构体,下⾯具体分析⼀下bus的注册流程,从bus_register开始:1int bus_register(struct bus_type *bus)2 {3int retval;4struct bus_type_private *priv;5 priv = kzalloc(sizeof(struct bus_type_private), GFP_KERNEL); //进⼊时bus_type->bus_type_private为NULL6if (!priv) //该函数主要是对其的设置7return -ENOMEM;8 priv->bus = bus; //私有成员的bus回指该bus9 bus->p = priv; //初始化bus->p,即其私有属性10 BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);11 retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name); //设置该bus的名字,bus是kset的封装12if (retval)13goto out;14//bus_kset即为所有bus的总起始端点15//围绕bus内嵌的kset初始化,和kset的初始化时围绕16 priv->subsys.kobj.kset = bus_kset; //kobj相似,没有parent时,就会⽤kset的kobj,此处即是17 priv->subsys.kobj.ktype = &bus_ktype; //属性操作级别统⼀为bus_ktype18 priv->drivers_autoprobe = 1; //设置该标志,当有driver注册时,会⾃动匹配devices19//上的设备并⽤probe初始化,20//当有device注册时也同样找到 driver并会初始化21 retval = kset_register(&priv->subsys); //注册kset,创建⽬录结构,以及层次关系22if (retval)23goto out;24 retval = bus_create_file(bus, &bus_attr_uevent); //当前bus⽬录下⽣成bus_attr_uevent属性⽂件25if (retval)26goto bus_uevent_fail;27 priv->devices_kset = kset_create_and_add("devices", NULL, //初始化bus⽬录下的devices⽬录,⾥⾯级联了该bus下设备,28 &priv->subsys.kobj); //仍然以kset为原型29if (!priv->devices_kset) {30 retval = -ENOMEM;31goto bus_devices_fail;32 }33 priv->drivers_kset = kset_create_and_add("drivers", NULL, //初始化bus⽬录下的drivers⽬录,⾥⾯级联了该bus下设备的driver34 &priv->subsys.kobj);35if (!priv->drivers_kset) {36 retval = -ENOMEM;37goto bus_drivers_fail;38 }39 klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put); //初始化klist_devices⾥的操作函数成员40 klist_init(&priv->klist_drivers, NULL, NULL); //klist_drivers⾥的操作函数置空41 retval = add_probe_files(bus); //增加bus_attr_drivers_probe和bus_attr_drivers_autoprobe42if (retval) //属性⽂件43goto bus_probe_files_fail;44 retval = bus_add_attrs(bus); //增加默认的属性⽂件45if (retval)46goto bus_attrs_fail;47 pr_debug("bus: '%s': registered/n", bus->name);48return0;49 bus_attrs_fail: //以下为错误处理50 remove_probe_files(bus);51 bus_probe_files_fail:52 kset_unregister(bus->p->drivers_kset);53 bus_drivers_fail:54 kset_unregister(bus->p->devices_kset);55 bus_devices_fail:56 bus_remove_file(bus, &bus_attr_uevent);57 bus_uevent_fail:58 kset_unregister(&bus->p->subsys);59out:60 kfree(bus->p);61 bus->p = NULL;62return retval;63 }由此可见,bus⼜是kset的封装,bus_register主要完成了其私有成员bus_type_private的初始化,并初始化了其下的两个⽬录devices和drivers,及其属性⽂件,bus有个⾃⼰的根⽬录也就是bus有个起始端点,是bus_kset,经过此番的注册,bus⽬录下将会出现我们注册的bus,并且其下会有device和driver两个⼦⽬录,代表它下⾯的driver和device链表。
Linux内核中DMI实现简介Linux内核中DMI实现简介1. 配置在配置内核时,如果选择了CONFIG_DMI选项,会将DMI (Desktop Management interface)功能添加到内核中。
此功能代码在drivers/firmware/Dmi_scan.c文件中。
2. 功能实现2.1 函数调用关系dmi_scan_machineàdmi_presentàdmi_walk_earlyàdmi_table àdmi_decodedmi_scan_machine函数在地址空间0xF0000~0x100000之间通过调用dmi_present函数判断DMI表是否存在;dmi_present函数检查标识_DMI_并计算CRC值来确定DMI表是否正确。
当此表正确时,通过表头信息得到dmi_num、dmi_len、dmi_base值;dmi_walk_early函数通过dmi_num、dmi_len、dmi_base信息,调用dmi_table对DMI表信息进行解析;dmi_table函数调用dmi_decode函数完成DMI信息的解析和存储;dmi_decode解析DMI信息,包含BIOS Information、System Information、Base Board Information、Chassis Information、Onboard Devices Information、OEM Strings、IPMI Device Information、Onboard Devices Extended Information,并且将这些信息存储起来;2.2 DMI信息使用通过dmi_decode函数,已经将相关信息进行了存储。
当内核中其他模块需要使用DMI信息时,可以调用相关函数,比如dmi_check_system。
此函数会调用dmi_matches,如果BIOS中存储的DMI信息与需要查找的信息相匹配,dmi_matches函数会返回真,否则dmi_matches函数会返回假。
Linux 同步方法剖析内核原子,自旋锁和互斥锁你也许接触过并发(concurrency)、临界段(critical section)和锁定,不过怎么在内核中使用这些概念呢?本文讨论了 2.6 版内核中可用的锁定机制,包括原子运算符(atomic operator)、自旋锁(spinlock)、读/写锁(reader/writer lock)和内核信号量(kernel semaphore)。
本文还探讨了每种机制最适合应用到哪些地方,以构建安全高效的内核代码。
本文讨论了 Linux 内核中可用的大量同步或锁定机制。
这些机制为 2.6.23 版内核的许多可用方法提供了应用程式接口(API)。
不过在深入学习 API 之前,首先需要明白将要解决的问题。
并发和锁定当存在并发特性时,必须使用同步方法。
当在同一时间段出现两个或更多进程并且这些进程彼此交互(例如,共享相同的资源)时,就存在并发现象。
在单处理器(uniprocessor,UP)主机上可能发生并发,在这种主机中多个线程共享同一个 CPU 并且抢占(preemption)创建竞态条件。
抢占通过临时中断一个线程以执行另一个线程的方式来实现 CPU 共享。
竞态条件发生在两个或更多线程操纵一个共享数据项时,其结果取决于执行的时间。
在多处理器(MP)计算机中也存在并发,其中每个处理器中共享相同数据的线程同时执行。
注意在 MP 情况下存在真正的并行(parallelism),因为线程是同时执行的。
而在 UP 情形中,并行是通过抢占创建的。
两种模式中实现并发都较为困难。
Linux 内核在两种模式中都支持并发。
内核本身是动态的,而且有许多创建竞态条件的方法。
Linux 内核也支持多处理(multiprocessing),称为对称多处理(SMP)。
临界段概念是为解决竞态条件问题而产生的。
一个临界段是一段不允许多路访问的受保护的代码。
这段代码能操纵共享数据或共享服务(例如硬件外围设备)。
linux系统的内核子系统之间的关系Linux系统的内核子系统之间的关系Linux操作系统的内核是其最核心的组成部分,它负责管理和控制整个系统的运行。
内核由多个子系统组成,每个子系统负责不同的功能模块,它们之间相互配合,共同完成系统的各项任务。
本文将介绍几个常见的内核子系统及其之间的关系。
1. 文件系统子系统文件系统子系统负责管理文件和目录的存储和访问。
它提供了对文件系统的抽象,使用户和应用程序可以通过文件路径来访问文件和目录。
文件系统子系统由虚拟文件系统层、各种具体的文件系统类型和存储设备驱动程序组成。
虚拟文件系统层提供了一个统一的接口,使不同的文件系统可以以相同的方式进行访问。
具体的文件系统类型如ext4、NTFS等负责实现不同的文件系统格式,而存储设备驱动程序则负责控制硬盘、闪存等存储设备的读写。
2. 进程管理子系统进程管理子系统负责管理系统中的进程。
它负责创建、终止和调度进程,并提供进程间通信和同步的机制。
进程管理子系统包括进程调度器、进程控制块、进程间通信和同步机制等。
进程调度器决定了系统中运行哪些进程以及它们的优先级和时间片分配。
进程控制块保存了进程的状态信息,包括程序计数器、寄存器和运行时堆栈等。
进程间通信和同步机制如管道、信号量、消息队列等,使不同进程之间可以进行数据交换和协调工作。
3. 设备驱动子系统设备驱动子系统负责管理和控制硬件设备的访问。
它提供了对设备的抽象接口,使应用程序可以通过统一的方式访问不同类型的设备。
设备驱动子系统包括字符设备驱动和块设备驱动。
字符设备驱动用于管理字符设备,如串口、键盘等,它提供了以字节为单位的读写接口。
块设备驱动用于管理块设备,如硬盘、闪存等,它提供了以块为单位的读写接口。
设备驱动子系统还包括中断处理、DMA控制等功能,用于处理设备的中断请求和数据传输。
4. 网络子系统网络子系统负责管理和控制系统的网络功能。
它提供了网络协议栈、网络接口和网络设备驱动等功能。
linux内核设计与实现的读书笔记
进程的调度程序是保证进程能有效工作的一个内核子系统。调
度程序负责决定将哪个进程投入运行,何时运行以及运行多少时间。
简单的来说,调度程序就是在给一堆就绪的进程分配处理器的时间,
调度程序是多任务操作系统的基础。调度程序的原则就是最大限度的
使用cpu的资源,也就是说,当系统中只要有可运行的进程,就不能
让cpu处于空闲的状态,如果系统中没有就绪的进程时,则cpu会运
行一个idle进程。
1.多任务
多任务操作系统就是能够同时并发的交互执行多个进程的操作
系统,需要注意这里是并发,而不是并行。如果你的计算机有两个或
者两个以上的cpu那么,你的计算机就可以真正同时、并行的执行多
个任务。多任务操作系统可以分为两类:抢占式多任务和非抢占式多
任务。
抢占式多任务中,由调度程序来决定什么时候停止一个进程的
执行,这种由调度程序强行停止一个进程执行的动作称为抢占
(preemption)。进程在被抢占之前运行的时间是固定的,而且有一个
专门的名字,叫做时间片(timeslice)。时间片实际上是分配给每个
进程的处理器时间段。
而非抢占式多任务是由进程自己做出让步,在执行了一段时间
之后,主动地让出cpu。进程主动挂起自己的操作称为让步(yielding),
如果某个进程悬挂起来并且拒不作出让步的话,可能会导致操作系统
崩溃。
所以总述上面的两种情况,抢占式多任务就像“法律”,只要
时间到了,就把你撤下来。而非抢占式却像“道德”一样,你要是有
道德,执行了一会之后,你就自己撤下来,如果有的“人”占着茅坑
不拉屎,那其他进程除了用“道德”谴责它,也没有其他的办法了。
2.linux进程调度
linux最初的进程调度程序是非常原始的,很难适应一些众多
的可运行进程和多处理器环境。后来从linux2.5开始,对linux的
进程调度程序做了大的调整,使用了称为O(1)的调度算法,这个算
法引起算法行为而得名。O(1)调度算法虽然在数以十计的多处理器上
能表现出近乎完美的特性和可扩展性,但是由于这个算法在调度交互
进程的时候并没有表现出很理想的效果。所以在linux2.6的开发初
期,提出了CFS算法,即完全公平调度算法。
3.策略
(1)IO消耗型进程和处理器消耗型进程
IO消耗型进程指的是进程的大部分时间是用来等待IO的操作,
例如图形用户界面(GUI)程序就属于IO消耗型程序,这个程序需要不
断的监听用户的输入。这样的进程经常处于可运行的状态,但是每次
运行的时间都很短。
处理器消耗型进程是指进程的大部分时间用在执行代码上,比
如大型的计算程序MATLAB就属于处理器消耗型进程。
还有一些应用程序虽然划分为IO消耗型进程,但是也有处理器
消耗型进程的特征。例如,字处理程序,在大多数时间可能等待用户
的输入,但是在某段时间该程序又可能粘住处理器疯狂的进行语法和
拼写错误的检查。
调度程序需要在两个矛盾目标中寻找平衡————进程的迅速
响应和高吞吐量。unix和linux为了获得良好的用户响应,因此都
倾向于调度IO消耗型进程。
(2)进程优先级
调度算法中最基本的一种就是基于进程优先级的调度,这是一
种根据进程的价值和其对处理器的时间需求来对进程分级的一种想
法。通常的做法是优先级高的进程先执行,低的后运行,相同优先级
的进程按轮转方式进行调度(一个接一个,重复进行)。在某些操作系
统中,优先级高的进程的使用的时间片也长一些。调度程序总是选择
优先级高的,并且时间片尚未用尽的进程。
linux系统采用了两种不同类别的优先级,第一种是使用nice
值,范围是从-20到+19,值越大表示优先级越低。这个优先级适用
于一般的进程。
另外,linux对实时进程采用实时优先级,值从0-99,值越大
代表优先级越高。实时进程的优先级都高于普通进程,因此这两个进
程优先级是处于两个互不相交的范围内。
(3)时间片
时间片是一个数值,他表示进程在被抢占前能够持续运行的时
间。时间片过长会导致系统对交互的响应表现欠佳,时间片过短,却
又明显增大进程切换带来的处理器时间消耗。所以IO消耗型进程和
处理器消耗型进程的矛盾在这里又再次显现出来,IO消耗型进程不
需要长的时间片,而处理器消耗型进程则希望时间片越长越好。
长时间片将导致系统的交互性表现欠佳,很多的操作系统都很
重视这一点,因此将时间片设置的很短,如10ms。但是linux的CFS
调度算法并没有直接分配时间片到进程,它是将处理器的使用比分给
了进程,这样进程获得的处理器的时间是和系统负载密切相关的。这
个比例还会受到nice值的影响,nice值作为权重将调整进程使用处
理器时间的使用比。具有更高nice值(低优先级)的进程将被赋予低
权重,从而丧失一小部分处理器的使用比,而具有低nice值(高优先
级)的进程江北赋予高权重,从而获得更多的处理器使用比。
在多数的抢占式操作系统中,一个新就绪的进程能否立即执行
(即抢占原来的进程),完全取决于新进程的优先级和是否有时间片。
而在linux中采用的CFS调度器,其抢占时机取决于新的进程消耗了
多少处理器的使用比,如果新就绪的进程消耗的处理器使用比低于当
前的进程,则新进程抢占当前进程,立即投入运行。