当前位置:首页 > linux内核
  • 精致全景图 | linux内核输出的日志去哪里了

    因为图片比较大,微信公众号上压缩的比较厉害,所以很多细节都看不清了,我单独传了一份到github上,想要原版图片的,可以点击下方的阅读原文,或者直接使用下面的链接,来访问github:https://github.com/wangyuntao/linux-kernel-illustrated另外,精致全景图系列文章,以及之后的linux内核分析文章,我都会整理到这个github仓库里,欢迎大家star收藏。熟悉linux内核,或者看过linux内核源码的同学就会知道,在内核中,有一个类似于c语言的输出函数,叫做printk,使用它,我们可以打印各种我们想要的信息,比如内核当前的运行状态,又或者是我们自己的调试日志等,非常方便。那当我们调用printk函数后,这些输出的信息到哪里去了呢?我们又如何在linux下的用户态,查看这些信息呢?为了解答这些疑问,我画了一张printk全景图,放在了文章开始的部分,这张图既包含了printk在内核态的实现,又包含了其输出的信息在用户态如何查看。我们可以根据这张图,来理解printk的整体架构。在内核编码时,如果想要输出一些信息,通常并不会直接使用printk,而是会使用其衍生函数,比如 pr_err / pr_info / pr_debug 等,这些衍生函数附带了日志级别、所属模块等其他信息,比较友好,但其最终还是调用了printk。printk函数会将每次输出的日志,放到内核为其专门分配的名为ring buffer的一个槽位里。ring buffer其实就是一个用数组实现的环形队列,不过既然是环形队列,就会有一个问题,即当ring buffer满了的时候,下一条新的日志,会覆盖最开始的旧的日志。ring buffer的大小,可以通过内核参数来修改。printk在将日志放到ring buffer后,会再调用系统console的相关方法,将还未输出到系统控制台的消息,继续输出到控制台,这个后面会详细说,这里就暂不赘述。以上就是printk在内核态的实现。在用户态,我们有几个方式,可以查看printk输出的内核日志,比如使用dmesg命令,cat /proc/kmsg文件,或者是使用klogctl函数等,这些方式分别对应于全景图中用户态的橙色、绿色、和蓝色的部分。dmesg命令,在默认情况下,是通过读取/dev/kmsg文件,来实现查看内核日志的。当该命令运行时,dmesg会先调用open函数,打开/dev/kmsg文件,该打开操作在内核中的逻辑,会为dmesg分配一个file实例,在这个file实例里,会有一个seq变量,该变量记录着下一条要读取的内核日志在ring buffer中的位置。刚打开/dev/kmsg文件时,这个seq指向的就是ring buffer中最开始的那条日志。之后,dmesg会以打开的/dev/kmsg文件为媒介,不断的调用read函数,从内核中读取日志消息,每读取出一条,seq的值都会加一,即指向下一条日志的位置,依次往复,直到所有的内核日志读取完毕,dmesg退出。以上就是dmesg的主体实现。第二种查看内核日志的方式,是通过 cat /proc/kmsg 命令。该命令和dmesg命令的实现机制基本类似,都是通过读文件,只不过cat读取的是/proc/kmsg文件,而dmesg读取的是/dev/kmsg文件。读取这两个文件最大的区别是,/dev/kmsg文件每次打开时,内核都会为其分配一个单独的seq变量,而/proc/kmsg文件每次打开时,用的都是同一个全局的静态seq变量,叫做syslog_seq。syslog_seq指向的也是下一条要读取的内核日志在ring buffer中的位置,但因为它是一个全局的静态变量,当有多个进程要读取/proc/kmsg文件时,就会有一个比较严重的问题,即内核日志会被这几个进程随机抢占读取,也就是说,每个进程读到的都是整个内核日志的一部分,是不完整的,这也是dmesg命令默认不使用/proc/kmsg文件的原因。第三种查看内核日志的方式,是通过klogctl函数。该函数是glibc对syslog系统调用的一个简单封装,其具体使用方式,可以参考全景图中用户态的蓝色部分。klogctl函数可以指定很多命令,在上图的示例中,我们使用的是SYSLOG_ACTION_READ命令,以此来模拟 cat /proc/kmsg 行为。其实在内核层面,cat /proc/kmsg命令,使用的就是klogctl对应的syslog系统调用的SYSLOG_ACTION_READ命令的处理逻辑,所以示例中的klogctl函数相关代码,和 cat /proc/kmsg 命令其实是等价的。也就是说,klogctl函数在内核里使用的也是syslog_seq变量,它也有和/proc/kmsg文件同样的问题。其实还有一种方式可以查看内核日志,就是通过系统控制台。但这种方式和前面讲的三种方式都不一样,它是完全被动的,是内核在调用printk函数,将日志信息放到ring buffer后,再去通知系统控制台,告知其可以输出这些日志。系统控制台也是通过一个console_seq变量,记录下一条要输出内核日志的所在位置。这里说的系统控制台,是指我们在开机的时候,黑色屏幕输出的那些内容,但当我们进入图形化界面后,我们就看不到系统控制台的输出了,除非我们再用 ctrl alt f1/f2/f3 等方式,切换成系统控制台。系统控制台输出的内容,是被日志级别过滤过的,内核默认的日志过滤级别是7,即debug级别以上的日志,比如info / err 等,这些都会输出,但debug级别不会输出。该日志过滤级别,可以通过很多方式改变,比如说,可以通过内核参数 loglevel,所以,如果发现系统控制台没有输出想要的日志信息,先看下其是否被过滤掉了。以上就是printk生态的完整实现。了解printk函数的实现,对于内核开发者或研究者来说,意义非常大,但对于普通的应用开发人员来说,又有什么帮助呢?其实,随着技术的深入,我们不应该再只关心应用层面的行为,而且还要关心系统层面的行为,这样我们才能更好的去定位问题,更好的去保证我们应用的健康运行。比如,当我们的应用需要内存时,会向操作系统申请,操作系统此时给我们的,其实是虚拟内存,只有当我们的进程真正的在使用这些内存时,比如读/写,操作系统才会为其分配物理内存。但假设此时物理内存没有了,那操作系统会怎么办?对于linux内核来说,它会选择一个使用内存最多的进程,然后将其kill掉,以此来释放内存,保证后续的内存分配操作能够成功,这个我在之前文章 为什么我的进程被kill掉了 有详细讲过。对于内核的这种行为,我们就应该多加关注,而关注的方式,就是查看内核日志。比如,linux内核在kill掉进程时,会用pr_err记录一行日志:如果我们发现一个进程跑着跑着就没有了,就可以通过dmesg命令,查看是否有这个日志,如果有,说明该进程因为系统内存不足,被操作系统kill掉了。类似的,内核里还有很多error级别,甚至更高级别的日志需要我们关注,通过这些日志,我们可以及时的发现系统的异常情况,必要时可以人工介入进行干预。总之,对系统了解的越深,内核日志对我们的帮助就越大。就这些,希望你喜欢。

    时间:2021-09-14 关键词: linux内核

  • Linux内核维护者这些事!你了解吗?

    Linux是一种开源电脑操作系统内核。它是一个用C语言写成,符合POSIX标准的类Unix操作系统。就目前而言windows仍然是最主流的系统,但最近的linux的势头也相当猛,很多人开始放弃windows转向使用Linux,Linux相对于window而言,运行也更快。Linux的内核也是有人在维护的,但是但你了解Linux的内核维护者的这些事吗?你对内核维护者又了解多少呢? Linux的5.5内核的发布时间是2020年1月,到现在已经有近87,000个patch,来自于近4600名开发者,都被合并到mainline仓库中了。review所有这些patch的工作,对于愿意花时间的内核开发者来说也都是一项艰巨的任务,所以是否要接受合并patch,这个决定权就被委托给了各个子系统的维护者(maintainer)来代理决定,也就是说他们是相对独立的。他们每个人都对内核中这一部分的改动具有部分或者完整的决定权。这些维护者们就被记录在一个叫MAINTAINERS的文件中(当然是这个名字)。 在与 Dirk 的谈话中,Linus 认为 Linux 内核开发对大多数人来说是无聊的。“ 我说过内核很无聊,但我的意思是从某种意义上说,许多新技术应该比内核开发更有趣。但是对我和其他内核开发者来说,没有什么能够比与底层硬件交互的内核开发更有趣的了,因为这能真正控制所有将在计算机中发生的事情。所以内核对我来说并不是很无聊,但是我想大多数人应该会认为它们很无聊。”也就是说,他们做linux维护时非常无聊。 Linux内核维护时非常无聊的事情。但又必须有人来做。但是,MAINTAINERS文件也需要维护,它能很好地反映现实情况吗?MAINTAINERS文件的存在目的,并不仅仅是为了让大家给维护者点赞。开发者们需要用它来确定该把patch发到哪里。get_maintainer.pl脚本通过查看这个patch修改了的文件,就可以生成一系列邮件地址来发送patch,从而让这一过程变得更加自动化。如果这个文件中有错误信息的话,就可能会让patch发送到错误的地方去,所以我们需要这个文件能保持更新。最近,编者收到JakubKicinski的建议,他认为可以比较一下MAINTAINERS中的各个条目和现实世界中的工作的吻合程度,应该能得到一些线索。于是折腾了一会儿Python之后,我们就得到了一个新的分析脚本。 Linux之父Linus 在采访时透露“我们确实有不少编写代码的人,但缺少维护者。” 在 Linus 看来,Linux 内核项目未来交接的难点在于寻找一个自己的继任者。“ 事实证明,我们现在很难找到合格的新维护者。所以成为内核维护者的要求时非常严苛的,因为成为内核维护者,你必须一直存在,这意味着你每天都要在电脑前阅读来自全球各地的电子邮件,对邮件做出反应,就像我这 30 年来做的一样,从不间断。我们现在很难找到真正看待他人代码,并在上游严格把关每一个提交,判断它们能否合并到我们的主干代码上的人。这是我们面临的最主要的问题。” Linus 说。 当你成为linux维护者的时候,你需要遵循贡献者契约行为准则 ,准则是一个通用文档,旨在为几乎所有开源社区提供一套规则。 每个开源社区都是独一无二的,Linux内核也不例外。因此,本文描述了Linux内核社区中 如何解释它。我们也不希望这种解释随着时间的推移是静态的,并将根据需要进行调整。   Windows有微软的员工去维护,我个人认为,linux在将来必将超过window,成为最通用的系统,因为他的便捷性,开源性,成为内核维护者很难,要求很高,但是不代表不可能。正是因为这个难度,才能使linux成为真正好的系统。此前也有很多大牛,从windows转向了linux系统,所以它的前景一片光明。 Linux还有很多可以讲的东西,这里就不再赘述。有兴趣的可以持续关注21ic电子网,持续关注小编。

    时间:2021-02-22 关键词: Linux linux维护 linux内核

  • Linux内核驱动在新的NTFS中修订了九次 仍在审核中

    早在今年8月份的时候,Paragon软件公司高调宣布,他们的NTFS读写驱动在作为商业驱动提供给那些需要在Linux上对微软文件系统进行可靠支持的用户多年后,将在Linux内核中进行主线开发。 两个月后的今天,他们已经对这个驱动进行了第九次修订,以争取将其纳入内核主线。 在上游开发者提出一些最初的意见之后,Paragon很快就更新了他们的 "NTFS3"驱动补丁。他们继续完善代码,增加了额外的功能,修复了各种代码问题,改进了代码格式,处理了重新挂载文件系统等行为,并修复了压缩文件操作中的问题。 周五,他们发出了第九个版本的驱动,增加了额外的代码改进,最新的变化可以在内核邮件列表中找到。 由于目前的内核NTFS驱动主要集中在只支持读的状态,根据进度预估,这个新驱动不会登陆Linux 5.10,但合并操作大概会在明年出现。 这是因Linux 5.10合并窗口已经过了一半了,而这个NTFS3驱动的目标是取代现有的驱动,这使得最初的主线变得更加复杂,因为需要允许两个驱动都进入内核树一段时间,并且只允许一次编译一个,并且上游维护者更愿意在合并这个新驱动时选择多等等,直到它可以立即取代现有的NTFS支持代码。 不管怎么说,至少这个Paragon的贡献看起来已经很成熟了,为的是在2021年有更好的Linux NTFS支持。

    时间:2020-10-22 关键词: Linux ntfs linux内核

  • Linux内核驱动开发者探讨为Intel处理器引入降压功能的可行性

    在 Windows 平台上,一些高级用户已经习惯了通过 XTU 实用工具来降低英特尔移动处理器的电压。但在 Linux 平台上,如果你想要追求更好的散热 / 能源效率(或其它目的),目前社区里仍缺少可行的 CPU 降压方案。 好消息是,近段时间,已有不少内核驱动开发者在探讨这么做的可行性。 其实此前,已有独立开发者推出过非官方的 intel-undervolt 应用程序,可惜它仅限于 Haswell 和更新版本的英特尔处理器。 除了降低 CPU 电压,该工具还可操纵英特尔处理器的功耗 / 散热墙。但 intel-undervolt 功能毕竟依赖于反向工程和社区的支持发现,并且需要直接接触 CPU 的 MSRs 来执行操作。 庆幸的是,以 WireGuard 而被大家所熟知的 Jason Donenfeld,恰好向内核开发人员提交了一个补丁。其能够写入特定的 OC mailbox MSR,而不会导致内核警告。 具体说来是,该补丁源于近期在 Linux 上的一项工作,旨在加强从用户空间对 CPU MSR 的访问。在后续的讨论中,开发者们开始将重心放到了其它细节上。 最终结果是得到了大多数开发者的同意,且与通过 MSR 进行交互的方案相比,基于内核驱动程序的调节效果更具优势。 现在的问题是,开发者暂缺与 undervolt 相关的 MSR 文档、以及类似于 Windows 平台上的 Intel XTU 体验。至于后续的发展,或许还得看英特尔工程师们可以提供多少帮助。 所以我们可以期待一下了,毕竟现在Linux成为现在非常重要的系统之一。

    时间:2020-10-22 关键词: Linux intel处理器 linux内核

  • Linux内核电源管理的整体架构解析,你了解吗?

    Linux可以说是越来越火了,它在消费电子领域的应用相当普遍,而对于消费电子产品,省电是一个重要的议题。 Linux电源管理非常复杂,牵扯到系统级的待机、频率电压变换、系统空闲时的处理以及每个设备驱动对于系统待机的支持和每个设备的运行时电源管理,可以说和系统中的每个设备驱动都息息相关。 对于消费电子产品来说,电源管理相当重要。因此,这部分工作往往在开发周期中占据相当大的比重,图1呈现了Linux内核电源管理的整体架构。大体可以归纳为如下几类: 1.CPU在运行时根据系统负载进行动态电压和频率变换的CPUFreq。 2.CPU在系统空闲时根据空闲的情况进行低功耗模式的CPUIdle。 3.多核系统下CPU的热插拔支持。 4.系统和设备对于延迟的特别需求而提出申请的PMQoS,它会作用于CPUIdle的具体策略。 5.设备驱动针对系统SuspendtoRAM/Disk的一系列入口函数。 6.SoC进入suspend状态、SDRAM自刷新的入口。 7.设备的runtime(运行时)动态电源管理,根据使用情况动态开关设备。 8.底层的时钟、稳压器、频率/电压表(OPP模块完成)支撑,各驱动子系统都可能用到。 这样,你对linux的电源管理知道多少了?

    时间:2020-10-22 关键词: Linux 电源管理 linux内核

  • linux内核是啥?linux内核学习路线时怎么样的?

    Linux可以说是近期非常火的了,有的人想学习linux内核,那他到底是什么呢? Linux内核是一个操作系统(OS)内核,本质上定义为类Unix。它用于不同的操作系统,主要是以不同的Linux发行版的形式。Linux内核是第一个真正完整且突出的免费和开源软件示例。Linux 内核是第一个真正完整且突出的免费和开源软件示例,促使其广泛采用并得到了数千名开发人员的贡献。 Linux 内核由芬兰赫尔辛基大学的学生 Linus Torvalds 于 1991 年创建。随着程序员调整其他自由软件项目的源代码以扩展内核的功能,它迅速取得了进展。Torvalds 首先使用 80386 汇编语言编写的任务切换器以及终端驱动程序,然后将其发布到 comp.os.minix Usenet 组。它很快被 MINIX 社区所改编,为该项目提供了见解和代码。 Linux 内核越来越受欢迎,因为 GNU 自己的内核 GNU Hurd 不可用且不完整,而 Berkeley Software Distribution(BSD)操作系统仍然受到法律问题的困扰。在开发人员社区的帮助下,Linux 0.01 于 1991 年 9 月 17 日发布。 linux内核学习路线 很多同学接触Linux不多,对Linux平台的开发更是一无所知。而现在的趋势越来越表明,作为一 个优秀的软件开发人员,或计算机IT行业从业人员,掌握Linux是一种很重要的谋生资源与手段。下来我将会结合自己的几年的个人开发经验,及对 Linux,更是类UNIX系统,及开源软件文化,谈谈Linux的学习方法与学习中应该注意的一些事。 就如同刚才说的,很多同学以前可能连Linux是什么都不知道,对UNIX更是一无所知。所以我们从最基础的讲起,对于Linux及UNIX的历史我们不做多谈,直接进入入门的学习。 Linux入门是很简单的,问题是你是否有耐心,是否爱折腾,是否不排斥重装一类的大修。没折腾可以说是学不好Linux的,鸟哥说过,要真正了解Linux的分区机制,对LVM使用相当熟练,没有20次以上的Linux装机经验是积累不起来的,所以一定不要怕折腾。 由于大家之前都使用Windows,所以我也尽可能照顾这些“菜鸟”。我的推荐,如果你第一次接触Linux,那么首先在虚拟机中尝试它。虚拟机我推荐Virtual Box,我并不主张使用VM,原因是VM是闭源的,并且是收费的,我不希望推动盗版。当然如果你的Money足够多,可以尝试VM,但我要说的是即使是VM,不一定就一定好。付费的软件不一定好。首先,Virtual Box很小巧,Windows平台下安装包在80MB左右,而VM动辄600MB,虽然功能强大,但资源消耗也多,何况你的需求Virtual Box完全能够满足。所以,还是自己选。如何使用虚拟机,是你的事,这个我不教你,因为很简单,不会的话Google或Baidu都可以,英文好的可以直接看官方文档。 现在介绍Linux发行版的知识。正如你所见,Linux发行版并非Linux,Linux仅是指操作系统的内核,作为科班出生的你不要让我解释,我也没时间。我推荐的发行版如下: UBUNTU适合纯菜鸟,追求稳定的官方支持,对系统稳定性要求较弱,喜欢最新应用,相对来说不太喜欢折腾的开发者。 Debian,相对UBUNTU难很多的发行版,突出特点是稳定与容易使用的包管理系统,缺点是企业支持不足,为社区开发驱动。 Arch,追逐时尚的开发者的首选,优点是包更新相当快,无缝升级,一次安装基本可以一直运作下去,没有如UBUNTU那样的版本概念,说的专业点叫滚动升级,保持你的系统一定是最新的。缺点显然易见,不稳定。同时安装配置相对Debian再麻烦点。 Gentoo,相对Arch再难点,考验使用者的综合水平,从系统安装到微调,内核编译都亲历亲为,是高手及黑客显示自己技术手段,按需配置符合自己要求的系统的首选。 Slackware与Gentoo类似。 CentOS,社区维护的RedHat的复刻版本,完全使用RedHat的源码重新编译生成,与RedHat的兼容性在理论上来说是最好的。如果你专注于Linux服务器,如网络管理,架站,那么CentOS是你的选择。 LFS,终极黑客显摆工具,完全从源代码安装,编译系统。安装前你得到的只有一份文档,你要做的就是照文档你的说明,一步步,一条条命令,一个个软件包的去构建你的Linux,完全由你自己控制,想要什么就是什么。如果你做出了LFS,证明你的Linux功底已经相当不错,如果你能拿LFS文档活学活用,再将Linux从源代码开始移植到嵌入式系统,我敢说中国的企业你可以混的很好。 你得挑一个适合你的系统,然后在虚拟机安装它,开始使用它。如果你想快速学会Linux,我有一个建议就是忘记图形界面,不要想图形界面能不能提供你问题的答案,而是满世界的去找,去问,如何用命令行解决你的问题。在这个过程中,你最好能将Linux的命令掌握的不错,起码常用的命令得知道,同时建立了自己的知识库,里面是你积累的各项知识。 再下个阶段,你需要学习的是Linux平台的C/C++开发,同时还有Bash脚本编程,如果你对Java兴趣很深还有Java。同样,建议你抛弃掉图形界面的IDE,从VIM开始,为什么是VIM,而不是Emacs,我无意挑起编辑器大战,但我觉得VIM适合初学者,适合手比较笨,脑袋比较慢的开发者。Emacs的键位太多,太复杂,我很畏惧。然后是GCC,Make,Eclipse(Java,C++或者)。虽然将C++列在了Eclipse中,但我并不推荐用IDE开发C++,因为这不是Linux的文化,容易让你忽略一些你应该注意的问题。IDE让你变懒,懒得跟猪一样。如果你对程序调试,测试工作很感兴趣,GDB也得学的很好,如果不是GDB也是必修课。这是开发的第一步,注意我并没有提过一句Linux系统API的内容,这个阶段也不要关心这个。你要做的就是积累经验,在Linux平台的开发经验。我推荐的书如下:C语言程序设计,谭浩强的也可以。C语言,白皮书当然更好。C++推荐C++ Primer Plus,Java我不喜欢,就不推荐了。工具方面推荐VIM的官方手册,GCC中文文档,GDB中文文档,GNU开源软件开发指导(电子书),汇编语言程序设计(让你对库,链接,内嵌汇编,编译器优化选项有初步了解,不必深度)。 如果你这个阶段过不了就不必往下做了,这是底线,最基础的基础,否则离开,不要霍霍Linux开发。不专业的Linux开发者作出的程序是与Linux文化或UNIX文化相背的,程序是走不远的,不可能像Bash,VIM这些神品一样。所以做不好干脆离开。 接下来进入Linux系统编程,不二选择,APUE,UNIX环境高级编程,一遍一遍的看,看10遍都嫌少,如果你可以在大学将这本书翻烂,里面的内容都实践过,有作品,你口头表达能力够强,你可以在面试时说服所有的考官。(可能有点夸张,但APUE绝对是圣经一般的读物,即使是Windows程序员也从其中汲取养分,Google创始人的案头书籍,扎尔伯克的床头读物。) 这本书看完后你会对Linux系统编程有相当的了解,知道Linux与Windows平台间开发的差异在哪?它们的优缺点在哪?我的总结如下:做Windows平台开发,很苦,微软的系统API总在扩容,想使用最新潮,最高效的功能,最适合当前流行系统的功能你必须时刻学习。Linux不是,Linux系统的核心API就100来个,记忆力好完全可以背下来。而且经久不变,为什么不变,因为要同UNIX兼容,符合POSIX标准。所以Linux平台的开发大多是专注于底层的或服务器编程。这是其优点,当然图形是Linux的软肋,但我站在一个开发者的角度,我无所谓,因为命令行我也可以适应,如果有更好的图形界面我就当作恩赐吧。另外,Windows闭源,系统做了什么你更本不知道,永远被微软牵着鼻子跑,想想如果微软说Win8不支持QQ,那腾讯不得哭死。而Linux完全开源,你不喜欢,可以自己改,只要你技术够。另外,Windows虽然使用的人多,但使用场合单一,专注与桌面。而Linux在各个方面都有发展,尤其在云计算,服务器软件,嵌入式领域,企业级应用上有广大前景,而且兼容性一流,由于支持POSIX可以无缝的运行在UNIX系统之上,不管是苹果的Mac还是IBM的AS400系列,都是完全支持的。另外,Linux的开发环境支持也绝对是一流的,不管是C/C++,Java,Bash,Python,PHP,Javascript,。。。。。。就连C#也支持。而微软除Visual Stdio套件以外,都不怎么友好,不是吗? 如果你看完APUE的感触有很多,希望验证你的某些想法或经验,推荐UNIX程序设计艺术,世界顶级黑客将同你分享他的看法。 现在是时候做分流了。 大体上我分为四个方向:网络,图形,嵌入式,设备驱动。 如果选择网络,再细分,我对其他的不是他熟悉,只说服务器软件编写及高性能的并发程序编写吧。相对来说这是网络编程中技术含量最高的,也是底层的。需要很多的经验,看很多的书,做很多的项目。 我的看法是以下面的顺序来看书: APUE再深读 – 尤其是进程,线程,IPC,套接字 多核程序设计 - Pthread一定得吃透了,你很NB UNIX网络编程 – 卷一,卷二 TCP/IP网络详解 – 卷一 再看上面两本书时就该看了 5.TCP/IP 网络详解 – 卷二 我觉得看到卷二就差不多了,当然卷三看了更好,努力,争取看了 6.Lighttpd源代码 - 这个服务器也很有名了 7.Nginx源代码 – 相较于Apache,Nginx的源码较少,如果能看个大致,很NB。看源代码主要是要学习里面的套接字编程及并发控制,想想都激动。如果你有这些本事,可以试着往暴雪投简历,为他们写服务器后台,想一想全球的魔兽都运行在你的服务器软件上。 Linux内核 TCP/IP协议栈 – 深入了解TCP/IP的实现 如果你还喜欢驱动程序设计,可以看看更底层的协议,如链路层的,写什么路由器,网卡,网络设备的驱动及嵌入式系统软件应该也不成问题了。 当然一般的网络公司,就算百度级别的也该毫不犹豫的雇用你。只是看后面这些书需要时间与经验,所以35岁以前办到吧!跳槽到给你未来的地方! 图形方向,我觉得图形方向也是很有前途的,以下几个方面。 Opengl的工业及游戏开发,国外较成熟。 影视动画特效,如皮克斯,也是国外较成熟。 GPU计算技术,可以应用在浏览器网页渲染上,GPU计算资源利用上,由于开源的原因,有很多的文档程序可以参考。如果能进火狐开发,或google做浏览器开发,应该会很好 。 嵌入式方向:嵌入式方向没说的,Linux很重要。 掌握多个架构,不仅X86的,ARM的,单片机什么的也必须得懂。硬件不懂我预见你会死在半路上,我也想走嵌入式方向,但我觉得就学校教授嵌入式的方法,我连学电子的那帮学生都竞争不过。奉劝大家,一定得懂硬件再去做,如果走到嵌入式应用开发,只能祝你好运,不要碰上像Nokia,Hp这样的公司,否则你会很惨的。 驱动程序设计:软件开发周期是很长的,硬件不同,很快。每个月诞生那么多的新硬件,如何让他们在Linux上工作起来,这是你的工作。由于Linux的兼容性很好,如果不是太低层的驱动,基本C语言就可以搞定,系统架构的影响不大,因为有系统支持,你可能做些许更改就可以在ARM上使用PC的硬件了,所以做硬件驱动开发不像嵌入式,对硬件知识的要求很高。可以从事的方向也很多,如家电啊,特别是如索尼,日立,希捷,富士康这样的厂子,很稀缺的。 LDD – Linux驱动程序设计与内核编程的基础读物 深入理解Linux内核 – 进阶的 Linux源代码 – 永无止境的 当然你还的看个方面的书,如网络啊什么的。 学习linux内核的建议 学习linux内核,这个可不像学一门语言,c或者java一个月或者3月你就能精通掌握。学习linux内核是需要一步一步循序渐进,掌握正确的linux内核学习路线对学习至关重要,本篇文章就来分享学习linux内核的一些建议吧。 1. 了解操作系统基本概念。如果不会,可以学习《操作系统:设计与实现》Andrew S.Tanenbaum 写的那本。以MINIX为例子讲解操作系统的概念。非常推荐。 2. 有了操作系统的基本概念以后,可以了解Linux的机制了。推荐《Linux内核设计与实现》Robert Love 写的。这本书从概念上讲解了Linux有什么,他们是怎么运行的。这本书要反复认真看透。 3. 有了Linux内核的了解,还需要具体研究Linux内核源码。经典的就是《深入理解Linux内核》Daniel P. Bovet 写的。学习这本书的时候,要对着内核代码看着学。这本书学起来相当费力了,那么多多代码要研究。不过这本书如果学明白了,恭喜你,Linux内核你已经很熟悉了。 4. 如果要开发设备驱动,可以学习《linux设备驱动程序》O‘Reilly出版社的。这本作为驱动的入门是很好的资料。另外还有一本《精通Linux 驱动程序开发》也是不错的教材,可以参考着看。学习驱动,免不了要学习一些硬件的协议和资料,研究哪个就找到相应的硬件文档,把硬件的工作原理搞明白。这些就不细说了。 5. 网络部分,学些Linux网络部分就学习《深入理解LINUX网络技术内幕》。这本书把Linux的网络部分讲的非常清晰透彻。但是通常不做这方面的工作研究,也不用研究这么深,毕竟现在相关职位较少。 6. 现在Linux相关的工作,多集中在一些嵌入式开发领域,arm,mips等,要学习以下这些体系架构的的资料,了解CPU的设计和工作方式。ARM就看对应的芯片手册,讲的很细致。MIPS就看 《see mips run》,有一二两版,两版内容有些差异,推荐都看。 7. 补充一点经验。不要认为Linux很庞大,很复杂,就觉的很难学。任何东西认真学下来都是能学会的,看你都恒心和毅力了。另外,不要走弯路,不要看市面上讲什么Linux0.11的那些书,直接学你要学的东西。就像学C语言看什么谭浩强一样,弯路走了,力气没少花,还严重影响学习效果。 关于linux内核学习路线,再多说几句应用编程,有时候经常会需要的: 1. 学习Linux应用编程,建议看《unix环境高级编程》,把里面的例子都做一遍,会对整个Linux编程有系统都认识。 2. 针对Linux,有本 《Linux系统编程》,学完上一本,这本很快看一遍就懂了。主要是针对Linux具体懂一些内容,讲的挺全了,很实用。 3. Linux网络编程,系统的学习一下《unix网络编程。卷1,套接字联网api》,基本上网络应用相关的程序就都没问题了。 这些内容,分几年时间,分步计划学习,就会成为Linux高手了。个人建议参加华清远见的培训,学习效率会高很多,有目的性的参加培训,缩短周期,快速成型才是时代所需。 以上就是linux内核学习路线,关于学习linux内核的建议,希望对小伙伴们有帮助。

    时间:2020-09-23 关键词: Linux linux内核

  • 浅谈linux内核参数设置和功能

    linux内核的参数设置怎么弄呢,Linux 操作系统修改内核参数有以下三种方式: 修改 /etc/sysctl.conf 文件; 在文件中加入配置项,格式为 key = value,保存修改后的文件,执行命令 sysctl -p 加载新配置。 使用 sysctl 命令临时修改; 如:sysctl -w net.ipv4.tcp_mem = “379008 505344 758016”直接修改 /proc/sys/ 目录中的文件。 如:echo “379008 505344 758016” 》 /proc/sys/net/ipv4/tcp_mem注意:第一种方式在重启操作系统后自动永久生效;第二种和第三种方式在重启后失效。 内核参数 kernel.core_uses_pid = 1 core_uses_pid 可以控制 core 文件的文件名中是否添加 pid 作为扩展名。设置为1,表示添加 pid 作为扩展名,生成的 core 文件格式为 core.xxx;设置为0(默认),表示生成的 core 文件统一命名为 core。 kernel.core_pattern = core core_pattern 可以控制 core 文件的保存位置和文件格式。 如:kernel.core_pattern = “/corefile/core-%e-%p-%t”,表示将 core 文件统一生成到 /corefile 目录下,产生的文件名为 core-命令名-pid-时间戳。 以下是参数列表: %p - insert pid into filename 添加 pid %u - insert current uid into filename 添加当前 uid %g - insert current gid into filename 添加当前 gid %s - insert signal that caused the coredump into the filename 添加导致产生 core 的信号 %t - insert UNIX time that the coredump occurred into filename 添加 core 文件生成时的 unix 时间 %h - insert hostname where the coredump happened into filename 添加主机名 %e - insert coredumping executable name into filename 添加命令名 kernel.msgmax = 8192 进程间的消息传递是在内核的内存中进行的。msgmax 指定了消息队列中消息的最大值。(65536B=64KB) kernel.msgmnb = 16384 msgmnb 规定了一个消息队列的最大值,即一个消息队列的容量。msgmnb 控制可以使用的共享内存的总页数。Linux共享内存页大小为4KB,共享内存段的大小都是共享内存页大小的整数倍。一个共享内存段的最大大小是 16G,那么需要共享内存页数是16GB / 4KB = 16777216KB / 4KB = 4194304(页),也就是64Bit系统下16GB物理内存,设置kernel.shmall = 4194304才符合要求。 kernel.shmall = 1048576 表示在任何给定时刻,系统上可以使用的共享内存的总量(bytes)。 kernel.shmmax = 4294967295 表示内核所允许的最大共享内存段的大小(bytes)。用于定义单个共享内存段的最大值,64位 linux 系统,可取的最大值为物理内存值 - 1byte,建议值为多于物理内存的一半,一般取值大于SGA_MAX_SIZE即可,可以取物理内存-1byte。例如,如果为64GB物理内存,可取64 * 1024 * 1024 * 1024 - 1 = 68719476735。 实际可用最大共享内存段大小=shmmax * 98%,其中大约2%用于共享内存结构。可以通过设置 shmmax,然后执行ipcs -l来验证。 kernel.sysrq = 0 控制系统调试内核的功能,不同的值对应不同的功能: 0 完全禁用 sysrq 组合键 1 启用 sysrq 组合键的全部功能 2 启用控制台日志级别控制 4 启用键盘控制(SAK、unraw) 8 启用进程的调试信息输出等 16 启用同步命令 32 启用重新挂载为只读 64 启用进程信号(终止、杀死、溢出杀死) 128 允许重启/关机 256 控制实时任务的优先级控制(nicing) net.core.netdev_max_backlog = 262144 表示当每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许发送到队列的数据包的最大数目。 net.core.rmem_default = 8388608 为 TCP socket 预留用于接收缓冲的内存默认值。 net.core.rmem_max = 16777216 为 TCP socket 预留用于接收缓冲的内存最大值。 net.core.somaxconn = 128 listen(函数)的默认参数,挂起请求的最大数量限制。web 应用中 listen 函数的 backlog 默认会给我们内核参数的 net.core.somaxconn 限制到128。nginx 服务器中定义的 NGX_LISTEN_BACKLOG 默认为511。 net.core.wmem_default = 8388608 为 TCP socket 预留用于发送缓冲的内存默认值。 net.core.wmem_max= 16777216 为 TCP socket 预留用于发送缓冲的内存最大值。 net.ipv4.conf.all.accept_source_route = 0、net.ipv4.conf.default.accept_source_route = 0 处理无源路由的包。 net.ipv4.conf.all.rp_filter = 1、net.ipv4.conf.default.rp_filter = 1 开启反向路径过滤。 net.ipv4.ip_forward = 0、net.ipv4.conf.all.send_redirects = 0、v4.conf.default.send_redirects = 0 不充当路由器。 net.ipv4.icmp_echo_ignore_broadcasts = 1 避免放大攻击。 net.ipv4.icmp_ignore_bogus_error_responses = 1 开启恶意 ICMP 错误消息保护。 net.ipv4.ip_local_port_range = 1024 65535 增加系统 IP 端口限制。表示用于向外连接的端口范围。参考 net.ipv4.tcp_fin_TImeout = 30 如果套接字由本端要求关闭,这个参数决定了它保持在 FIN-WAIT-2 状态的时间。对端可以出错并永远不关闭连接,甚至意外宕机。缺省值是 60s,2.2 内核通常是 180s,你可以按这个设置,但要记住的是,即使你的机器是一个轻载的 WEB 服务器,也有因为大量的死套接字而内存溢出的风险,FIN-WAIT-2 的危险性比 FIN-WAIT-1 要小,因为它最多只能吃掉 1.5k 内存,但是它们的生存期长些。 net.ipv4.tcp_keepalive_TIme = 1200 表示当 keepalive 起作用的时候,TCP 发送 keepalive 消息的频度。(单位:秒,缺省值:2小时) net.ipv4.tcp_max_orphans = 3276800 表示系统中最多有多少个 TCP 套接字不被关联到任何一个用户文件句柄上。这个限制仅仅是为了防止简单的 DoS 攻击,不能郭飞依赖依靠它或者认为地减小这个值,如果增加了内存之后,更应该增加这个值。 net.ipv4.tcp_max_syn_backlog = 262144 记录的是那些尚未收到客户端确认信息的连接请求的最大值。对于有 128M 内存的系统而言,缺省值是 1024,小内存的系统则是 128。 net.ipv4.tcp_max_tw_buckets = 6000 表示系统同时保持 TIME_WAIT 套接字的最大数量,默认是 180000。 net.ipv4.tcp_mem = 94500000 915000000 927000000 确定 TCP 栈应该如何反映内存使用;每个值的单位都是内存页(通常是 4KB): 第一个值是内存使用的下限。 第二个值是内存压力模式开始对缓冲区使用应用压力的上限。 第三个值是内存上限。在这个层次上可以将报文丢弃,从而减少对内存的使用。对于较大的 BDP 可以增大这些值(但是要记住,其单位是内存页,而不是字节)。 net.ipv4.tcp_sack = 1 启用有选择的应答(1表示启用),通过有选择地应答乱序接收到的报文来提高性能,让发送者只发送丢失的报文段,(对于广域网通信来说)这个选项应该启用,但是会增加对CPU的占用。 net.ipv4.tcp_synack_retries = 1 为了打开对端的连接,内核需要发送一个 SYN 并附带一个回应前面一个 SYN 的 ACK。也就是所谓三次握手中的第二次握手。这个设置决定了内核放弃连接之前发送 SYN+ACK 包的数量。 net.ipv4.tcp_syncookies = 1 开启 SYN 洪水攻击保护。 net.ipv4.tcp_syn_retries = 1 在内核放弃建立连接之前发送 SYN 包的数量。 net.ipv4.tcp_TImestamps = 0 该参数用于设置时间戳,可以避免序列号的卷绕。一个1Gbps的链路肯定会遇到以前用过的序列号。时间戳能够让内核接受这种“异常”的数据包。设置为 0 表示将其关掉。 net.ipv4.tcp_tw_recycle = 0 是否开启 TCP 连接中 TIME-WAIT sockets 的快速回收,0 表示关闭,1 表示开启。当 tcp_tw_recycle 与 tcp_timestamp 同时开启时会降低连接成功率。 net.ipv4.tcp_tw_reuse = 1 是否开启重用,允许将 TIME-WAIT sockets 重新用于新的 TCP 连接,0 表示关闭,1 表示开启。 net.ipv4.tcp_window_scaling = 1 启用 RFC 1323 定义的 window scaling,要支持超过 64KB 的 TCP 窗口,必须启用该值(1表示启用),TCP窗口最大至 1GB,TCP 连接双方都启用时才生效。 net.ipv6.conf.all.disable_ipv6 = 1、net.ipv6.conf.default.disable_ipv6 = 1 禁用 IPv6 linux内核的功能有哪些 内核主要有以下4能:系统内存管理;软件程序管理;硬件管理;文件系统管理; (1)系统内存管理 内存管理是操作系统内核的主要功能之一。内核不仅能管理可用的物理内存,还可以创建并管理虚拟内存。 内存管理必须使用硬盘空间,该空间被称为交换空间。内核不断的在该交换空间和实际物理内存之间交换虚拟内存位置的内容。这样系统的可用内存比实际内存多。 将内存位置分组为多个数据块,此操作被称为分页。内核定位物理内存或交换空间中的每个内存分页,然后维护一个内存分页页表,此表说明位于物理内存的分页和交换到磁盘的分页。内存跟踪使用的分页,并且自动将一段时间没有用到的内存分页复制到交换空间区域,称为换出过程。即使内存够也执行这个过程。当程序需要访问已经换出的分页时,内存必须换出另一个分页,以在物理内存中为该内存分页腾出空间,然后从交换空间换入需要的分页。 在Linux系统上运行的每个进程都有自己的内存分页,一个进程不能访问另一个进程正在使用的分页。内核也有自己的内存区域,出于安全考虑,任何进程都不能访问内核进程正在使用的内存。 (2)软件程序管理 正在运行的程序被称为进程。进程可以在前台运行,也可以在后台运行。 内核创建的第一个进程被称为初始进程,该进程在系统上启动所有其它进程。内核启动的时候,初始进程被加载到虚拟内存中,内存每启动一个其它进程,都将在虚拟内存中为其分配一个唯一的空间,用于储存该进程的数据和代码。 (3)硬件管理 Linux系统需要与之通信的设备都必须在内核代码中插入驱动程序代码。驱动程序代码使内核能够向设备传输数据。在Linux中插入设备驱动的程序有两种方法: 1.在内核中编译驱动程序。 2.向内核添加驱动程序模块。 Linux将硬件设备标识为特殊文件,称为设备文件。设备文件分为3种: 1.字符 2.块 3.网络 字符设备文件用于那些一次仅处理一个字符的设备。块文件用于那些一次可处理大量数据块的设备。网络文件用于那些使用数据包发送和接收数据的设备。 (4)文件系统管理 Linux系统可以使用不同类型的文件系统与硬盘传输数据。除了本身的文件系统外,还可以通过其他的操作系统使用的文件系统传输数据。Linux内核使用虚拟文件系统与每个文件系统进行连接。为内核与其他文件系统类型的通信提供了一个标准接口,挂载和使用每个文件系统时。虚拟文件系统中缓存相关的信息。 以上的内容你了解了吗?

    时间:2020-09-23 关键词: Linux linux内核

  • Linux内核中的形形色色的“钟表”,你了解多少?

    如果Linux也是一个普通人的话,那么她的手腕上应该有十几块手表,包括:CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_PROCESS_CPUTIME_ID、CLOCK_THREAD_CPUTIME_ID、CLOCK_MONOTONIC_RAW、CLOCK_REALTIME_COARSE、CLOCK_MONOTONIC_COARSE、CLOCK_BOOTTIME、CLOCK_REALTIME_ALARM、CLOCK_BOOTTIME_ALARM、CLOCK_TAI。本文主要就是介绍Linux内核中的形形色色的“钟表”。 二、理解Linux中各种clock分类的基础 既然本文讲Linux中的计时工具,那么我们首先面对的就是“什么是时间?”,这个问题实在是太难回答了,因此我们这里就不正面回答了,我们只是从几个侧面来窥探时间的特性,而时间的本质就留给物理学家和哲学家思考吧。 1、如何度量时间 时间往往是和变化相关,因此人们往往喜欢使用有固定周期变化规律的运动行为来定义时间,于是人们把地球围自转一周的时间分成24份,每一份定义为一个小时,而一个小时被平均分成3600份,每一份就是1秒。然而,地球的运动周期不是那么稳定,怎么办?多测量几个,平均一下嘛。 虽然通过天体的运动定义了秒这样的基本的时间度量单位,但是,要想精确的表示时间,我们依赖一种有稳定的周期变化的现象。上一节我们说过了:地球围绕太阳运转不是一个稳定的周期现象,因此每次观察到的周期不是固定的(当然都大约是24小时的样子),用它来定义秒多少显得不是那么精准。科学家们发现铯133原子在能量跃迁时候辐射的电磁波的振荡频率非常的稳定(不要问我这是什么原理,我也不知道),因此被用来定义时间的基本单位:秒(或者称之为原子秒)。 2、Epoch 定义了时间单位,等于时间轴上有了刻度,虽然这条代表时间的直线我们不知道从何开始,最终去向何方,我们终归是可以把一个时间点映射到这条直线上了。甚至如果定义了原点,那么我们可以用一个数字(到原点的距离)来表示时间。 如果说定义时间的度量单位是技术活,那么定义时间轴的原点则完全是一个习惯问题。拿出你的手表,上面可以读出2017年5月10,23时17分28秒07毫秒……作为一个地球人,你选择了耶稣诞辰日做原点,讲真,这弱爆了。作为linuxer,你应该拥有这样的一块手表,从这个手表上只能看到一个从当前时间点到linux epoch的秒数和毫秒数。Linux epoch定义为1970-01-01 00:00:00 +0000 (UTC),后面的这个UTC非常非常重要,我们后面会描述。 除了wall time,linux系统中也需要了解系统自启动以来过去了多少的时间,这时候,我们可以把钟表的epoch调整成系统的启动时间点,这时候获取系统启动时间就很容易了,直接看这块钟表的读数即可。 3、时间调整 记得小的时候,每隔一段时间,老爸的手表总会慢上一分钟左右的时间,也是他总是在7点钟,新闻联播之前等待那校时的最后一响。一听到“刚才最后一响是北京时间7点整”中那最后“滴”的一声,老爸也把自己的手表调整成为7点整。对于linux系统,这个操作类似clock_set接口函数。 类似老爸机械表的时间调整,linux的时间也需要调整,机械表的发条和齿轮结构没有那么精准,计算机的晶振亦然。前面讲了,UTC的计时是基于原子钟的,但是来到Linux内核这个场景,我们难道要为我们的计算机安装一个原子钟来计时吗?当然可以,如果你足够有钱的话。我们一般人的计算机还是基于系统中的本地振荡器来计时的,虽然精度不理想,但是短时间内你也不会有太多的感觉。当然,人们往往是向往更精确的计时(有些场合也需要),因此就有了时间同步的概念(例如NTP(Network Time Protocol))。 所谓时间同步其实就是用一个精准的时间来调整本地的时间,具体的调整方式有两种,一种就是直接设定当前时间值,另外一种是采用了润物细无声的形式,对本地振荡器的输出进行矫正。第一种方法会导致时间轴上的时间会向前或者向后的跳跃,无法保证时间的连续性和单调性。第二种方法是对时间轴缓慢的调整(而不是直接设定),从而保证了连续性和单调性。 4、闰秒(leap second) 通过原子秒延展出来的时间轴就是TAI(International Atomic Time)clock。这块“表”不管日出、日落,机械的按照ce原子定义的那个秒在推进时间。冷冰冰的TAI clock虽然精准,但是对人类而言是不友好的,毕竟人还是生活在这颗蓝色星球上。而那些基于地球自转,公转周期的时间(例如GMT)虽然符合人类习惯,但是又不够精确。在这样的背景下,UTC(Coordinated Universal Time)被提出来了,它是TAI clock的基因(使用原子秒),但是又会适当的调整(leap second),满足人类生产和生活的需要。 OK,至此,我们了解了TAI和UTC两块表的情况,这两块表的发条是一样的,按照同样的时间滴答(tick,精准的根据原子频率定义的那个秒)来推动钟表的秒针的转动,唯一不同的是,UTC clock有一个调节器,在适当的时间,可以把秒针向前或者向后调整一秒。 TAI clock和UTC clock在1972年进行了对准(相差10秒),此后就各自独立运行了。在大部分的时间里,UTC clock跟随TAI clock,除了在适当的时间点,realtime clock会进行leap second的补偿。从1972年到2017年,已经有了27次leap second,因此TAI clock的读数已经比realtime clock(UTC时间)快了37秒。换句话说,TAI和UTC两块表其实可以抽象成一个时间轴,只不过它们之间有一个固定的偏移。在1972年,它们之间的offset是10秒,经过多年的运转,到了2017年,offset累计到37秒,让我静静等待下一个leap second到了的时刻吧。 5、计时范围 有一类特殊的clock称作秒表,启动后开始计时,中间可以暂停,可以恢复。我们可以通过这样的秒表来记录一个人睡眠的时间,当进入睡眠状态的时候,按下start按键开始计时,一旦醒来则按下stop,暂停计时。linux中也有这样的计时工具,用来计算一个进程或者线程的执行时间。 6、时间精度 时间是连续的吗?你眼中的世界是连续的吗?看到窗外清风吹拂的树叶的时候,你感觉每一个树叶的形态都被你捕捉到了。然而,未必,你看急速前进的汽车的轮胎的时候,感觉车轮是倒转的。为什么?其实这仅仅是因为我们的眼睛大约是每秒15~20帧的速度在采样这个世界,你看到的世界是离散的。算了,扯远了,我们姑且认为时间的连续的,但是Linux中的时间记录却不是连续的,我们可以用下面的图片表示: 系统在每个tick到来的时候都会更新系统时间(到linux epoch的秒以及纳秒值记录),当然,也有其他场景进行系统时间的更新,这里就不赘述了。因此,对于linux的时间而言,它是一些离散值,是一些时间采样点的值而已。当用户请求时间服务的时候,例如获取当前时间(上图中的红线),那么最近的那个Tick对应的时间采样点值再加上一个当前时间点到上一个tick的delta值就精准的定位了当前时间。不过,有些场合下,时间精度没有那么重要,直接获取上一个tick的时间值也基本是OK的,不需要校准那个delta也能满足需求。而且粗粒度的clock会带来performance的优势。 7、睡觉的时候时间会停止运作吗? 在现实世界提出这个问题会稍显可笑,鲁迅同学有一句名言:时间永是流逝,街市依旧太平。但是对于Linux系统中的clock,这个就有现实的意义了。比如说clock的一个重要的派生功能是创建timer(也就是说timer总是基于一个特定的clock运作)。在一个5秒的timer超期之前,系统先进入了suspend或者关机状态,这时候,5秒时间到达的时候,一般的timer都不会触发,因为底层的clock可能是基于一个free running counter的,在suspend或者关机状态的时候,这个HW counter都不再运作了,你如何期盼它能唤醒系统,来执行timer expired handler?但是用户还是有这方面的实际需求的,最简单的就是关机闹铃。怎么办?这就需要一个特别的clock,能够在suspend或者关机的时候,仍然可以运作,推动timer到期触发。 三、Linux下的各种clock总结 在linux系统中定义了如下的clock id: #define CLOCK_REALTIME 0 #define CLOCK_MONOTONIC 1 #define CLOCK_PROCESS_CPUTIME_ID 2 #define CLOCK_THREAD_CPUTIME_ID 3 #define CLOCK_MONOTONIC_RAW 4 #define CLOCK_REALTIME_COARSE 5 #define CLOCK_MONOTONIC_COARSE 6 #define CLOCK_BOOTTIME 7 #define CLOCK_REALTIME_ALARM 8 #define CLOCK_BOOTTIME_ALARM 9 #define CLOCK_SGI_CYCLE 10 /* Hardware specific */ #define CLOCK_TAI 11 CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID这两个clock是专门用来计算进程或者线程的执行时间的(用于性能剖析),一旦进程(线程)被切换出去,那么该进程(线程)的clock就会停下来。因此,这两种的clock都是per-process或者per-thread的,而其他的clock都是系统级别的。   leap second? clock set? clock tunning? original point resolution active in suspend? realtime yes yes yes Linux epoch ns no monotonic yes no yes Linux epoch ns no monotonic raw yes no no Linux epoch ns no realtime coarse yes yes yes Linux epoch tick no monotonic coarse yes no yes Linux epoch tick no boot time yes no yes machine start ns no realtime alarm yes yes yes Linux epoch ns yes boottime alarm yes no yes machine start ns yes tai no no no Linux epoch ns no

    时间:2020-09-22 关键词: Linux 钟表 linux内核

  • 浅谈Linux内核的同步机制

    在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实像多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问,尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问。 在主流的Linux内核中包含了如下这些同步机制包括: 原子操作 信号量(semaphore) 读写信号量(rw_semaphore) Spinlock Mutex BKL(Big Kernel Lock,只包含在2.4内核中,不讲) Rwlock brlock(只包含在2.4内核中,不讲) RCU(只包含在2.6内核及以后的版本中) seqlock(只包含在2.6内核及以后的版本中) 本文章分为两部分,这一章我们主要讨论原子操作,自旋锁,信号量和互斥锁。 一、原子操作 原子操作的概念来源于物理概念中的原子定义,指执行结束前不可分割(即不可打断)的操作,是最小的执行单位。 原子操作与硬件架构强相关,其API具体的定义均位于对应arch目录下的include/asm/atomic.h文件中,通过汇编语言实现,内核源码根目录下的include/asm-generic/atomic.h则抽象封装了API,该API最后分派的实现来自于arch目录下对应的代码。 Structure Definition typedefstruct{intcounter;}atomic_t; 原子操作主要用于实现资源计数, 许多引用计数(refcnt)就是通过原子操作实现,例如TCP/IP协议栈的IP碎片中,struct ipq中的refcnt字段,类型即为atomic_t。 atomic_add 原子操作的实现比较简单,以下为例。 原子操作的原子性依赖于ldrex与strex实现,ldrex读取数据时会进行独占标记,防止其他内核路径访问,直至调用strex完成写入后清除标记。自然strex也不能写入被别的内核路径独占的内存,若是写入失败则循环至成功写入。 API 原子操作的API包括如下, 以arm平台为例: 二 、自旋锁(spinlock) 自旋锁是这样一种同步机制:若自旋锁已被别的执行者保持,调用者就会原地循环等待并检查该锁的持有者是否已经释放锁(即进入自旋状态),若释放则调用者开始持有该锁。自旋锁持有期间不可被抢占。 Structure Definition 从定义出发, spinlock根本的实现依赖于具体架构实现中slock这个变量,由于spin_lock是大多locking机制的基础,我们看一看它的实现。 Lock & Unlock 核心unlock函数,使owner自增,保持数据同步。 核心lock函数,使slock +2^16, 当next==owner时,释放锁,否则进入循环等待。Prefetchw用于cache预加载数据。 由于slock与tickets共享同一块内存(union),slock 占32位4字节,tickets内部变量next与owner各16位2字节。以大端序为例,slock 高2字节与next共享,低2字节与owner共享,因此arch_spin_lock实际上是将tickets.next+1。假设初始时next与owner皆为0,此时next与owner不等,通过wfe指令进入一小段时间等待状态,而后读取新的owner值检查与next是否相等,不等则继续等待,相等则结束等待。 而owner的值由arch_spin_unlock控制,即unlock控制何时结束等待。 Spin_lock basic API Spin_lock API & irq 性能上,spin_lock > spin_lock_bh > spin_lock_irq > spin_lock_irqsave。 安全上,spin_lock_irqsave > spin_lock_irq > spin_lock_bh >spin_lock。 Spin_lock 不同版本的使用 spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效(spin_lock_irq)和软中断失效(spin_lock_bh)却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问。 如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问最好使用spin_lock_bh和spin_unlock_bh来保护。 如果被保护的共享资源只在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况相同,因为tasklet和timer是用软中断实现的。 如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。 如果被保护的共享资源只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源需要用spin_lock和spin_unlock来保护,因为同样的软中断可以同时在不同的CPU上运行。 如果被保护的共享资源在软中断(包括tasklet和timer)或进程上下文和硬中断上下文访问,那么在软中断或进程上下文访问期间,可能被硬中断打断,从而进入硬中断上下文对共享资源进行访问,因此,在进程或软中断上下文需要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。 在使用spin_lock_irq和spin_unlock_irq的情况下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具体应该使用哪一个也需要依情况而定,如果可以确信在对共享资源访问前中断是使能的,那么使用spin_lock_irq更好一些,因为它比spin_lock_irqsave要快一些。 三、信号量(Semaphore) Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它不可能在内核之外使用,因此它与System V的IPC机制信号量完全不同。 信号量是这样一种同步机制:信号量在创建时设置一个初始值count,用于表示当前可用的资源数。一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作为count-1,若当前count为负数,表明无法获得信号量,该任务必须挂起在该信号量的等待队列等待;若当前count为非负数,表示可获得信号量,因而可立刻访问被该信号量保护的共享资源。当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把count+1实现,如果count为非正数,表明有任务等待,它也唤醒所有等待该信号量的任务。 Structure Definition 可以发现,信号量是基于spinlock实现的,对其封装以满足高级的功能,例如全局共享资源的配置,并通过等待队列较为灵活的调度。信号量与接下来要讲的mutex都建立在自旋锁实现的执行同步上。 了解了信号量的结构与定义,我们来看看最核心的两个实现down ,up。 down & up down用于调用者获得信号量,若count大于0,说明资源可用,将其减一即可。 若count<0,将task加入等待队列,并进入等待队列,并进入调度循环等待,直至其被__up唤醒,或者因超时以被移除等待队列。 up用于调用者释放信号量,若waitlist为空,说明无等待任务,count+1,该信号量可用。 若waitlist非空,将task从等待队列移除,并唤醒该task,对应__down条件。 Semaphore API 四、互斥锁(Mutex) Linux 内核互斥锁是非常常用的同步机制,互斥锁是这样一种同步机制:在互斥锁中同时只能有一个任务可以访问该锁保护的共享资源,且释放锁和获得锁的调用方必须一致。因此在互斥锁中,除了对锁本身进行同步,对调用方(或称持有者)必须也进行同步。当互斥锁无法获得时,task会加入等待队列,直至可获得锁为止。 Structure Definition 互斥锁从结构上看与信号量十分类似,但将原本的int类型的count计数,改成了atomic_long_t的owner以便同步,保证释放者与持有者一致。 mutex_lock & mutex_unlock 上图简单的表现了mutex_lock与mutex_unlock实现的对称性,___mutex_trylock_fast用于owner为0的特殊状态,用于快速加锁,实现核心在slowpath版本上。 *might_sleep指在之后的代码执行中可能会sleep。 由于mutex实现的具体步骤相当复杂,这里选讲比较核心简单的两块。Mutex有关等待队列的处理比较复杂,有兴趣阅读相关内核书籍。 当且仅当lock当前的owner没有变化时(没有其他mutex抢先拥有该锁),此时获得锁,返回NULL, owner 为 curr | flags,owner本身对应task指针。若该锁已被占用,owner和当前task不匹配,返回owner对应指针。 当unlock时,不考虑等待队列的影响,则与上述类似,当且仅当之前持有锁的owner可以解锁,解锁时本来应将lock的owner置为初始0,但是这里保留了mutex的flag以便后续操作。 *这里的owner实际上是task_struct的指针,也就是地址,由于task_struct的地址是L1_cache对齐的,因此实际上指针地址后三位为0,因此linux内核利用这三个比特位用于设置mutex的标志位,不影响指针地址的表示也更高效利用了冗余的比特位。 Mutex 的改进 最初的互斥锁仅支持睡眠等待,然而经过漫长时间的改进,如今的互斥锁已经可以支持自旋等待,通过MCS锁机制实现。在内核中可以选择配置以支持,CONFIG_MUTEX_SPIN_ON_OWNER。 如上是4.9内核中mutex中常用有效的字段,目前最常用的算法是OSQ算法。自旋等待机制的核心原理是当发现持有者正在临界区执行并且没有其他优先级高的进程要被调度(need_resched)时,那么mutex当前所在进程认为该持有者很快会离开临界区并释放锁,此时mutex选择自旋等待,短时间的自旋等待显然比睡眠-唤醒开销小一些。 在实现上MCS保证了同一时间只有一个进程自旋等待持有者释放锁。MCS 的实现较为复杂,具体可参考一些内核书籍。MCS保证了不会存在多个cpu争用锁的情况,从而避免了多个CPU的cacheline颠簸从而降低系统性能的问题。 经过改进后,mutex的性能有了相当大的提高,相对信号量的实现要高效得多。因此我们尽量选用mutex。 Mutex 的使用条件 Mutex虽然高效,灵活,但存在若干限制条件,需要牢记: 同一时刻只有一条内核路径可以持有锁 只有锁持有者可以解锁 不允许递归加锁解锁 进程持有mutex时不可退出 Mutex 可能导致睡眠阻塞,不可用于中断处理与下半部使用 Mutex API

    时间:2020-09-22 关键词: Linux linux内核

  • “棱镜门”的反思 我国自主知识产权的手机OS诞生记

      “960”代表中国960万平方公里疆土,960寓意为中国信息安全保驾护航。960,乍一眼看去以为和周鸿祎的360有关系,其实不然。   昨日,由同洲电子潜心研发的,具有我国自主知识产权的智能手机操作系统在京发布。这款名为“960”的安全操作系统,是基于Linux内核的原生操作系统,而非基于Android。960 OS定位致力于保护手机信息安全,是目前除Android 、IOS 、Windows phone之后的一个全新手机操作系统。   960的来历   为何起名960?同洲董事长袁明对960操作系统的命名进行了如此解读。他表示,“960”代表中国960万平方公里疆土,960寓意为中国信息安全保驾护航。另外,“960”三个数字还分别代表该系统9年研发、6大安全手段和0风险。   中国工程院院士刘韵洁对此指出,960操作系统的推出是我国在智能通讯领域的重要突破,对我国移动互联网产业从终端软件系统底层建立信息安全机制,主动、有效地确保移动互联网体系与用户信息安全具有划时代的战略意义。   据悉,960os是基于Linux内核完全自主开发的原生操作系统,是国内具有自主知识产权的智能手机操作系统,能从底层监控并阻止通讯录、短信、文件、位置等信息窃取行为,能真正保障手机信息安全。   袁明进一步说明,如果手机操作系统尽被国外巨头垄断,国家和个人的信息安全都将受到严重威胁,事关国家信息安全的,更是马虎不得。另外,拥有自主产权的手机操作系统,对于提升中国移动互联网产业竞争力也是具有十分重要的意义。   此外,作为同洲集团新成员的全智达科技,其负责人李明携核心研发团队也进行了首次集体亮相。李明表示:“960 OS已是前后开发了15年,最近的这个版本也已开发了9年,是一直在不断进步的成熟的操作系统,也是中国近30年来,唯一一个具有关键知识产权的手机操作系统。” 他透露,在过去五年,该系统的研发一直得到国家发改委、工信部、科技部、财政部“核高基”项目的支持,是国家科技重大专项任务。   960手机即将问世   袁明还指出:“智能手机安全隐患无处不在,但痛心疾首的是99.9%的用户缺乏基本的安全意识,他们被各种灰色App绑架,他们的照片、短信、位置等隐私成为不良App的牟利来源,他们时刻面临着隐私与经济安全威胁却浑然不觉中,他们当中不乏军政要员、成功商人、公司高管、销售精英、娱乐明星等高阶人群。”   为此,同洲电子还发起一场名为 “手机斯诺登万名信息安全推广员招募”公益活动,活动号召网友吐槽手机使用过程中遇到的各类安全问题,以揭露不良厂商和不法分子的窥私行为,以此来提高国民警惕,防止损失。袁明还表示,“手机信息安全,关乎我们每一个人的生活,更影响着中国960万平方公里的今天与明天,只有每个人的信息都安全了,才能保障国家信息的安全”。   据了解,除了960 OS发布之外,同洲电子不久还将继续推出960安全手机,但产品具体的上市时间尚未透露。   曾报道同洲电子是国家北斗导航项目中标公司泰斗微电子股东之一。为此,业界曾猜测同洲960安全手机将很有可能搭载我国自行研制的全球卫星定位与通信系统北斗导航。   资料显示,“北斗”卫星导航系统是我国自主研发的卫星导航系统,其具有定位、通信功能,是继美国的GPS、俄罗斯的GLONASS之后,全球第三个成熟的卫星导航系统,它打破了美国和俄罗斯在这一领域的长期垄断地位。   据目前国内市场上公开销售的手机中还没有基于北斗导航的手机,绝大多数使用的是美国的GPS,也正因此,美国政府掌握每个人的位置信息变得轻而易举。   业内人士对同洲电子此举有着截然不同的两种看法。一种是具有我国自主知识产权的系统并不是头一次听说,能否做出成绩来不仅仅是口号和有政府支持问题,还需要完整的生态链规划,同洲电子要走的路还很长。   另一种则表明,美国“棱镜门”事件折射出一个“只要有利益驱动,数据泄露和信息攻击便无所不在,如应对不当,可能会给行业乃至于国家安全带来不利影响”的不争事实。在“信息战”愈演愈烈的今天,信息安全成为国家最关心的问题,我国需要有这样的企业身先士卒,成功与否先做出来才知道,一个民营企业为了社会的责任、为了国家的利益,决心挑起这样一个艰巨的任务,至少应该鼓励。

    时间:2020-09-03 关键词: 信息安全 棱镜门 linux内核

  • 微软向物联网靠近 定制 Linux内核版问世

    开放还是封闭这对微软曾经是一个问题,但后来,微软一次又一次向开源靠拢,像是两条腿走路,稳固原有的商业模式同时,紧跟时代步伐。如今,微软再次发布定制版Linux内核,靠近物联网。 年前,微软的Azure云平台就多次向linux系统靠拢,表明了在全新的云领域,微软的开放心态。如今物联网发展的如火如荼,作为未来三到五年最具有发展前景的行业之一,微软再次抱着开放的心态来袭。在旧金山举行的新闻发布会上,微软宣布了针对物联网设备的解决方案 Azure Sphere。 据了解,Azure Sphere 包含三个组件:Sphere MCU、Azure Sphere IoT OS和 提供安全云服务的Azure Sphere Security Service。 其中,Sphere MCU将免费提供给厂商,联发科将在今年晚些时候推出首款产品 MT3620,微软称它的芯片具有 ARM Cortex A 系列芯片设计的通用性和处理能力,同时更小开销更低。 事实上,从微软与联发科的合作我们能够发现,微软正在建构一个由多家芯片供应商、ODM、OEM 共同组成的全新生态系统。

    时间:2020-07-30 关键词: 微软 物联网 linux内核

  • 2019年,我学习Linux的心得

     在2019年,有很多新事物:农药、吃鸡、抖音、头条等词,我想每个人都知道,在这个互联网世界,即使您没有玩过,也会听过。那么,在过去的一年中,您的娱乐时间甚至在学习上的时间是否也花在这些娱乐上了呢?如果是这样!找到一个安静的地方,花几分钟回顾过去的一年您是否感到空虚?不记得自己拥有什么,赢得了什么?还有我一个普通的学生在学校并不认真,但是这并不是一件辛苦的事,因为在我学到的东西中,我总是感到非常满足,但是我对学校的理论知识不感兴趣。中国教科书更喜欢讲理论,讲那些令人费解的专业名词,让人看不懂,好让您知道不是所有的人都能胜任这门课的教学。当然这只是个玩笑。 如果你要用这些东西有用吗?我可以明确的告诉你,肯定是有用的啊!数据结构、计算机原理、操作系统原理、计算机网路、编程语言,哪一项不是现今主流技术的理论基石?因为是基石,基石肯定由理论组成,那为什么理论这么难懂呢?我私自认为,理论嘛。用一句话形容的话,可以这么说,实践的高度抽象的总结,我觉得就是理论。因为是高度抽象的,不是我们生活中能见到的,所以具体的理解你得靠自己去想,所以这就难了,因为你没有实践过呗,所以看不懂很正常,不要过度纠结,我曾经就十分过度纠结这些理论,认为自己有些地方没完全懂,我就不想去学了,其实完全没这个必要。你又不是专门搞理论研究的那一套的,所以没必要拘泥于细节,有个大概印象就行了。但是,学习linux不一样,你可以边实践边看理论,在实践中学理论,你会发现学习linux命令如此简单。 学一门东西,首先你要学会问自己Why? 有一种东西他是免费的,有什么比免费的东西更让人欣喜吗?所以你不用花一分钱去买这个东西,让你节约大笔的金钱成本; 有一种东西他是开放的,全世界的爱好计算机的都在讨论他,他有着广泛的社区,你可以尽情的把自己的问题提出来,你也可以去解决他人的疑惑,享受帮助他人的感觉。 有一种东西他是开源的,开源意味着什么?意味着这个系统的完善,每个人都能加入其中贡献自己的一份力,意味着你可以根据自己的需求,修改其中的源代码,按照你想要的方式去服务与你。所以这个系统将会是越来越好用! 这种东西就是linux,并且他还是未来发展的趋势,如今我们已几乎全面挺近信息时代,你手中各种智能电子产品,都离不开linux。而如今,炒的火热的云计算、大数据、人工智能同样也离不开linux。所以,你还有什么理由不去学这门牛B的技术! 给自己规定一个学习路线How? 说实话,看别人的学习路线每个人都有其不同态度,有的人觉得好,有的人觉得不好。你如果你按照他说的,你真真切切的坚持学了下去,想必肯定有收获,那你就认为他说的好。所以吧,制定一个你能坚持学下去的学习路线才是重要的! 那我就说说我的学习路线吧! 选择一本指导书是必不可少的,我推荐《Linux就该这么学》,这本书很适合初学者,里面的文字通俗易懂,省去了很多不必要的废话,非常适合新手入门。当然任何一门书都不可能直接教会所有人,否则还要老师干嘛呢!书中必然有疑难点,对于这些疑难点,你需要做的就是找一些linux相关的视频讲解,去听老师的口头叙述,或许你就豁然开朗呢!当你把书中所有知识点都掌握了,我认为你的linux水平就可以了。 我认为彻底看懂一本书就可以呢,俗话说:“贪多嚼不烂”是有道理的!如果你搞那么多资料,不是所以人都是天才,大部分的人都是普通人,而我们的学习时间又是很有限的,你弄那么多资料,没有完成当初制定的目标,只会徒增你的焦虑、失败。所以我们应该找一条我们能够坚持下去的路线,这才是最明智的! 找一个你大展手脚的方向Where? 不忘初心,方得始终。所以,不管我们知识面多么有限,记得给自己一个明确的、可度量的目标。以下是我所理解的几个方向,希望对你有所帮助! Linux内核开发:这个就比较有难度,一看到内核,无疑是操作系统中最难的,所以你如果是一个热爱静静思考的人,想做一个别人眼中当之无愧的大神。这个,你可以试试! Linux嵌入式开发:这个了也有难度,软件硬件你得通吃才行,现在人们追求的智能化,所以往后智能化产品肯定会蜂拥而至,而智能化产品就是学这个开发的,所以,学好这个,工作不愁,高薪不愁! Linux运维:这个嘛,虽说起点低,但是终点很远,入了这个行,你以后的选择可以更多,因为这个职业的特殊性要求你掌握的东西多,无论是技术还是人情世故,你都得学。所以,这一行在计算机界当老板的也是最多的! 还有就是,如今炒得火热的云计算、虚拟化、大数据、人工智能等等,其实或多或少都有linux的身影。 所以,学完linux后施展手脚的可选方向还是很多的,但是,你只有不断的保持热情坚持的去学习和深入,你才有可能在其中的一个方向有所突破和发展。 未来一年,祝愿大家在学习linux方面取得新收获。

    时间:2019-11-19 关键词: Linux linux嵌入式 linux运维 linux内核

  • linux内核分析之虚拟文件系统

    本文主要是介绍一下linux内核的虚拟文件系统(VFS)。虚拟文件系统(VFS,virtual filesystem),是一个内核软件层,是物理文件系统与服务之间的一个接口层,它对Linux的每个文件系统的所有细节进行抽象,使得不同的文件系统在Linux核心以及系统中运行的其他进程看来,都是相同的。  严格说来,VFS并不是一种实际的文件系统。它只存在于内存中,不存在于任何外存空间。VFS在系统启动时建立,在系统关闭时消亡。 Linux中系统可以使用连接成单一树形结构的不同文件系统。VFS可以无缝地使用多个不同的文件系统,通过VFS可以访问文件系统的系统调用提供一个统一的抽象接口,支持多种文件系统。   VFS的作用是采用标准的UNIX系统调用读/写位于不同物理介质上的不同文件系统,方便的在不同文件系统间进行数据交换和管理。  Linux系统中,VFS使用4个主要对象:超级块、索引节点、目录项、文件对象。每个对象的数据结构中不仅包含了属性还包含了对应的操作。   1、超级块  超级块用于存储已安装文件系统的控制信息的数据结构,代表已安装的文件系统,一个安装实例和一个超级块是一一对应关系。超级块描述文件系统的状态、文件系统类型、大小、区块数、索引节点数等,存放在磁盘的特定扇区中。  超级块使用结构体super_block表示。  (1)变量s_list指向超级块链表的指针,同一时间内核会拥有好几个文件系统的超级块,内核采用双向循环链表的方式来组织这些超级块。  (2)变量s_root指针指向目录项的根目录。  2、索引节点  存储文件的元数据的一种结构,文件的元数据就是文件的相关信息,和文件本身是两种概念。它包含诸如文件大小、拥有者、创建时间、磁盘位置等和文件相关的信息。  每个索引节点都有一个索引节点号,唯一的标识文件系统中的文件。索引节点在文件的整个生命周期都存在。 索引节点使用结构体inode来表示。  (1)变量i_hash包含了一个指向哈希链表的指针,哈希表加速索引节点的查找。   (2)i_ino保存一个索引号,唯一标识。  (3)变量i_sb指针指向文件所驻留的文件系统的超级块。 3、目录项(dentry) 存放目录项(文件的特定名称)与对应文件进行链接的有关信息。在一个文件路径中,路径中的每一部分都被称为目录,如/home/source/hello.c中,目录/,home,source和文件hello.c都是一个目录项。   目录项使用结构体struct dentry来表示。  4、文件对象  文件对象表示进程已打开的一个文件,它是已打开文件在内存中的表示。多进程可能同时打开同一文件,内存中可能存在多个与同一文件对应的文件对象,但与该文件对应的索引节点和目录项是唯一的。   文件对象使用结构体struct file来表示。    5、四者的关系 

    时间:2019-11-12 关键词: Linux 虚拟文件系统 linux内核

  • linux内核解析之--内存管理

    本文主要介绍了linux内核的内存管理机制。什么是内存管理机制?内存管理主要负责完成当进程请求内存时给进程分配可用的内存,当进程释放内存时,回收相应的内存,同时负责跟踪系统中相应内存的使用状态。   Linux采用页式内存管理,页是物理内存管理的基本单位。但严格来说Linux采用的是段页式内存管理,既分段也分页。内存映射的时候,先确定对应的段,确定段基地址,段内分页,再找到对应的页表项,确定页基地址,再由逻辑地址的低位确定的页偏移量就能找到最终的物理地址。但Linux中的所有段地址都是0,即所有的段是相同的,之所以有段的概念是因为Linux为了符合硬件体系。所以Linux实际采用的是页式内存管理,但段的概念在内核中确实存在。  1、物理内存的管理   Linux中首先将内存分为若干个节点,每个节点下面又可分为1~3个区,每个区下面会有若干个页。   (1)节点  内存节点主要是依据CPU访问代价不同而划分的。一个CPU对应一个节点。内核数组node_data[]形式组织节点,存储的为struct page_data_t指针来描述内存分区。  (2)区 内核以struct_zone来描述内存分区。内核将所有的物理页分为3个区:ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。  ZONE_DMA区中包含的页可以用来进行DMA操作,即直接内存访问操作,通常为物理内存的起始16M。ZONE_NORMAL区包含的页是可以进行正常的内存映射的页物理内存为16~896M。ZONE_HIGHMEM区称为“高端内存”,该区所包含的页不可以进行永久映射,即不可以永久映射到内核地址,物理内存896M以后的。  高端内存的边界为896M的原因:32为Linux系统中虚拟内存空间为0-4G,3G-4G为内核态。为了应对内核映射超过1G,Linux采取的策略:内核地址空间的896M采用固定映射,映射方法:虚拟地址-3G=物理地址,只能映射896M,即3G~3G+896M,剩余的128M(3G+896M~4G)采用动态映射。  Linux下以struct zone结构体来表示一个区,在该结构体中变量struct page *zone_mem_map用来管理该区下的内存映射表。  (3)页  每一个物理页框都使用一个数据结构struct page来描述,该结构体中的lru变量构建用于LRU页面置换的链表。在页框空闲情况下,该成员变量用于构建伙伴算法、链表同等大小的空闲内存块。大多数32bit的操作系统的页大小为4KB。  2、伙伴算法  Linux采用的是伙伴(Buddy)算法对物理内存进行管理。伙伴机制是操作系统的一种动态存储管理算法,该算法通过不断平分较大的空闲内存块来获得较小的空闲内存块,直到获得所需的内存块。当内存释放时,该算法尽可能的合并空闲块。该算法要求内存块的分配和合并都是以2的幂次方为单位。  在“区”内存结构体struct zone中有一struct free_area类型的数组free_area[],数组最大为12个元素。数组的下标k对应着固定大小2^k个页框空闲内存区域的双向链表头。当需要空闲块为4(即2^2)个页框时则查找free_area[2],如果没有合适的,则查找free_area[3],直到找到合适的。 3、slab分配器  Linux中引入slab是为了减少对伙伴算法的调用,采用slab分配器来减少频繁分配和释放内存数据结构的开销,同时减少了碎片的产生。slab分配机制是基于伙伴算法之上实现。  slab是基于一组对象缓存,把不同对象划分为caches(物理内存),每个cache保存一种类型的对象,每个cache由一个或者多个slab组成,每个slab包含一个或者多个page组成。  每个slab处于3中状态之一,即full、partial和empty(分别是满、部分满、空),其中满状态的slab没有任何可分配的空闲对象。当请求空闲对象时则从部分满和空的slab中分配。   Linux内核中的cache以结构体kmem_cache_s来表示,结构体中变量lists中存储的为三个链表分别对应于slab的三种状态。  总结来说,当为一对象申请内存时,首先查找到该对象的cache,然后查找cache中的slab列表,分配空闲内存。当释放该对象内存时,则返回给该对象对应的slab。这样伙伴算法就不需要频繁的进行分配和合并操作。  4、虚拟内存  (1)逻辑地址->线性地址->物理地址的转换过程  逻辑地址即程序指令的地址,线性地址指页式转换前的地址(虚拟地址),物理地址则是物理内存中的地址。  一个逻辑地址由两部份组成,段标识符: 段内偏移量。段基址确定它所在的段居于整个存储空间的位置,偏移量确定它在段内的位置。Linux中由于段基址都是0,所以逻辑地址和线性地址相同。线性地址再通过MMU进行转换到物理地址,这个过程下面重点讲下。  Linux也是内存管理使用三级表结构:页目录、页中间目录、页表。一个活动任务都有一个页目录,大小一般为一页,页目录必须在内存中。页中间目录可以跨越多个页。页表同样可以跨越多个页,对应具体的页框。具体过程如下图: (2)页面置换算法  Linux中页结构体的组织方式为双向循环链表。Linux中页面置换算法基于时钟算法机制实现,页结构体page中有一变量count专门用来计算页面被引用的次数。每当页面被访问一次时,count加1。在Linux后台,Linux周期性地扫描全局页池,并且当它在内存中的所有页间循环时,将扫描的每一页的count减1。age越大则使用频率越高。最终内核通过最近未使用(LRU)算法进行页面置换。 5、高速缓存  Linux使用了一系列的高速缓存相关的内存管理技术来提高性能。此处的高速缓存并非 是物理缓存,而是软件方法。Linux中主要包括以下几个缓存:   (1)Buffer Cache,包括了用于块设备驱动程序的数据缓冲区。这些缓存区固定(一般512B),包括从块设备要读取的数据和要写入块设备的数据。操作时先查看缓冲区。   (2)Page Cache,用来加快对磁盘上映像和数据的访问。用来缓存文件的逻辑内容,一次缓存一页。   (3)Swap Cache,只有改动过的(或脏)页才存在交换文件中,只要交换文件没有再次修改,下次这些页需要交换出时就不需要再写到交换文件中。     (4)Hardware Cache,常见方法是在处理器中PTE的高速缓存。这种情下处理器不需要直接读取页表,需要时把页表放在缓存区中。CPU有转换表缓冲区(TLB),来快速查找置换页表。

    时间:2019-11-12 关键词: Linux 内存管理 逻辑地址 linux内核

  • linux内核解析之内核同步机制

      本文讲述了linux内核中常见的同步机制,使读者掌握每处理器变量和rcu这两种新的同步机制。 内核同步机制  内核同步主要是同步各执行单元对共享数据的访问,尤其是多处理器的同步。 Linux2.6中内核同步机制主要包括以下几种:原子操作、信号量(semaphore)、读写信号量(rw_semaphore)、自旋锁(spinlock)、大内核锁(BKL)等。  (1)原子操作     原子操作就是指某一个操作在执行过程中不可以被打断,要么全部执行,要不就一点也不执行。原子操作需要硬件的支持,与体系结构相关,使用汇编语言实现。  原子操作主要用于实现资源计数,很多引用计数就是通过原子操作实现。Linux中提供了两种原子操作接口,分别是原子整数操作和原子位操作。 原子整数操作只对atomic_t类型的数据进行操作,不能对C语言的int进行操作,使用atomic_t只能将其作为24位数据处理,主要是在SPARC体系结构中int的低8为中设置了一个锁,避免对原子类型数据的并发访问。  原子位操作是针对由指针变量指定的任意一块内存区域的位序列的某一位进行操作。它只是针对普通指针的操作,不需要定义一个与该操作相对应的数据类型。  (2)自旋锁   Linux自旋锁保证了任意时刻只能有一个执行线程进入临界区,其他试图进入临界区的线程将一直进行尝试(即自旋),直到获得该锁。自旋锁主要应用在加锁时间不长并且不会睡眠的情况。   自旋锁的本质是对内存区域的一个整数的操作,任何线程进入临界区之前都必须检查该整数,可用则进入,都则一直忙循环等待。  自旋锁机制让试图获得该锁的线程一直进行忙循环(占用CPU),因此自旋锁适合于断时间内进行轻量级加锁。而且自旋锁绝对不可以递归使用,否则会被自己锁死。  Linux自旋锁主要应用与多核处理器中,单CPU中不会进行自旋锁操作。 linux上的自旋锁有三种实现:  a. 在单cpu,不可抢占内核中,自旋锁为空操作。  b. 在单cpu,可抢占内核中,自旋锁实现为“禁止内核抢占”,并不实现“自旋”。  c. 在多cpu,可抢占内核中,自旋锁实现为“禁止内核抢占” + “自旋”。 其中,禁止内核抢占只是关闭“可抢占标志”,而不是禁止进程切换。显式使用schedule或进程阻塞(此也会导致调用schedule)时,还是会发生进程调度的。  (3)读/写自旋锁  Linux中规定,读/写自旋锁允许多个线程同时以只读的方式访问临界资源,只有当一个线程想更新数据时,才会互斥访问资源。读写自旋锁包括一个24位读者计数和一个解锁标记来实现的。  (4)信号量   Linux中提供了两种信号量:  a. 内核信号量,由内核程序使用  b. System V IPC 信号量,由用户进程使用  当一个线程去请求以不可用的信号量时,和自旋锁不同,该进程会进入睡眠(不再占用CPU),加入到等待队列中,直到被唤醒,所以只有可睡眠的状态才可以使用信号量。 信号量实现的结构体semphore中有一变量count计数。根据count取值的设定,信号量可以分为二元信号量和计数信号量,当count初值为1时,则为二元信号量。计数信号量允许任意数量的锁持有者,这点和自旋锁是不同的(自旋锁只允许一个)。  (5)读/写信号量  读写信号量实际上对于读者使用的是一个计数信号量,写者使用的是二元信号量。读写信号量同读写自旋锁一样提高了内核的并发度。  Linux内核时按照先进先出(FIFO)的顺序来处理等待读写信号量的进程。具体过程是如果一个进程试图获取一个不可用的信号量时,加入到等待队列的末尾,当信号量可用时,内核首先唤醒等待队列的第一个进程,如果该进程为写进程,那么该进程获得信号量。如果该进程如果为一个读进程,那么其后的所有的读进程都可以被唤醒并获得信号量,但是中间不能跳跃。  (6)BKL(Big Kernel Lock)  BKL即全局内核锁,也称大内核锁,它是一个全局自旋锁。大内核锁也是用来保护临界区资源的,避免出现多个处理器上的进程同时访问同一区域,整个内核中只有一个大内核锁。  BKL是一个名为kernel_flag的自旋锁,持有该锁的进程仍可以睡眠,当睡眠时持有的锁将被自动释放,该进程被唤醒时重新持有该锁。Linux允许一个进程可以递归的持有BKL,BKL是一个递归锁。  它的设计思想是,一旦某个内核路径获取了这把锁,那么其他所有的内核路径都不能再获取到这把锁。 自旋锁加锁的对象一般是一个全局变量,大内核锁加锁的对象是一段代码,里面可能包含多个全局变量。 那么他带来的问题是,虽然A只需要互斥访问全局变量a,但附带锁了全局变量b,从而导致B不能访问b了  (7)屏障  屏障或称内存屏障,是用来解决内存同步问题的,具体为对由于编译器的优化和缓存的使用,导致对内存的写入操作不能及时的反应出来,也就是说当完成对内存的写入操作之后,读取出来的可能是旧的内容的一种解决机制。  内存屏障分类:  a.编译器引起的内存屏障 b.缓存引起的内存屏障 c.乱序执行引起的内存屏障

    时间:2019-11-12 关键词: Linux 内核同步机制 linux内核

  • linux内核解析--中断及异常处理

      Linux是一种开源电脑操作系统内核。它是一个用C语言写成,符合POSIX标准的类Unix操作系统。本文小编带你了解一下linux内核的中断及异常处理的基本内容。 一、系统调用  Linux的每个系统调用都是通过一些宏、一张系统调用表、一个系统调用入口来完成。  (1)宏  Linux为每个系统调用定义了一个唯一的编号,成为系统调用号。通过宏定义方式定义,例如#define  __NR_setup 0。  Linux中系统调用号一旦分配就不可以再进行更改,否则已经编译好的木块将不能正常使用。即使删除的系统调用,也不可以把之前已经分配的系统调用号重新分配,删除的系统调用有相应的空处理。  (2)系统调用表  系统调用表是一个函数指针数组,跳转时以系统调用号作为数组下表,找到相应的函数指针。  (3)系统调用入口  系统调用入口其实是由系统调用入口函数实现。功能是将系统调用号放入eax寄存器后移用int $0x80使处理器转向系统调用入口,查找系统调用表,进而执行内核调用真正的函数。  Linux系统调用实际是软中断。系统调用过程中,Linux首先通过执行相应的机器代码指令int $0x80产生一个软中断的异常处理信号,使系统自动从用户态切换到内核态。  二、中断机制  Linux中断主要分为硬中断(IRQ)和软中断两类。  IRQ主要分为:短类型IRQ和长类型IRQ。短类型IRQ需要很短的时间,在此期间机器的其他部分被锁定,而且不能发生其他中断被处理。长类型IRQ需要较长的时间,期间可能发生其他中断。  当用户程序被来自外部信号中断后,立即保存现场工作,包括保存返回地址和用户寄存器等数据,然后查找中断向量表,找出相应的中断处理程序。系统将中断分为三种:捕俘、系统调用和外中断。捕俘:通过捕俘处理程序入口表查找到用户编写的处理程序执行。系统调用:软中断,通过系统调用表找到操作系统核心提供的服务例程。外中断:直接调用核心提供的外中断处理程序运行。  1、硬中断过程  Linux中,若一个硬件想向CPU发送中断信号,必须首先获得一个可用的“中断请求线”(即中断前必须获得一个可用的IRQ号),产生一个中断信号后以电信号发送给中断控制器 (硬件芯片),接着CPU根据中断控制器的状态位判定中断的来源,获得中断号,根据中断号查找中断向量表,从表中获得中断处理函数的地址,然后跳转到中断函数入口地址处,执行这个函数。 2、中断处理程序—硬中断  中断处理程序主要做的工作:  a. 保护未被硬件保护的一些必须的寄存器 b. 识别各个中断源,分析产生中断的原因 c. 处理发生的中断事件 d. 恢复正常的工作  Linux规定中断处理程序是不可重入的,指的是同一中断线上不可以再发生新的中断,因为所有的处理器都将原中断所在的中断线已经屏蔽。  Linux中同样规定了同一中断程序不能够并行,这样同一个中断处理程序不可以被同时调用来处理嵌套的中断。 Linux中将中断处理程序分为两部分:上半部和下半部。   上半部主要用来处理那些具有严格时限要求的任务。上半部可以看做是一个用来“登记中断”功能的函数,将中断例程的下半部挂到下半部执行队列中。上半部要求执行很快,主要是因为上半部完全屏蔽中断下执行,即不可中断。  下半部主要用于处理那些可以稍后执行的任务。下半部是可中断的,当发生其他中断时,下半部可中断等待另外一个中断的上半部执行完毕后再继续执行。  3、下半部机制  Linux中提供了三种机制来实现下半部机制。  (1)软中断  软中断是一组静态定义的下半部结构,使用数组来组织软中断结构体,共有32个。两个相同的软中断可以同时执行,必须在编译期间进行静态注册。  软中断机制一般都保留给系统中对时间要求最严格以及重要的下半部来使用。Linux2.6中只有两个子系统是通过软中断来实现的:网络子系统和SCSI。  (2)tasklet   tasklet要比软中断机制方便且简单,而且它本身也是基于软中断实现,属于软中断,既可以静态的创建tasklet,也可以动态的创建tasklet。   Linux中tasklet分为两类:HI_SOFTIRQ和TASKLET_IRQ,前者比后者的优先级要高,优先调用前者。在中断数组irq_desc[]中会分配两项给tasklet,即两种类型各占数组中一项。两者分别以一个链表来组织。  (3)工作队列(work queue)   工作队列与前两者最大的不同之处是它是唯一一个能在进程上下文中运行的下半部机制,意味着它能允许睡眠。  工作队列的实质是将推后的工作交给一个内核线程来完成,核心思想即时创建一个内核线程,Linux中已经默认提供了一种命名为enents一类工作者线程来实现工作队列。  4、中断的数据结构  Linux内核中定义了一个数组irq_desc[]数组来管理中断。数组中的每一项对应一个中断源。数组中的每个成员都为irq_desc_t结构体,即数组中的每一项对应着中断向量表中的一项。  (1)irq_desc_t结构体  irq_desc_t结构体用来描述中断源。其中结构体中的handler指向hw_interrupt_type结构体的指针,action变量指向由irqaction结构体组成的单向链表的头的指针。  (2)irqaction结构体  该结构体中指明内核接收到特定IRQ后该才去的动作。结构体中变量handler指向中断处理程序。  (3)hw_interrupt_type结构体    用来描述中断控制器,是一个抽象的中断控制器。 5、中断上下文  当一个中断处理程序正在执行时,内核处于中断上下文中。中断上下文是不可以睡眠的。与进程上下文是不同的,进程上下文即使睡眠了也可以重新调度将其唤醒,中断上下文不可以被重新调度。    中断处理程序没有自己的堆栈,它会共享被它中断的那个进程的堆栈,如果没有进程正在执行,则占用idle进程的堆栈(每个处理器都有自己的运行队列,队列中都有idle进程,当前运行队列都dequeue时则运行idle进程)。

    时间:2019-11-12 关键词: Linux 中断 异常处理 linux内核

  • ALSA 编程:入门篇

    1、GNU/Linux 系统下三大主流声卡驱动程序集 Linux 有三个主流的声卡驱动程序集:OSS/Lite(也称为OSS/Free)、OSS/Full (商业软件)、ALSA(自由软件)。 OSS/Lite 是现在linux内核中自带的声卡驱动程序集,最初由 Hannu Savolainen 开发。后来 Hannu 跑去开发 Open Sound System(也就是上面所说的OSS/Full)。 由于 Hannu 的“逃跑”,RH 资助 Alan Cox 增强 Hannu 开发的驱动程序,并使它们 完全模块化。现在 Alan Cox 是内核声卡驱程集的维护人。OSS/Lite 从kernel-2.0开 始并入内核,现在大家使用的声卡驱程默认都是OSS/Lite中的。 OSS/Full 是由 4Front Technologies 开发并销售的商业软件。它可以驱动很多 声卡并且可以用在很多 UNIX 系统中。OSS/Full 完全兼容以前基于 OSS/Lite 开发 的应用程序。作为一个商业软件,你虽然可以使用它,但是你得不到它的源代码,并且 你必须为此而付钱。 ALSA 是linux内核的下一代标准声卡驱动集。开始时 Jaroslav Kysela 等人为 Gravis UltraSound Card 开发驱动程序,后来该计划改名为 ALSA「先进的linux音频 体系」,因为他们认为 ALSA 比原来内核中的 OSS/Lite 驱动程序集更优秀,完全可以 代替 OSS/Lite。他们是对的,ALSA 支持的声卡比 OSS/Lite 多,完全兼容以前基于 OSS 开发的程序,SMP(多处理器) 与 线程安全设计,并且从 2.5 分支的内核开始, ALSA 的驱动程序集开始并入内核,大家可以在今年的 2.6 版本的内核中看到使用它们。 2、为什么我要使用 ALSA 开发音频程序 首先,ALSA 是 linux 以后声卡驱动程序的标准,OSS/Lite 迟早会从内核中去除。 开发基于 ALSA 的音频程序可以保证以后的兼容。 其次,我们简单比较一下开发基于 OSS 与 ALSA 的方法。 OSS 向应用程序提供了一系列的系统接口。开发基于 OSS 的应用程序需要使用 open,close,ioctl,read,write 等低级系统调用来完成音频设备的控制、音频流的输入 输出。 而 ALSA 则为应用程序开发人员提供了一个优秀的音频库。利用该音频库,开发 人员可以方便快捷地开发出自己的应用程序,细节则留给音频库内部处理。当然 ALSA 也提供了类似于 OSS 的系统接口,不过 ALSA 的开发者建议应用程序开发者使用音频 库而不是驱程API。 3、那么我该从何开始呢 第一步当然是安装 ALSA 驱动程序与音频库。 当前 ALSA 有两个分支,一个是以前的0.5版本,一个是现在的0.9。ALSA的开发者 已经不支持0.5版本了,所以我们要使用0.9。大家可以在 ALSA 的主页www.alsa-project.org 上下载安装。这个页面上的信息对大家安装很有用:www.alsa-project.org/alsa-doc ,建议先浏览一下。 第二步是参考文档与例子。 在 ALSA 的文档页面上有两篇为应用程序开发者提供的文章: ALSA 0.9.0 HOWTO [ http://www.suse.de/~mana/alsa090_howto.html ] Howto use the ALSA API [ http://www.op.net/~pbd/alsa-audio.html ] 当然,还有音频库API的在线参考:http://www.alsa-project.org/alsa-doc/alsa-lib 在开发过程中大家肯定会遇到问题或者困难,这时请教跟讨论就少不了了。所以要 做的第三步就是订阅 ALSA 的邮件列表了: alsa-devel@lists.sourceforge.net 。或者 你 E 文不好的话,可以在这里跟别人讨论一下。不过不要期望过高,国内开发 ALSA 音频程序的人本来就少,能来这里的人就更少了。 要是你已经完成以上几步的话,那么你就应该开始开发了。而我的任务到此也结束了。 4、结语 我曾经在论坛服务区上建议开一个linux下的音频编程版块(见: http://www.linuxforum.net/forum/showflat.php?Cat=&Board=uglyduck&Number=359899&page=2&view=collapsed&sb=5&o=31&fpart= )。然而响应者甚廖,David 也没有表态,看着开版块的希望已经 没有了,只好退而求其次,到这个版块来发表这篇豆腐干。 大家可以看出其实这篇贴子没什么技术含量,除第二部分是自己开发一个基于 ALSA 的小程序时的一些体会外,第一部分其实是参考 linux sound howto [ http://www.ibiblio.org/pub/Linux/docs/HOWTO/Sound-HOWTO ] 的,而第三部分则没 什么内容。我只是想告诉大家应该关注一下音频编程,顺便给大家指个方向而已。而后 面的两篇“基础”与一篇“深入”是从 ALSA 的文档页面上转载的,大家有兴趣可以看 看。 我还是那句话:GNU/Linux 系统只有从应用层次转变为开发层次才真正在中国扎下根 来。希望大家多努力。 http://www.unixresources.net/linux/clf/program/archive/00/00/37/05/370523.html

    时间:2019-10-08 关键词: 编程 linux内核

  • Linux音频编程指南

    简介: 虽然目前Linux的优势主要体现在网络服务方面,但事实上同样也有着非常丰富的媒体功能,本文就是以多媒体应用中最基本的声音为对象,介绍如何在Linux平台下开发实际的音频应用程序,同时还给出了一些常用的音频编程框架。 http://www.ibm.com/developerworks/cn/linux/l-audio/一、数字音频 音频信号是一种连续变化的模拟信号,但计算机只能处理和记录二进制的数字信号,由自然音源得到的音频信号必须经过一定的变换,成为数字音频信号之后,才能送到计算机中作进一步的处理。 数字音频系统通过将声波的波型转换成一系列二进制数据,来实现对原始声音的重现,实现这一步骤的设备常被称为模/数转换器(A/D)。A/D转换器以每秒钟上万次的速率对声波进行采样,每个采样点都记录下了原始模拟声波在某一时刻的状态,通常称之为样本(sample),而每一秒钟所采样的数目则称为采样频率,通过将一串连续的样本连接起来,就可以在计算机中描述一段声音了。对于采样过程中的每一个样本来说,数字音频系统会分配一定存储位来记录声波的振幅,一般称之为采样分辩率或者采样精度,采样精度越高,声音还原时就会越细腻。 数字音频涉及到的概念非常多,对于在Linux下进行音频编程的程序员来说,最重要的是理解声音数字化的两个关键步骤:采样和量化。采样就是每隔一定时间就读一次声音信号的幅度,而量化则是将采样得到的声音信号幅度转换为数字值,从本质上讲,采样是时间上的数字化,而量化则是幅度上的数字化。下面介绍几个在进行音频编程时经常需要用到的技术指标: 采样频率 采样频率是指将模拟声音波形进行数字化时,每秒钟抽取声波幅度样本的次数。采样频率的选择应该遵循奈奎斯特(Harry Nyquist)采样理论:如果对某一模拟信号进行采样,则采样后可还原的最高信号频率只有采样频率的一半,或者说只要采样频率高于输入信号最高频率的两 倍,就能从采样信号系列重构原始信号。正常人听觉的频率范围大约在20Hz~20kHz之间,根据奈奎斯特采样理论,为了保证声音不失真,采样频率应该在 40kHz左右。常用的音频采样频率有8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz等,如 果采用更高的采样频率,还可以达到DVD的音质。 量化位数 量化位数是对模拟音频信号的幅度进行数字化,它决定了模拟信号数字化以后的动态范围,常用的有8位、12位和16位。量化位越高,信号的动态范围越大,数字化后的音频信号就越可能接近原始信号,但所需要的存贮空间也越大。 声道数 声道数是反映音频数字化质量的另一个重要因素,它有单声道和双声道之分。双声道又称为立体声,在硬件中有两条线路,音质和音色都要优于单声道,但数字化后占据的存储空间的大小要比单声道多一倍。 回页首 二、声卡驱动 出于对安全性方面的考虑,Linux下的应用程序无法直接对声卡这类硬件设备进行操作,而是必须通过内核提供的驱动程序才能完成。在Linux上进行音频编程的本质就是要借助于驱动程序,来完成对声卡的各种操作。 对硬件的控制涉及到寄存器中各个比特位的操作,通常这是与设备直接相关并且对时序的要求非常严格,如果这些工作都交由应用程序员来负责,那么对声卡的编程将变得异常复杂而困难起来,驱动程序的作用正是要屏蔽硬件的这些底层细节,从而简化应用程序的编写。目前Linux下常用的声卡驱动程序主要有两种:OSS和ALSA。 最早出现在Linux上的音频编程接口是OSS(Open SoundSystem),它由一套完整的内核驱动程序模块组成,可以为绝大多数声卡提供统一的编程接口。OSS出现的历史相对较长,这些内核模块中的一部分(OSS/Free)是与Linux内核源码共同免费发布的,另外一些则以二进制的形式由4FrontTechnologies公司提供。由于得到了商业公司的鼎力支持,OSS已经成为在Linux下进行音频编程的事实标准,支持OSS的应用程序能够在绝大多数声卡上工作良好。 虽然OSS已经非常成熟,但它毕竟是一个没有完全开放源代码的商业产品,ALSA(Advanced Linux SoundArchitecture)恰好弥补了这一空白,它是在Linux下进行音频编程时另一个可供选择的声卡驱动程序。ALSA除了像OSS那样提供了一组内核驱动程序模块之外,还专门为简化应用程序的编写提供了相应的函数库,与OSS提供的基于ioctl的原始编程接口相比,ALSA函数库使用起来要更加方便一些。ALSA的主要特点有: 支持多种声卡设备模块化的内核驱动程序支持SMP和多线程提供应用开发函数库兼容OSS应用程序ALSA和OSS最大的不同之处在于ALSA是由志愿者维护的自由项目,而OSS则是由公司提供的商业产品,因此在对硬件的适应程度上OSS要优于ALSA,它能够支持的声卡种类更多。ALSA虽然不及OSS运用得广泛,但却具有更加友好的编程接口,并且完全兼容于OSS,对应用程序员来讲无疑是一个更佳的选择。 回页首 三、编程接口 如何对各种音频设备进行操作是在Linux上进行音频编程的关键,通过内核提供的一组系统调用,应用程序能够访问声卡驱动程序提供的各种音频设备接口,这是在Linux下进行音频编程最简单也是最直接的方法。 3.1 访问音频设备 无论是OSS还是ALSA,都是以内核驱动程序的形式运行在Linux内核空间中的,应用程序要想访问声卡这一硬件设备,必须借助于Linux内核所提供的系统调用(systemcall)。从程序员的角度来说,对声卡的操作在很大程度上等同于对磁盘文件的操作:首先使用open系统调用建立起与硬件间的联系,此时返回的文件描述符将作为随后操作的标识;接着使用read系统调用从设备接收数据,或者使用write系统调用向设备写入数据,而其它所有不符合读/写这一基本模式的操作都可以由ioctl系统调用来完成;最后,使用close系统调用告诉Linux内核不会再对该设备做进一步的处理。 open系统调用 系统调用open可以获得对声卡的访问权,同时还能为随后的系统调用做好准备,其函数原型如下所示: int open(const char *pathname, int flags, int mode); 参数pathname是将要被打开的设备文件的名称,对于声卡来讲一般是/dev/dsp。参数flags用来指明应该以什么方式打开设备文件,它可以是 O_RDONLY、O_WRONLY或者O_RDWR,分别表示以只读、只写或者读写的方式打开设备文件;参数mode通常是可选的,它只有在指定的设备 文件不存在时才会用到,指明新创建的文件应该具有怎样的权限。 如果open系统调用能够成功完成,它将返回一个正整数作为文件标识符,在随后的系统调用中需要用到该标识符。如果open系统调用失败,它将返回-1,同时还会设置全局变量errno,指明是什么原因导致了错误的发生。 read系统调用 系统调用read用来从声卡读取数据,其函数原型如下所示: int read(int fd, char *buf, size_t count); 参数fd是设备文件的标识符,它是通过之前的open系统调用获得的;参数buf是指向缓冲区的字符指针,它用来保存从声卡获得的数据;参数count则 用来限定从声卡获得的最大字节数。如果read系统调用成功完成,它将返回从声卡实际读取的字节数,通常情况会比count的值要小一些;如果read系 统调用失败,它将返回-1,同时还会设置全局变量errno,来指明是什么原因导致了错误的发生。 write系统调用 系统调用write用来向声卡写入数据,其函数原型如下所示: size_t write(int fd, const char *buf, size_t count); 系统调用write和系统调用read在很大程度是类似的,差别只在于write是向声卡写入数据,而read则是从声卡读入数据。参数fd同样是设备文 件的标识符,它也是通过之前的open系统调用获得的;参数buf是指向缓冲区的字符指针,它保存着即将向声卡写入的数据;参数count则用来限定向声 卡写入的最大字节数。 如果write系统调用成功完成,它将返回向声卡实际写入的字节数;如果read系统调用失败,它将返回-1,同时还会设置全局变量errno,来指明是 什么原因导致了错误的发生。无论是read还是write,一旦调用之后Linux内核就会阻塞当前应用程序,直到数据成功地从声卡读出或者写入为止。 ioctl系统调用 系统调用ioctl可以对声卡进行控制,凡是对设备文件的操作不符合读/写基本模式的,都是通过ioctl来完成的,它可以影响设备的行为,或者返回设备的状态,其函数原型如下所示: int ioctl(int fd, int request, ...); 参数fd是设备文件的标识符,它是在设备打开时获得的;如果设备比较复杂,那么对它的控制请求相应地也会有很多种,参数request的目的就是用来区分 不同的控制请求;通常说来,在对设备进行控制时还需要有其它参数,这要根据不同的控制请求才能确定,并且可能是与硬件设备直接相关的。 close系统调用 当应用程序使用完声卡之后,需要用close系统调用将其关闭,以便及时释放占用的硬件资源,其函数原型如下所示: int close(int fd); 参数fd是设备文件的标识符,它是在设备打开时获得的。一旦应用程序调用了close系统调用,Linux内核就会释放与之相关的各种资源,因此建议在不需要的时候尽量及时关闭已经打开的设备。 3.2 音频设备文件 对于Linux应用程序员来讲,音频编程接口实际上就是一组音频设备文件,通过它们可以从声卡读取数据,或者向声卡写入数据,并且能够对声卡进行控制,设置采样频率和声道数目等等。 /dev/sndstat 设备文件/dev/sndstat是声卡驱动程序提供的最简单的接口,通常它是一个只读文件,作用也仅仅只限于汇报声卡的当前状态。一般说来,/dev/sndstat是提供给最终用户来检测声卡的,不宜用于程序当中,因为所有的信息都可以通过ioctl系统调用来获得。 Linux提供的cat命令可以很方便地从/dev/sndstat获得声卡的当前状态: [xiaowp@linuxgam sound]$ cat /dev/sndstat /dev/dsp 声卡驱动程序提供的/dev/dsp是用于数字采样(sampling)和数字录音(recording)的设备文件,它对于 Linux下的音频编程来讲非常重要:向该设备写数据即意味着激活声卡上的D/A转换器进行放音,而向该设备读数据则意味着激活声卡上的A/D转换器进行 录音。目前许多声卡都提供有多个数字采样设备,它们在Linux下可以通过/dev/dsp1等设备文件进行访问。 DSP是数字信号处理器(Digital Signal Processor)的简称,它是用来进行数字信号处理的特殊芯片,声卡使用它来实现模拟信号和数字信号的转换。声卡中的DSP设备实际上包含两个组成部 分:在以只读方式打开时,能够使用A/D转换器进行声音的输入;而在以只写方式打开时,则能够使用D/A转换器进行声音的输出。严格说来,Linux下的 应用程序要么以只读方式打开/dev/dsp输入声音,要么以只写方式打开/dev/dsp输出声音,但事实上某些声卡驱动程序仍允许以读写的方式打开 /dev/dsp,以便同时进行声音的输入和输出,这对于某些应用场合(如IP电话)来讲是非常关键的。 在从DSP设备读取数据时,从声卡输入的模拟信号经过A/D转换器变成数字采样后的样本(sample),保存在声卡驱动程序 的内核缓冲区中,当应用程序通过read系统调用从声卡读取数据时,保存在内核缓冲区中的数字采样结果将被复制到应用程序所指定的用户缓冲区中。需要指出 的是,声卡采样频率是由内核中的驱动程序所决定的,而不取决于应用程序从声卡读取数据的速度。如果应用程序读取数据的速度过慢,以致低于声卡的采样频率, 那么多余的数据将会被丢弃;如果读取数据的速度过快,以致高于声卡的采样频率,那么声卡驱动程序将会阻塞那些请求数据的应用程序,直到新的数据到来为止。 在向DSP设备写入数据时,数字信号会经过D/A转换器变成模拟信号,然后产生出声音。应用程序写入数据的速度同样应该与声卡 的采样频率相匹配,否则过慢的话会产生声音暂停或者停顿的现象,过快的话又会被内核中的声卡驱动程序阻塞,直到硬件有能力处理新的数据为止。与其它设备有 所不同,声卡通常不会支持非阻塞(non-blocking)的I/O操作。 无论是从声卡读取数据,或是向声卡写入数据,事实上都具有特定的格式(format),默认为8位无符号数据、单声道、 8KHz采样率,如果默认值无法达到要求,可以通过ioctl系统调用来改变它们。通常说来,在应用程序中打开设备文件/dev/dsp之后,接下去就应 该为其设置恰当的格式,然后才能从声卡读取或者写入数据。 /dev/audio /dev/audio类似于/dev/dsp,它兼容于Sun工作站上的音频设备,使用的是mu-law编码方式。如果声卡驱动程序提供了对/dev /audio的支持,那么在Linux上就可以通过cat命令,来播放在Sun工作站上用mu-law进行编码的音频文件: [xiaowp@linuxgam sound]$ cat audio.au > /dev/audio 由于设备文件/dev/audio主要出于对兼容性的考虑,所以在新开发的应用程序中最好不要尝试用它,而应该以/dev/dsp进行替代。对于应用程序来说,同一时刻只能使用/dev/audio或者/dev/dsp其中之一,因为它们是相同硬件的不同软件接口。 /dev/mixer 在声卡的硬件电路中,混音器(mixer)是一个很重要的组成部分,它的作用是将多个信号组合或者叠加在一起,对于不同的声卡来说,其混音器的作用可能各 不相同。运行在Linux内核中的声卡驱动程序一般都会提供/dev/mixer这一设备文件,它是应用程序对混音器进行操作的软件接口。混音器电路通常 由两个部分组成:输入混音器(input mixer)和输出混音器(output mixer)。 输入混音器负责从多个不同的信号源接收模拟信号,这些信号源有时也被称为混音通道或者混音设备。模拟信号通过增益控制器和由软件控制的音量调节器后,在不 同的混音通道中进行级别(level)调制,然后被送到输入混音器中进行声音的合成。混音器上的电子开关可以控制哪些通道中有信号与混音器相连,有些声卡 只允许连接一个混音通道作为录音的音源,而有些声卡则允许对混音通道做任意的连接。经过输入混音器处理后的信号仍然为模拟信号,它们将被送到A/D转换器 进行数字化处理。 输出混音器的工作原理与输入混音器类似,同样也有多个信号源与混音器相连,并且事先都经过了增益调节。当输出混音器对所有的模拟信号进行了混合之后,通常 还会有一个总控增益调节器来控制输出声音的大小,此外还有一些音调控制器来调节输出声音的音调。经过输出混音器处理后的信号也是模拟信号,它们最终会被送 给喇叭或者其它的模拟输出设备。 对混音器的编程包括如何设置增益控制器的级别,以及怎样在不同的音源间进行切换,这些操作通常来讲是不连续的,而且不会像录音或者放音那样需要占用大量的 计算机资源。由于混音器的操作不符合典型的读/写操作模式,因此除了open和close两个系统调用之外,大部分的操作都是通过ioctl系统调用来完 成的。与/dev/dsp不同,/dev/mixer允许多个应用程序同时访问,并且混音器的设置值会一直保持到对应的设备文件被关闭为止。 为了简化应用程序的设计,Linux上的声卡驱动程序大多都支持将混音器的ioctl操作直接应用到声音设备上,也就是说如果已经打开了/dev /dsp,那么就不用再打开/dev/mixer来对混音器进行操作,而是可以直接用打开/dev/dsp时得到的文件标识符来设置混音器。 /dev/sequencer 目前大多数声卡驱动程序还会提供/dev/sequencer这一设备文件,用来对声卡内建的波表合成器进行操作,或者对MIDI总线上的乐器进行控制,一般只用于计算机音乐软件中。 回页首 四、应用框架 在Linux下进行音频编程时,重点在于如何正确地操作声卡驱动程序所提供的各种设备文件,由于涉及到的概念和因素比较多,所以遵循一个通用的框架无疑将有助于简化应用程序的设计。 4.1 DSP编程 对声卡进行编程时首先要做的是打开与之对应的硬件设备,这是借助于open系统调用来完成的,并且一般情况下使用的是/dev/dsp文件。采用何种模式对声卡进行操作也必须在打开设备时指定,对于不支持全双工的声卡来说,应该使用只读或者只写的方式打开,只有那些支持全双工的声卡,才能以读写的方式打开,并且还要依赖于驱动程序的具体实现。Linux允许应用程序多次打开或者关闭与声卡对应的设备文件,从而能够很方便地在放音状态和录音状态之间进行切换,建议在进行音频编程时只要有可能就尽量使用只读或者只写的方式打开设备文件,因为这样不仅能够充分利用声卡的硬件资源,而且还有利于驱动程序的优化。下面的代码示范了如何以只写方式打开声卡进行放音(playback)操作: int handle = open("/dev/dsp", O_WRONLY); if (handle == -1) { perror("open /dev/dsp"); return -1; } 运行在Linux内核中的声卡驱动程序专门维护了一个缓冲区,其大小会影响到放音和录音时的效果,使用ioctl系统调用可以对它的尺寸进行恰当的设置。调节驱动程序中缓冲区大小的操作不是必须的,如果没有特殊的要求,一般采用默认的缓冲区大小也就可以了。但需要注意的是,缓冲区大小的设置通常应紧跟在设备文件打开之后,这是因为对声卡的其它操作有可能会导致驱动程序无法再修改其缓冲区的大小。下面的代码示范了怎样设置声卡驱动程序中的内核缓冲区的大小: int setting = 0xnnnnssss; int result = ioctl(handle, SNDCTL_DSP_SETFRAGMENT, &setting); if (result == -1) { perror("ioctl buffer size"); return -1; } // 检查设置值的正确性 在设置缓冲区大小时,参数setting实际上由两部分组成,其低16位标明缓冲区的尺寸,相应的计算公式为buffer_size =2^ssss,即若参数setting低16位的值为16,那么相应的缓冲区的大小会被设置为65536字节。参数setting的高16位则用来标明分片(fragment)的最大序号,它的取值范围从2一直到0x7FFF,其中0x7FFF表示没有任何限制。 接下来要做的是设置声卡工作时的声道(channel)数目,根据硬件设备和驱动程序的具体情况,可以将其设置为0(单声道,mono)或者1(立体声,stereo)。下面的代码示范了应该怎样设置声道数目: int channels = 0; // 0=mono 1=stereo int result = ioctl(handle, SNDCTL_DSP_STEREO, &channels); if ( result == -1 ) { perror("ioctl channel number"); return -1; } if (channels != 0) { // 只支持立体声 } 采样格式和采样频率是在进行音频编程时需要考虑的另一个问题,声卡支持的所有采样格式可以在头文件soundcard.h中找到,而通过ioctl系统调用则可以很方便地更改当前所使用的采样格式。下面的代码示范了如何设置声卡的采样格式: int format = AFMT_U8; int result = ioctl(handle, SNDCTL_DSP_SETFMT, &format); if ( result == -1 ) { perror("ioctl sample format"); return -1; } // 检查设置值的正确性 声卡采样频率的设置也非常容易,只需在调用ioctl时将第二个参数的值设置为SNDCTL_DSP_SPEED,同时在第三个参数中指定采样频率的数值就行了。对于大多数声卡来说,其支持的采样频率范围一般为5kHz到44.1kHz或者48kHz,但并不意味着该范围内的所有频率都会被硬件支持,在Linux下进行音频编程时最常用到的几种采样频率是11025Hz、16000Hz、22050Hz、32000Hz和44100Hz。下面的代码示范了如何设置声卡的采样频率: int rate = 22050; int result = ioctl(handle, SNDCTL_DSP_SPEED, &rate); if ( result == -1 ) { perror("ioctl sample format"); return -1; } // 检查设置值的正确性 4.2 Mixer编程 声卡上的混音器由多个混音通道组成,它们可以通过驱动程序提供的设备文件/dev/mixer进行编程。对混音器的操作是通过ioctl系统调用来完成的,并且所有控制命令都由SOUND_MIXER或者MIXER开头,表1列出了常用的几个混音器控制命令: 名 称 作 用 SOUND_MIXER_VOLUME 主音量调节 SOUND_MIXER_BASS 低音控制 SOUND_MIXER_TREBLE 高音控制 SOUND_MIXER_SYNTH FM合成器 SOUND_MIXER_PCM 主D/A转换器 SOUND_MIXER_SPEAKER PC喇叭 SOUND_MIXER_LINE 音频线输入 SOUND_MIXER_MIC 麦克风输入 SOUND_MIXER_CD CD输入 SOUND_MIXER_IMIX 回放音量 SOUND_MIXER_ALTPCM 从D/A 转换器 SOUND_MIXER_RECLEV 录音音量 SOUND_MIXER_IGAIN 输入增益 SOUND_MIXER_OGAIN 输出增益 SOUND_MIXER_LINE1 声卡的第1输入 SOUND_MIXER_LINE2 声卡的第2输入 SOUND_MIXER_LINE3 声卡的第3输入 表1 混音器命令 对声卡的输入增益和输出增益进行调节是混音器的一个主要作用,目前大部分声卡采用的是8位或者16位的增益控制器,但作为程序员来讲并不需要关心这些,因为声卡驱动程序会负责将它们变换成百分比的形式,也就是说无论是输入增益还是输出增益,其取值范围都是从0到100。在进行混音器编程时,可以使用SOUND_MIXER_READ宏来读取混音通道的增益大小,例如在获取麦克风的输入增益时,可以使用如下的代码: int vol; ioctl(fd, SOUND_MIXER_READ(SOUND_MIXER_MIC), &vol); printf("Mic gain is at %d %%n", vol); 对于只有一个混音通道的单声道设备来说,返回的增益大小保存在低位字节中。而对于支持多个混音通道的双声道设备来说,返回的增益大小实际上包括两个部分,分别代表左、右两个声道的值,其中低位字节保存左声道的音量,而高位字节则保存右声道的音量。下面的代码可以从返回值中依次提取左右声道的增益大小: int left, right; left = vol & 0xff; right = (vol & 0xff00) >> 8; printf("Left gain is %d %%, Right gain is %d %%n", left, right); 类似地,如果想设置混音通道的增益大小,则可以通过SOUND_MIXER_WRITE宏来实现,此时遵循的原则与获取增益值时的原则基本相同,例如下面的语句可以用来设置麦克风的输入增益: vol = (right

    时间:2019-10-02 关键词: Linux linux内核

  • Linux内核USB主设备驱动程序

    http://www.shangshuwu.cn/index.php/Linux%E5%86%85%E6%A0%B8USB%E4%B8%BB%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8%E7%A8%8B%E5%BA%8F 目录 [隐藏] 1 ehci-hcd控制器 1.1 EHCI构架介绍1.2 EHCI驱动程序分析2 Mass Storage主机驱动程序 2.1 Mass Storage规范介绍2.2 Bulk-Only传输协议介绍2.3 SCSI命令描述块结构2.4 Mass Storage设备对象结构2.5 Mass Storage设备初始化2.6 探测函数storage_probe分析 ehci-hcd控制器 EHCI构架介绍 USB主控器规范包括USB1.1主控器规范和USB2.0主控器规范。USB1.1主控器规范有包括UHCI(Universal Host Controller Interface)和OHCI(Open Host Controller Interface Specification);USB2.0主控器规范为EHCI(Enhanced Host Controller Interface Specification)。UHCI和OHCI在硬件实现以及对底层软件访问上都有所不同,但二者又都完全USB 1.1中对主控制器的要求。 USB主控制器驱动程序的整个系统框架图如图4所示,从图中可以看出USB驱动程序包括客户驱动、通用总线驱动程序、EHCI驱动程序等组 成。其中,客户驱动程序是特定USB设备的驱动程序,提供了USB设备的功能操作及特定子类协议封装;USB驱动程序(USBD)是特定操作系统上抽象出 的主机控制器驱动程序共有特性,对应于Linux USB驱动程序的HCD层;EHCI控制器驱动程序(EHCD)是依赖于特定硬件寄存器接口定义的主控制器驱动程序。USB设备是执行终端用户功能硬件设 备。 图4 USB驱动程序系统框架图 EHCI通用构架如图5所示。每个EHCI接口定义了三个接口空间,该个接口空间说明如下: PCI配置空间包括PCI寄存器,它们用来系统部件枚举和PCI电源管理。 寄存器空间,通常称为I/O空间。它必须被用作内存映射I/O空间。它包括特定应用参数寄存器和能力寄存器、加上可选的控制和状态寄存器。 调度接口空间是特殊分配的内存并且被EHCI驱动程序管理用来周期性或异步调度。 图 EHCI通用构架图 EHCI支持两种类型传输:异步类型和周期类型。周期类型包括同步传输和中断传输,异步类型包括控制传输和批量传输。EHCI调度接口给两种类型提 供了分开的调度。周期调度基于时间发起的帧链表,它代表主机控制器工作条目的滑动窗口。所有的同步和中断传输都通过周期调度来进行。 异步调度是简单的调度工作条目的循环链表,它给所有异步传输提供了循环调度服务。 EHCI使用一个简单的buffer队列数据结构来管理所有的中断、批量和控制传输类型。排队的数据结构提供了自动的、排序的数据传输流。软件能异步地加数据buffer到一个队列并维护数据流。USB定义的短包语法在没有软件干预下完全支持所有的边界条件处理。 USB总线的主机控制器要求应用根集线器,主机控制器模拟了根集线器,它在操作寄存器空间装有端口寄存器,寄存器含有在USB规范中需要管 理每个端口的最小硬件状态和控制。事务通过根端口被广播下流的USB设备,端口寄存器提供给系统软件对端口的管理和端口的状态信息,包括:设备的连接与断 开、执行设备复位、处理端口功率和端口电源管理。 EHCI控制器提供了两套软件可访问的寄存器:内存映射的主机控制器寄存器和可选的PCI配置寄存器。PCI配置寄存器仅是用到主机控制器的PCI设备需要的。 主机控制器能力寄存器定义了限制、主机控制器使用的能力,如:下行端口数、主机控制器的接口版本号、同步调度门限等。在代码中使用结构ehci_caps来描述。 主机控制器操作寄存器位于能力寄存器之后,是双字对齐读写寄存器。这些寄存器分为两套,第一套从地址00到3Fh,在主控制器核心电源好的 情况下使用,包括USB控制命令、状态、中断使能、帧序号寄存器。第二套寄存器从40h到可使用的寄存器空间结尾,在外围辅助电源好的情况下使用,包括每 个端口的状态与控制寄存器。在代码中使用结构ehci_regs来描述。 接口数据结构在hcd软件和ehci控制器硬件之间用于通信控制、数据和状态。接口由周期调度、周期帧链表、异步调度、同步事务描述子 (iTD)、分离事务同步传输描述子(siTD)、队列头(QH)和队列元素传输描述子(qTD)组成。在代码中,qTD用结构ehci_qtd描 述,QH用结构ehci_qh描述。iTD用结构ehci_itd描述,siTD用结构ehci_sitd描述。 EHCI主机控制器带有一个模拟操作的根集线器,通过寄存器可完成对根集线器的各个端口的状态及连接控制,因此,它不会调用到USB核心层中有关HUB的操作函数。 EHCI主机控制器对于URB的提交排队及传输、调度以及控制器的各种状态转移提供了控制。特别是寄存器级的控制函数与EHCI控制器本身结构相关,牵涉到对众多寄存器值的理解,因而这里只说明了ehci控制器的上层功能函数。 EHCI驱动程序分析 EHCI驱动程序的编写思路是:EHCI驱动程序是一个结构hc_driver实例,它应该实现结构hc_driver中的函数,另外,从硬件上层 来看,EHCI主控制从PCI总线桥接,应是一个PCI驱动程序实例,因此,应实现结构pci_driver中的函数,并用PCI注册函数 pci_register_driver注册此实例。 函数__init init注册了&ehci_pci_driver控制器驱动程序,由于ehci-hcd是通过PCI总线与CPU相连,因而,它被注册成一个新的PCI驱动程序。 函数__init init分析如下(在drivers/usb/host/ehci-hcd.c中): #ifdef CONFIG_PCI #include "ehci-pci.c" #define PCI_DRIVER ehci_pci_driver #endif static int __init ehci_hcd_init(void) { int retval = 0;   ……   #ifdef PCI_DRIVER //注册驱动程序,初始化ehci_pci_driver并加到内核对象体系中去 retval = pci_register_driver(&PCI_DRIVER); if (retval < 0) goto clean1; #endif …… clean1: #endif #ifdef PLATFORM_DRIVER platform_driver_unregister(&PLATFORM_DRIVER); …… #endif return retval; } PCI驱动程序结构实例ehci_pci_driver的一些函数定义如下: static const char hcd_name [] = "ehci_hcd"; /* pci driver glue; this is a "new style" PCI driver module */ static struct pci_driver ehci_pci_driver = { .name = (char *) hcd_name, .id_table = pci_ids,   .probe = usb_hcd_pci_probe, //探测函数 .remove = usb_hcd_pci_remove, //移去设备时的清除函数   #ifdef CONFIG_PM .suspend = usb_hcd_pci_suspend, .resume = usb_hcd_pci_resume, #endif }; pci_ids 是PCI驱动程序选择元数据,PCI热插拔使用到它,通过它来选择驱动程序ehci_driver。pci_ids列出如下: static const struct pci_device_id pci_ids [] = { { //处理任何USB 2.0 EHCI控制器 PCI_DEVICE_CLASS(((PCI_CLASS_SERIAL_USB driver_data)) return -EINVAL;     //使设备的I/O和设备内存区有效,唤醒设备,在被驱动程序使用前初始化设备 if (pci_enable_device (dev) < 0) return -ENODEV; dev->current_state = 0; dev->dev.power.power_state = 0;   if (!dev->irq) {//没有中断 dev_err (&dev->dev, "Found HC with no IRQ. Check BIOS/PCI %s setup!n", pci_name(dev)); retval = -ENODEV; goto done; }   //HC寄存器使用内存 if (driver->flags & HCD_MEMORY) { // EHCI, OHCI region = 0; resource = pci_resource_start (dev, 0); //得到PCI设备的0号区域资源 len = pci_resource_len (dev, 0);     //申请名字为driver->description的I/O内存区域,     //从resource开始,长度为len if (!request_mem_region (resource, len, driver->description)) { dev_dbg (&dev->dev, "controller already in usen"); retval = -EBUSY; goto done; }     //映射resource开始的物理地址到CPU的虚拟地址base base = ioremap_nocache (resource, len); if (base == NULL) {//映射失败 dev_dbg (&dev->dev, "error mapping memoryn"); retval = -EFAULT; clean_1: release_mem_region (resource, len); //释放资源 dev_err (&dev->dev, "init %s fail, %dn", pci_name(dev), retval); goto done; }   } else { // UHCI resource = len = 0;     //标准PCI配置6个region(或说6个bar) for (region = 0; region < PCI_ROM_RESOURCE; region++) { //如果不是IO资源 if (!(pci_resource_flags (dev, region) & IORESOURCE_IO)) continue;   resource = pci_resource_start (dev, region); len = pci_resource_len (dev, region); //申请名字为driver->description的资源 if (request_region (resource, len, driver->description)) break; } if (region == PCI_ROM_RESOURCE) {//如果是rom,则说明无资源可用 dev_dbg (&dev->dev, "no i/o regions availablen"); retval = -EBUSY; goto done; } base = (void __iomem *) resource; }   //创建并初始化结构hcd hcd = usb_create_hcd (driver); …… // hcd zeroed everything hcd->regs = base; hcd->region = region;   //将hcd驱动程序结构赋给pci设备结构,即dev ->dev->driver_data = hcd pci_set_drvdata (dev, hcd); hcd->self.bus_name = pci_name(dev); #ifdef CONFIG_PCI_NAMES hcd->product_desc = dev->pretty_name; #endif hcd->self.controller = &dev->dev;   //创建4个DMA池 if ((retval = hcd_buffer_create (hcd)) != 0) { clean_3: pci_set_drvdata (dev, NULL); usb_put_hcd (hcd); goto clean_2; }   //打印信息 dev_info (hcd->self.controller, "%sn", hcd->product_desc);   //到现在为止,HC已在一个不确定状态,调用驱动程序的reset函数复位 if (driver->reset && (retval = driver->reset (hcd)) < 0) { dev_err (hcd->self.controller, "can't resetn"); goto clean_3; }      //使能设备上的bus-mastering总线 pci_set_master (dev); …   //申请共享中断号,中断处理函数是usb_hcd_irq,设备名为description retval = request_irq (dev->irq, usb_hcd_irq, SA_SHIRQ, hcd->driver->description, hcd); … hcd->irq = dev->irq;   //注册总线到sysfs和/proc文件系统 usb_register_bus (&hcd->self); …… return retval; } 函数 usb_create_hcd创建并初始化一个结构usb_hcd实例,参数driver是此HCD使用的HCD驱动程序。如果内存不可用,返回NULL。 函数 usb_create_hcd列出如下(在drivers/usb/core/hcd.c中): struct usb_hcd *usb_create_hcd (const struct hc_driver *driver) { struct usb_hcd *hcd;   hcd = kcalloc(1, sizeof(*hcd) + driver->hcd_priv_size, GFP_KERNEL); if (!hcd) return NULL;   usb_bus_init(&hcd->self);//初始化usb_bus结构 hcd->self.op = &usb_hcd_operations; hcd->self.hcpriv = hcd; hcd->self.release = &hcd_release;   init_timer(&hcd->rh_timer);   hcd->driver = driver; hcd->product_desc = (driver->product_desc) ? driver->product_desc : "USB Host Controller"; hcd->state = USB_STATE_HALT;   return hcd; } Mass Storage主机驱动程序 Mass Storage规范介绍 USB大存储(USB Mass Storage)工作组(CWG Class Working Group)规范包括: USB Mass Storage Class Control/Bulk/Interrupt(CBI) Transport 即USB大存储类控制/批量/中断传输协议。 USB Mass Storage Class Bulk-Only Transport 即USB大存储类批量传输协议。 USB Mass Storage Class UFI Command Specification 即USB大存储类UFI命令规范。 USB Mass Storage Class Bootability Specfication 即USB大存储类系统启动规范。 USB Mass Storage Class Compliance Test Specification 即USB大存储类遵从测试规范。 其中,CBI传输规范仅用于全速软盘驱动器,不能用于高速设备或其它非软盘设备。 USB大存储类使用几种命令集规范,这些命令集的命令块放在符合USB协议的USB包裹器中,USB大存储类规范定义了下面几种命令集: 软驱、光驱和磁带驱动器使用的ATAPI规范(Advanced Technology Attachment Packet Interface)。 精简块命令(Reduced Block Commands(RBC)) 多媒体命令集2(Multi-Media Command Set 2 (MMC-2)) SCSI主命令(SCSI Primary Commands-2(SPC-2)) USB规范(Universal Serial Bus Specification) USB大存储类设备的接口描述子包含了一个bInterfaceSubClass和bInterfacePortocal的 域,bInterfaceSubClass描述了USB大存储类支持的命令块规范,如:它为06h时表示支持的是SCSI传输命令集。 bInterfacePortocal描述了USB大存储类支持的接口传输协议,如:它为50h时表示支持的是Bulk-Only传输协议。 大存储设备(Mass Storage)包括U盘、读卡器及USB接口的光驱等其它块存储设备,它们看作是SCSI接口设备,当用户从设备上读写数据时,文件系统将读写操作传送 到SCSI协议层,SCSI协议层的读写请求封装成USB请求块(URB)通过USB接口传递给设备,USB设备从URB中解析出SCSI协议命令后再操 作块设备。USB接口大存储设备的操作流程图如图6所示。 图6 USB接口大存储设备的操作流程图 USB接口大存储设备驱动程序的设计思路是:设计一个控制线程,这个线程被注册为虚拟SCSI控制器,这个线程在设备插入/移去时一直作为SCSI 节点存在的。这样,被移去的设备能在再插上时被给以与以前/dev中同一节点。当一个设备被插上时,控制线程从SCSI中间层代码得到命令。控制线程接收 命令,在检查后送命令到协议处理函数。这些处理函数负责再写命令(如果必要)到设备得接受的形式。例如:ATAPI设备不能支持6byte命令,这样,它 们必须被再写成10byte变量。一旦协议处理函数已再写了命令,它们被送到传输处理函数。传输处理函数负责送命令到设备、交换数据、并接着得到设备的状 态。在协议处理函数和传输处理函数之间有一小段代码,来决定REQUEST_SENSE命令是否应该发出。在命令被处理后,scsi_done()被调用 来发信号给SCSI层命令已完成。我们准备接收下一条命令。 作为具有操作系统的智能嵌入设备,它使用了SCSI命令块集与Bulk-only传输协议。它既能作为主机来操作其它USB大存储设备,称 为大存储设备主机。同时,也能作为USB大存储设备被其它主机控制。下面对具有linux操作系统的嵌入设备分别就两种模式分别进行分析。 Bulk-Only传输协议介绍 Bulk-Only传输协议是USB大容量存贮器类中的USB批量数据传输协议,它定义了仅通过批量端点传输的命令、数据和状态。它使用命令块数据 包裹器(CBW)发送命令,使用命令状态数据包裹器(CSW)接收返回的状态。命令块数据包裹器(CBW)是一个包含命令块和相关信息的数据包。 命令状态数据包(CSW)裹器是一个包含命令块状态的数据包。命令块数据包裹器(CBW)的格式如表1所示。 表1 命令块数据包裹器(CBW)格式表 Byte Bit 7 6 5 4 3 2 1 0 0-3 dCBWSignature 4-7 dCBWTag 8-11 (08h-0Bh) dCBWDataTransferLength 12 (0Ch) bmCBWFlags 13 (0Dh) Reserved(0) bCBWLUN 14 (0Eh) Reserved(0) bCBWCBLength 15-30 (0Fh-1Eh) CBWCB 命令块数据包裹器(CBW)用下述数据结构描述(在drivers/usb/storage/transport.h中): struct bulk_cb_wrap { __le32 Signature; //签名'USBC' __u32 Tag; //每个命令唯一的ID __le32 DataTransferLength; //数据大小 __u8 Flags; //在bit 0中表示方向 __u8 Lun; //表示LUN(SCSI逻辑单元)正常为0 __u8 Length; //数据传输长度 __u8 CDB[16]; //传输的命令字节 }; 命令状态数据包裹器(CSW)的格式如表2所示。 表2 命令状态数据包(CSW)的格式表 Byte Bit 7 6 5 4 3 2 1 0 0-3 dCSWSignature 4-7 dCSWTag 8-11(Bh) dCSWDataResidue 12(Ch) dCSWStatus 命令状态数据包裹器(CSW)用下述数据结构描述(在drivers/usb/storage/transport.h中): /* 命令状态包裹器*/ struct bulk_cs_wrap { __le32 Signature; //签名 'USBS' __u32 Tag; //与CBW中Tag一样 __le32 Residue; //没有传输完的数据量 __u8 Status; //操作状态标识,如:成功、失败等 __u8 Filler[18]; };  传输过程是:当传输方向是从设备到主机时,则当CBW发送成功后,设备从设备的In端点读取CBW中规定长度的数据CBWCB;当传输方向是从主机到设 备时,则当CBW发送成功后,向设备的Out端点发送CBW中规定长度的数据CBWCB。CBWCB是命令块数据,是遵循某一规范的命令集, 如:SCSI-2命令集,最长16字节。  当主机与设备之间的数据传送完毕后,主机还需从设备的In端点读取传送状态,主机根据接收的CSW数据包即可判断出通信是否正常。若返回的结果有错误,还须进行相应的出错处理。 样例:从设备读取数据的传输过程 下面是一个从设备读取数据的传输过程的例子,主机先向端点1发出CBW命令,设备解析CBW解析命令后,从主机指定的端点2将数据传回给主 机。在传送成功后,主机又读取端点2的状态CSW。主机从设备读到数据的流程图如下图。从图中可看出,第0到第2包是发送CBW的过程,第3到第5包是读 取数据的过程,下面接着的第0到第1包是读取CSW的过程。令牌包和握手包是由控制管道(对应ep0)来发送接收的。 图 主机从设备读到数据的流程图 第0到第5包的数据格式图列出如图7所示: 图7 第0到第5包的数据格式图 在第1包中,CBW传输了31(1FH)个字节的数据。内容含义是:55 53 42 43 是CBW后面固有的特征码;28 E8 31 FE 是由主机产生的CBWTag;00 02 00 00 是CBW数据传输长度,在此情况下是0000,0200H=512字节;80 是后面固有的标志码;00 是后面固有的CBWLUN;0A 是CBWCB长度,意味着命令描述块(CDB)长度是10字节,其中。28表示对应SCSI协议28h读命令。对于命令块,看下节的SCSI命令描述块的 结构。 SCSI协议28h读命令是Read(10),在这个CBW中,要求读取0柱0道1扇区共512字节的MBR数据,前446字节为主引导记录,接着的64字节为DPT(Disk Partition Table盘分区表),最后的2字节"55 AA"为有效结束标志。 在第4包中传输了512字节的数据。 CSW包的数据格式图列出如图8所示: 图8 CSW包的数据格式图 CSW数据包传输13(0DH)个字节的数据。内容含义是:55 52 42 53是CSW后面固有的特征码;28 E8 31 FF是主机产生的CSWTag;00 00 00 00是CSW的数据冗余;00 指示在此情况下CSW的状态,此例中为OK。 SCSI命令描述块结构 各种SCSI命令描述块具有相似的结构,SCSI命令描述块的结构如表8所示。 表8一个典型的SCSI命令描述块结构   7 6 5 4 3 2 1 0 0 操作码 1 命令的指定参数 … … n-1 n 控制字节   SCSI命令描述块的结构的各项说明如下: 操作码(Opcode)   每个命令的0号字节就是操作码,它定义了命令的类型和长度。它的高3位代表了命令所属的命令组,低5位表示命令本身。每个命令组都有一个命令长度。因而,对命令的第一个字节进行解码以后,目标器就知道这个命令还剩下多少字节。操作码在不同设备上含义是不同的。 SCSI常用命令块有查询、读请求、测试单元准备、禁止媒介删除、读缓冲、写缓冲等。 命令组 代表命令组的高3位可以有8个不同的组合,所以可以代表8个命令组,当制造商实现自己的标准的时候,就必须使用6号组或者7号组,实际上,使用6号组或者7号组的情况很少发生。命令组的说明如表9所示。 表9 SCSI命令组说明 组 操作码 说明 0 00h~1Fh 6字节命令 1 20h~3Fh 10字节命令 2 40h~5Fh 10字节命令 3 60h~7Fh 保留 4 80h~9Fh 16字节命令 5 A0h~BFh 12字节命令 6 C0h~DFh 厂商自定 7 E0h~FFh 厂商自定 控制字节 控制字节的格式如表10所示。SCSI-2中,控制字节仅仅包含了在标准中定义的两位,它们是连接位(Link bit)和标志位(flag bit),而且这两位都是可选的。连接位使你可以将几个命令连接成一个命令链,命令链中的每一个命令被称为连接的命令。从而这些连接的命令就形成了一个连 接的I/O过程。这就可以阻止其他I/O过程的命令插入这个已形成命令链的I/O过程,这就是在目标器内的优化方法。举个例子,当一个逻辑数据块需要被读 取一修改一写回时,这个做法就变得十分有用。而且,连接的命令允许使用逻辑数据块的相对地址。 表10 控制字节的格式 位数 7 6 5 4 3 2 1 0   厂商自定 保留 ACA 状态 连接 标志位必须和连接命令一起使用。这引起在连接的命令执行结束之后发送服务响应LINKED COMMAND COMPLETE(WITH FLAG)(0BH),而不是发送服务响应LINKED COMMAND COMPLETE(OAH)。这样,你就可以在一个命令链中标出一个特定的命令。 在SCSI-3中出现了新的标志位:ACA位。ACA是偶然事件自动通信(auto contingent allegiance)的缩写,它是在命令执行过程中万一发生错误时LUN所采取的一种措施。如果ACA位没有被置"1",那么只要下一个命令从同一个启 动器中发出时,该错误状态就被取消。如果ACA位被置"1",它就会阻止取消错误状态的行动并保持这种状态。 Mass Storage设备对象结构 每个大存储设备用一个对象结构us_data来描述它的设备、管道、SCSI接口、传输、协议等各方面的信息及处理函数。 结构us_data列出如下(在drivers/usb/storage/usb.h中): /*我们提供了一个DMA映射I/O buffer给小USB传输使用。CB[I]需要12字节buffer,Bulk-only需要31字节buffer,但Freecom需要64字节buffer,因此,我们分配了64字节的buffer。*/ #define US_IOBUF_SIZE 64   typedef int (*trans_cmnd)(struct scsi_cmnd *, struct us_data*); typedef int (*trans_reset)(struct us_data*); typedef void (*proto_cmnd)(struct scsi_cmnd*, struct us_data*); typedef void (*extra_data_destructor)(void *); //格外的数据析构函数    struct us_data { //工作设备、接口结构及各种管道 struct semaphore dev_semaphore; //保护pusb_dev struct usb_device *pusb_dev;   //从类usb_device继承 struct usb_interface *pusb_intf; //从类usb_interface继承 struct us_unusual_dev *unusual_dev; //常用的设备链表定义 unsigned long flags; /* 最初来自过滤器的标识*/ unsigned int send_bulk_pipe; /* 缓存的管道值*/ unsigned int recv_bulk_pipe; unsigned int send_ctrl_pipe; unsigned int recv_ctrl_pipe; unsigned int recv_intr_pipe;   //设备的信息 char vendor[USB_STOR_STRING_LEN]; //供应商信息 char product[USB_STOR_STRING_LEN]; //产品信息 char serial[USB_STOR_STRING_LEN]; //产品序列号 char *transport_name; //传输协议名 char *protocol_name; //协议名 u8 subclass;  //子类 u8 protocol; u8 max_lun; //最大的逻辑单元   u8 ifnum; //接口数 u8 ep_bInterval; //中断传输间隔   //设备的函数指针 trans_cmnd transport;   //传输函数 trans_reset transport_reset; //传输设备复位 proto_cmnd proto_handler; //协议处理函数   //SCSI接口 struct Scsi_Host *host; //虚拟SCSI主机数据结构 struct scsi_cmnd *srb; //当前SCSI命令描述块   //线程信息 int pid; //控制线程   //控制和批量通信数据 struct urb *current_urb; //USB请求 struct usb_ctrlrequest *cr; //USB控制请求的setup数据 struct usb_sg_request current_sg; //碎片-收集请求 unsigned char *iobuf; //I/O buffer dma_addr_t cr_dma; //控制请求数据buffer的DMA地址 dma_addr_t iobuf_dma; // I/O buffer的DMA地址   //互斥保护和同步结构 struct semaphore sema; /* to sleep thread on */ struct completion notify; //线程开始/结束时发通知出去 wait_queue_head_t dev_reset_wait; //在复位期间等待 wait_queue_head_t scsi_scan_wait; //在SCSI扫描前等待  struct completion scsi_scan_done; //SCSI扫描线程结束时通知处理函数    //子驱动程序信息 void *extra; //任何格外的数据 extra_data_destructor extra_destructor;//格外的数据析构函数  }; Mass Storage设备初始化 函数usb_stor_init注册和初始化大存储驱动程序。函数usb_stor_init列出如下(在drivers/usb/storage/usb.c中): static int __init usb_stor_init(void) { int retval; printk(KERN_INFO "Initializing USB Mass Storage driver...n");   //注册驱动程序,如果操作失败,返回负值的错误代码  retval = usb_register(&usb_storage_driver); if (retval == 0) printk(KERN_INFO "USB Mass Storage support registered.n");   return retval; } 大存储设备驱动程序结构实例usb_storage_driver列出如下: struct usb_driver usb_storage_driver = { .owner = THIS_MODULE, .name = "usb-storage", .probe = storage_probe, //探测并初始化设备 .disconnect = storage_disconnect,//断开连接处理函数 .id_table = storage_usb_ids, }; 在usb_device_id结构类型数组中storage_usb_ids定义了设备类、子类及命令块集的协议类型。部分列出如下: static struct usb_device_id storage_usb_ids [] = { …… /* Bulk-only transport for all SubClass values */ { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_RBC, US_PR_BULK) }, { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_8020, US_PR_BULK) }, { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_QIC, US_PR_BULK) }, { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_UFI, US_PR_BULK) }, { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_8070, US_PR_BULK) }, #if !defined(CONFIG_BLK_DEV_UB) && !defined(CONFIG_BLK_DEV_UB_MODULE) { USB_INTERFACE_INFO(USB_CLASS_MASS_STORAGE, US_SC_SCSI, US_PR_BULK) }, #endif   /* Terminating entry */ { } }; 探测函数storage_probe分析 函数storage_probe 探测看是否能驱动一个新连接的USB设备。创建了大存储设备控制线程usb_stor_control_thread和SCSI设备后期扫描线程 usb_stor_scan_thread。函数storage_probe在控制线程中通过虚拟SCSI主机控制器发送SCSI命令,经Bulk- Only协议封装后,再填充为URB包,传送给USB核心层来发送给设备。函数storage_probe调用层次图如图2所示。下面按照这个图分析函数 storage_probe。 图2 函数storage_probe调用层次图 函数storage_probe列出如下(在drivers/usb/storage/usb.c中): static int storage_probe(struct usb_interface *intf, const struct usb_device_id *id) { struct us_data *us; const int id_index = id - storage_usb_ids; int result;   US_DEBUGP("USB Mass Storage device detectedn");   //分析us_data结构对象空间 us = (struct us_data *) kmalloc(sizeof(*us), GFP_KERNEL); if (!us) { printk(KERN_WARNING USB_STORAGE "Out of memoryn"); return -ENOMEM; } memset(us, 0, sizeof(struct us_data)); init_MUTEX(&(us->dev_semaphore)); init_MUTEX_LOCKED(&(us->sema)); init_completion(&(us->notify)); init_waitqueue_head(&us->dev_reset_wait); init_waitqueue_head(&us->scsi_scan_wait); init_completion(&us->scsi_scan_done);   //将USB设备与结构us_data关联起来   //设置 intf->dev ->driver_data = us,分配buffer result = associate_dev(us, intf); if (result) goto BadDevice;   //得到unusual_devs条目和描述子,初始化us。 //id_index与usb_device_id表中序号匹配,找到表中对应的条目。 get_device_info(us, id_index);   #ifdef CONFIG_USB_STORAGE_SDDR09 if (us->protocol == US_PR_EUSB_SDDR09 || //SDDR-09 的SCM-SCSI桥  us->protocol == US_PR_DPCM_USB) { // CB/SDDR09混合体 //设置配置,STALL在这儿是一个可接受的反应  if (us->pusb_dev->actconfig->desc.bConfigurationValue != 1) { US_DEBUGP("active config #%d != 1 ??n", us->pusb_dev ->actconfig->desc.bConfigurationValue); goto BadDevice; }     //重置配置,重新初始化端点及接口 result = usb_reset_configuration(us->pusb_dev); …… } #endif   //将传输方式、协议和管道设置赋给us result = get_transport(us); if (result) goto BadDevice; result = get_protocol(us); if (result) goto BadDevice; result = get_pipes(us); if (result) goto BadDevice;   //初始化所有需要的动态资源  result = usb_stor_acquire_resources(us); if (result) goto BadDevice; result = scsi_add_host(us->host, &intf->dev); if (result) { printk(KERN_WARNING USB_STORAGE "Unable to add the scsi hostn"); goto BadDevice; }   /*线程usb_stor_scan_thread执行延迟的SCSI设备扫描工作,扫描给定的适配器us->host,扫描通道及目标,扫描探测LUN。*/  result = kernel_thread(usb_stor_scan_thread, us, CLONE_VM); if (result < 0) { printk(KERN_WARNING USB_STORAGE "Unable to start the device-scanning threadn"); scsi_remove_host(us->host); goto BadDevice; }   return 0; …… } 函数usb_stor_acquire_resources初始化所有的需要的动态资源,启动控制线程,函数列出如下(在drivers/usb/storage/usb.c中): static int usb_stor_acquire_resources(struct us_data *us) { int p;   us->current_urb = usb_alloc_urb(0, GFP_KERNEL); //分配urb结构对象空间 if (!us->current_urb) { US_DEBUGP("URB allocation failedn"); return -ENOMEM; }   //当我们执行下两个操作时锁住设备。  down(&us->dev_semaphore);   //仅对于批量设备,得到最大逻辑单元值,   //在SCSI协议模型中,每个逻辑单元用来操作SCSI设备。 if (us->protocol == US_PR_BULK) { p = usb_stor_Bulk_max_lun(us); if (p < 0) { up(&us->dev_semaphore); return p; } us->max_lun = p; }   //如果设备需要初始化,在开始控制线程前初始化设备 if (us->unusual_dev->initFunction) us->unusual_dev->initFunction(us);   up(&us->dev_semaphore);   //因为这是一个新设备,我们需要注册一个设备的虚拟SCSI控制器,   //用来处理SCSI层高层协议。 us->host = scsi_host_alloc(&usb_stor_host_template, sizeof(us)); if (!us->host) { printk(KERN_WARNING USB_STORAGE "Unable to allocate the scsi hostn"); return -EBUSY; }   //设置为SCSI扫描准备的hostdata us->host->hostdata[0] = (unsigned long) us;   //启动控制线程 p = kernel_thread(usb_stor_control_thread, us, CLONE_VM); if (p < 0) { printk(KERN_WARNING USB_STORAGE "Unable to start control threadn"); return p; } us->pid = p;   //等待线程启动 wait_for_completion(&(us->notify));   return 0; } SCSI主机模板结构被用来分配SCSI主机,结构实例usb_stor_host_template列出如下(在drivers/usb/storage/scsiglue.c中): struct scsi_host_template usb_stor_host_template = { //基本的用户使用的接口 .name = "usb-storage", .proc_name = "usb-storage", .proc_info = proc_info, .info = host_info,   //命令接口,仅用于排队 .queuecommand = queuecommand,   //错误及错误退出处理函数 .eh_abort_handler = command_abort, .eh_device_reset_handler = device_reset, .eh_bus_reset_handler = bus_reset,   //排队命令数,每个LUN仅一个命令。  .can_queue = 1, .cmd_per_lun = 1,   /* unknown initiator id */ .this_id = -1,   .slave_alloc = slave_alloc, .slave_configure = slave_configure, //设置一些限制   //能被处理的碎片收集片断数 .sg_tablesize = SG_ALL,   //一次传输的总大小限制到240扇区,即120 KB .max_sectors = 240,   //融合命令... .use_clustering = 1,   //模拟HBA .emulated = 1,   //当设备或总线复位后做延迟操作。  .skip_settle_delay = 1,   //sysfs设备属性 .sdev_attrs = sysfs_device_attr_list,   /* 用于内核模块管理 */ .module = THIS_MODULE }; 线程函数usb_stor_control_thread分析处理SCSI命令请求描述块srb后,调用协议处理函数us->proto_handler来进行封装传输。函数usb_stor_control_thread列出如下: static int usb_stor_control_thread(void * __us) { struct us_data *us = (struct us_data *)__us; struct Scsi_Host *host = us->host;   lock_kernel();   //线程后台化,定向成从init进程继承,这样就去掉了不需要的进程资源 daemonize("usb-storage");   current->flags |= PF_NOFREEZE;   unlock_kernel();   //发信号表示我们已开始了这个线程 complete(&(us->notify));   for(;;) { US_DEBUGP("*** thread sleeping.n"); if(down_interruptible(&us->sema)) break;   US_DEBUGP("*** thread awakened.n");   /* lock the device pointers */ down(&(us->dev_semaphore));   //如果us->srb是NULL, 线程被请求退出。 if (us->srb == NULL) { US_DEBUGP("-- exit command receivedn"); up(&(us->dev_semaphore)); break; }   //锁住SCSI主机控制器 scsi_lock(host);   //命令超时 if (test_bit(US_FLIDX_TIMED_OUT, &us->flags)) { us->srb->result = DID_ABORT flags)) { US_DEBUGP("No command during disconnectn"); goto SkipForDisconnect; }   scsi_unlock(host);   //如果方向标识是未知的,拒绝命令。  if (us->srb->sc_data_direction == DMA_BIDIRECTIONAL) { US_DEBUGP("UNKNOWN data directionn"); us->srb->result = DID_ERROR srb->device->id && !(us->flags & US_FL_SCM_MULT_TARG)) { US_DEBUGP("Bad target number (%d:%d)n", us->srb->device->id, us->srb->device->lun); us->srb->result = DID_BAD_TARGET srb->device->lun > us->max_lun) { US_DEBUGP("Bad LUN (%d:%d)n", us->srb->device->id, us->srb->device->lun); us->srb->result = DID_BAD_TARGET srb->cmnd[0] == INQUIRY) && (us->flags & US_FL_FIX_INQUIRY)) { unsigned char data_ptr[36] = { 0x00, 0x80, 0x02, 0x02, 0x1F, 0x00, 0x00, 0x00};   US_DEBUGP("Faking INQUIRY commandn"); fill_inquiry_response(us, data_ptr, 36); us->srb->result = SAM_STAT_GOOD; }   //得到一个命令,按照功能设备支持的协议来转换SCSI命令 else { US_DEBUG(usb_stor_show_command(us->srb)); us->proto_handler(us->srb, us); }   /* 加锁 */ scsi_lock(host);   //指示命令执行完成 if (us->srb->result != DID_ABORT srb->result); us->srb->scsi_done(us->srb); } else { SkipForAbort: US_DEBUGP("scsi command abortedn"); }   /*如果一个错误退出请求被收到,我们需要发信号表示退出完成了。应该测试TIMED_OUT标识而不是srb->result == DID_ABORT,因为timeout/abort请求可能在所有的USB处理完成后被收到的*/ if (test_bit(US_FLIDX_TIMED_OUT, &us->flags)) complete(&(us->notify));   //完成了在这个命令上的操作 SkipForDisconnect: us->srb = NULL; scsi_unlock(host);   /* 解锁*/ up(&(us->dev_semaphore)); } /* for (;;) */   //通知exit例程我们实际上正在退出操作。 complete_and_exit(&(us->notify), 0); } 对于支持SCSI协议的功能设备来说,us->proto_handler协议处理函数就是函数 usb_stor_transparent_scsi_command,该函数把SCSI命令发送到传输层处理。该函数列出如下(在 drivers/usb/storage/protocol.c中): void usb_stor_transparent_scsi_command(struct scsi_cmnd *srb, struct us_data *us) { //发送命令到传输层 usb_stor_invoke_transport(srb, us);   if (srb->result == SAM_STAT_GOOD) { /* Fix the READ CAPACITY result if necessary */ if (us->flags & US_FL_FIX_CAPACITY) fix_read_capacity(srb); } } 函数usb_stor_invoke_transport是传输例程,它触发传输和基本的错误处理/恢复方法,它被协议层用来实际发送消息到设备并接收响应。 函数usb_stor_invoke_transport列出如下(在drivers/usb/storage/transport.c中): void usb_stor_invoke_transport(struct scsi_cmnd *srb, struct us_data *us) { int need_auto_sense; int result;   //发送命令到传输层 srb->resid = 0; result = us->transport(srb, us);   …… srb->result = SAM_STAT_GOOD;   //决定是否需要auto-sense标识 need_auto_sense = 0;   /*如果我们正在支持CB传输,它不能决定它自己的状态,我们将自动感知(auto-sense),除非操作包括在一个data-in的传输中。设备能通过安装bulk-in管道来发出大多关开data-in错误的信号。*/ if ((us->protocol == US_PR_CB || us->protocol == US_PR_DPCM_USB) && srb->sc_data_direction != DMA_FROM_DEVICE) { US_DEBUGP("-- CB transport device requiring auto-sensen"); need_auto_sense = 1; }   //如果有一个操作失败,我们将自动做REQUEST_SENSE。   //注意在传输机制中在“失败”和“错误”之间的命令是不同的。 if (result == USB_STOR_TRANSPORT_FAILED) { US_DEBUGP("-- transport indicates command failuren"); need_auto_sense = 1; }   …… //做auto-sense if (need_auto_sense) { int temp_result; void* old_request_buffer; unsigned short old_sg; unsigned old_request_bufflen; unsigned char old_sc_data_direction; unsigned char old_cmd_len; unsigned char old_cmnd[MAX_COMMAND_SIZE]; unsigned long old_serial_number; int old_resid;   US_DEBUGP("Issuing auto-REQUEST_SENSEn");   //存储旧的命令 memcpy(old_cmnd, srb->cmnd, MAX_COMMAND_SIZE); old_cmd_len = srb->cmd_len;   //设置命令和LUN memset(srb->cmnd, 0, MAX_COMMAND_SIZE); srb->cmnd[0] = REQUEST_SENSE; srb->cmnd[1] = old_cmnd[1] & 0xE0; srb->cmnd[4] = 18;   //在这儿必须做协议转换 if (us->subclass == US_SC_RBC || us->subclass == US_SC_SCSI) srb->cmd_len = 6; else srb->cmd_len = 12;   //设置传输方向 old_sc_data_direction = srb->sc_data_direction; srb->sc_data_direction = DMA_FROM_DEVICE;   //存buffer中内容 old_request_buffer = srb->request_buffer; srb->request_buffer = srb->sense_buffer;   //设置buffer传输长度 old_request_bufflen = srb->request_bufflen; srb->request_bufflen = 18;   //存碎片收集链表中的碎片数  old_sg = srb->use_sg; srb->use_sg = 0;   //改变序号 – 或非高位 old_serial_number = srb->serial_number; srb->serial_number ^= 0x80000000;   //发生auto-sense命令 old_resid = srb->resid; srb->resid = 0; temp_result = us->transport(us->srb, us);   //恢复命令 srb->resid = old_resid; srb->request_buffer = old_request_buffer; srb->request_bufflen = old_request_bufflen; srb->use_sg = old_sg; srb->serial_number = old_serial_number; srb->sc_data_direction = old_sc_data_direction; srb->cmd_len = old_cmd_len; memcpy(srb->cmnd, old_cmnd, MAX_COMMAND_SIZE);   …… /*设置result,让上层得到此值*/ srb->result = SAM_STAT_CHECK_CONDITION;   //如果ok,显示它们,sense buffer清0,这样不让高层认识到我们做了一个自愿的auto-sense  if (result == USB_STOR_TRANSPORT_GOOD && /* Filemark 0, ignore EOM, ILI 0, no sense */ (srb->sense_buffer[2] & 0xaf) == 0 && /* 没有ASC或ASCQ */ srb->sense_buffer[12] == 0 && srb->sense_buffer[13] == 0) { srb->result = SAM_STAT_GOOD; srb->sense_buffer[0] = 0x0; } }   //我们传输小于所要求的最小数据量  if (srb->result == SAM_STAT_GOOD && srb->request_bufflen - srb->resid < srb->underflow) srb->result = (DID_ERROR transport_reset(us); } 对于USB Mass Storage相适应的设备来说,us->transport(srb, us)调用的是函数usb_stor_Bulk_transport,该函数列出如下(在drivers/usb/storage/transport.c中): int usb_stor_Bulk_transport(struct scsi_cmnd *srb, struct us_data *us) { struct bulk_cb_wrap *bcb = (struct bulk_cb_wrap *) us->iobuf; struct bulk_cs_wrap *bcs = (struct bulk_cs_wrap *) us->iobuf; unsigned int transfer_length = srb->request_bufflen; unsigned int residue; int result; int fake_sense = 0; unsigned int cswlen; unsigned int cbwlen = US_BULK_CB_WRAP_LEN;   //对于BULK32设备,设置多余字节到0 if ( unlikely(us->flags & US_FL_BULK32)) {

    时间:2019-08-26 关键词: struct linux内核

  • 烧写内核

    1 配置编译linux内核  通过make menuconfig编译内核  make zImage  2 制作uImage  在.../linux-2.6.32.2/arch/arm/boot下面可以找到生成的zImage  把.../u-boot-2010.03-tekkaman-master/tools下的mkimage拷贝到/usr/sbin目录下  #cd /root  #ls -a  #vi .bashrc  加上路径 PATH=$PATH:/usr/sbin   #cd .../linux-2.6.32.2/arch/arm/boot  #mkimage -n 'mini2440' -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008040 -d zImage uImage  此处,注意-a和-e后的地址相差0x40,即64个字节,为了后面,用bootm 30008000来启动内核  从而生成uImage  uImage和zImage的区别是,uImage在zImage的前面加上了64字节的启动数据,供bootm来用,如果是zImage的话,只能通过go命令来启动。  最后,加上可执行权限,将uImage放置到tftpboot目录下。  chmod +x uImage  cp uImage /root/tftpboot 3 安装tftp  3.1 下载tftp服务器、客户端tftp和守护进程xinetd三个包   yum install xinetd tftp tftp-server  3.2 建立tftp工作目录并修改属性(不修改属性会出现error)   cd /   mkdir tftpboot   chmod 777 -R tftpboot/  3.2 配置vi /etc/xinetd.d/tftp   server_args= -s /tftpboot -c    /tftpboot是创建tftpboot的目录,可以修改,我的是/root/src/mini2440/tftpboot   -s,必须加,否则会出现决定路径之类的错误   -c, 是允许客户端更改工作目录下的文件   diable=no  3.4 启动服务   在开启服务之前,务必关闭防火墙service iptables stop   另外需修改/etc/selinux/config文件中的SELINUX="" 为 disabled ,然后重启。否则,出现Transfer timed out错误。   service xinetd start   可能service服务不识别,只需把该命令所在的路径加入路径环境变量即可PATH=$PATH:/sbin  3.5 查看服务   可以通过相关命令查看tftp服务是否启动   netstat -a|grep tftp   udp        0      0 *:tftp                      *:*   说明启动了  3.6 验证tftp服务可以使用   tftp 127.0.0.1   tftp> get uImage //uImage是tftpboot目录下的存在的文件   tftp> q      //退出   如果在执行此命令的目录下有了uImage文件,则说明tftp安装成功。    4 安装NFS  4.1 查看是否安装了NFS   rpm -qa|grep nfs   nfs-utils-lib-1.1.5-3.fc14.i686   nfs-utils-1.2.3-6.fc14.i686   如果有这两个包,则说明已安装,否则,用yum进行安装即可。  4.2 配置NFS   vi /etc/exports   /root/src/mini2440/rootfs 192.168.2.*(rw,sync,no_root_squash)   exports在第一次设置时是空的,在第一行输入上述信息   /root/src/mini2440/rootfs //允许挂载的目录   192.168.2.* //允许192.168.2.网段的访问   (rw,sync)  //访问的模式,可读可写,同步访问  4.3 开启服务   前提还是先关闭防火墙   service nfs start   查看NFS是否开启   service nfs status   关闭服务   service nfs stop  4.4 设置系统自动开启NFS   通过setup,进入system services,找到nfs,然后,按下空格选上,保存即可。   5 设置启动参数  进入u-boot的下载命令界面  print  查看环境变量值  setenv ipaddr=192.168.2.250  setenv serverip=192.168.2.103  setenv gatewayip=192.168.2.1  setenv bootargs noinitrd root=/dev/nfs rw nfsroot=192.168.2.103:/root/src/mini2440/rootfs ip=192.168.2.250:192.168.2.103::255.255.255.0 console=ttySAC0,115200 init=/linuxrc mem=64M 6 下载内核  6.1 首先验证网络   在u-boot里 ping 192.168.2.103   host 192.168.2.103 is alive   证明网络可行  6.2 利用tftp命令下载内核uImage,此uImage是处理好之后放在tftpboot目录下的   tftp 30008000 uImage   此时,uImage下载到了内存的30008000处   启动内核   bootm 30008000   如果根文件系统制作好了,并且放在了nfsroot知道的地方,即主机中开放出来的NFS挂载点   则启动成功。    

    时间:2019-08-26 关键词: linux内核

首页  上一页  1 2 3 下一页 尾页
发布文章

技术子站