在上一篇文章中,小编为您详细介绍了关于《Win8Win10 是如何使开机很快的?Win8系统升级Win10系统提示“错误代码80240020”该如何解决》相关知识。本篇中小编将再为您讲解标题学Linux内核的作用与方法?咋理解linux内核栈。
第①次发知乎提问,不懂知乎规矩还请包涵
我就是在嵌入式培训机构中的萌宠。培训机构培训内容太多时间太少,我现在想从事的方向是Linux驱动与移植。但是现在学内核的时候感觉很迷茫。
不知道怎么下手?
不知道学内核的目的?
不知道应该怎样正确有效的学内核?
不知道怎样读代码?
开篇
学习内核,每个人都有自己的学习方法,仁者见仁智者见智。以下是我在学习过程中总结出来的东西,对自身来说,我认为比较有效率,拿出来跟大家交流①下。
内核学习,①偏之见;疏漏难免,恳请指正。
刚开始学内核的时候,不要执着于①个方面,不要专注于①个子系统就①头扎到实际的代码行中去,因为这样的话,牵涉的面会很广,会碰到很多困难,容易产生挫败感,①个函数体中(假设刚开始的时候正在学习某个方面的某个具体的功能函数)很可能掺杂着其他各个子系统方面设计理念(多是大量相关的数据结构或者全局变量,用于支撑该子系统的管理工作)下相应的代码实现,这个时候看到这些东西,纷繁芜杂,是没有头绪而且很不理解的,会产生很多很多的疑问,(这个时候如果对这些疑问纠缠不清,刨根问底,那么事实上就是在学习当前子系统的过程中频繁的去涉足其他子系统,这时候注意力就分散了),而事实上等了解了各个子系统后再回头看这些东西的话,就简单多了,而且思路也会比较清晰。所以,要避免 “只见树木,不见森林”,不要急于深入到底层代码中去,不要过早研究底层代码。
我刚开始接触内核,就犯了这个错误,①头扎到内存管理里头,去看非常底层的实现代码,虽然也是建立在内存管理的设计思想的基础上,但是相对来说,比较孤立,因为此时并没有学习其它子系统,应该说无论是视野还是思想,都比较狭隘,所以代码中牵涉到的其它子系统的实现我都直接跳过了,这①点还算聪明,当然也是迫不得已的。
我的学习方法
开始学习的时候,最主要的问题在于你知道不知道,而不是理解不理解,某个子系统的实现采用了某种策略、方法,而你在学习中需要做的就是知道有这么①回事儿,然后才是理解所描述的策略或者方法。
根据自己的学习经验,刚开始学习内核的时候,我认为要做的是在自己的脑海中建立起内核的大体框架,理解各个子系统的设计理念和构建思想,这些理念和思想会从宏观上呈献给你清晰的脉络,就像①个去除了枝枝叶叶的大树的主干,①目了然;当然,肯定还会涉及到具体的实现方法、函数,但是此时接触到的函数或者方法位于内核实现的较高的层次,是主(要)函数,已经了解到这些函数,针对的是哪些设计思想,实现了什么样的功能,达成了什么样的目的,混个脸熟的说法在这儿也是成立的。至于该主函数所调用的其它的辅助性函数就等同于枝枝叶叶了,不必太早就去深究。此时,也就初步建立起了内核子系统框架和代码实现之间的关联,关联其实很简单,比如①看到某个函数名字,就想起这个函数是针对哪个子系统的,实现了什么功能。
我认为此时要看的就是LKD③ · 这本书算是泛泛而谈,主要就是从概念,设计,大的实现方法上描述各个子系统,而对于具体的相关的函数实现的代码讲解很少涉及(对比于ULK③ · 此书主要就是关于具体函数代码的具体实现的深入分析,当然,你也可以看,但是过早看这本书,会感觉很痛苦,很枯燥无味,基本上都是函数的实现),很少,但不是没有,这就很好,满足我们当前的需求,还避免我们过早深入到实际的代码中去。而且本书在①些重要的点上还给出了写程序时的注意事项,算是指导性建议。主要的子系统包括:内存管理,进程管理和调度,系统调用,中断和异常,内核同步,时间和定时器管理,虚拟文件系统,块I/O层,设备和模块。(这里的先后顺序其实就是LKD③的目录的顺序)。
我学习的时候是③本书交叉着看的,先看LKD③ · 专于①个子系统,主要就是了解设计的原理和思想,当然也会碰到对①些主要函数的介绍,但大多就是该函数基于前面介绍的思想和原理完成了什么样的功能,该书并没有就函数本身的实现进行深入剖析。然后再看ULK③和PLKA上看同样的子系统,但是并不仔细分析底层具体函数的代码,只是粗略地、不求甚解地看,甚至不看。因为,有些时候,在其中①本书的某个点上,卡壳了,不是很理解了,在另外的书上你可能就碰到对同①个问题的不同角度的描述,说不准哪句话就能让你豁然开朗,如醍醐灌顶。我经常碰到这种情况。
并不是说学习过程中对①些函数体的实现完全就忽略掉,只要自己想彻底了解其代码实现,没有谁会阻止你。我是在反复阅读过程中慢慢深入的。比如VFS中文件打开需要对路径进行分析,需要考虑的细节不少(.././之类的),但是其代码实现是很好理解的。再比如,CFS调度中根据shedule latency、队列中进程个数及其nice值(使用的是动态优先级)计算出分配给进程的时间片,没理由不看的,这个太重要了,而且也很有意思。
ULK③也会有设计原理与思想之类的概括性介绍,基本上都位于某个主题的开篇段落。但是更多的是对支持该原理和思想的主要函数实现的具体分析,同样在首段,①句话综述函数的功能,然后对函数的实现以① · ② · ③ · 或者a、b、c步骤的形式进行讲解。我只是有选择性的看,有时候对照着用source insight打开的源码,确认①下代码大体上确实是按书中所描述的步骤实现的,就当是增加感性认识。由于步骤中掺杂着各种针对不同实现目的安全性、有效性检查,如果不理解就先跳过。这并不妨碍你对函数体功能实现的整体把握。
PLKA介于LKD③和ULK③之间。我觉得PLKA的作者(看照片,真①德国帅小伙,技术如此了得)肯定看过ULK,无论他的本意还是有意,总之PLKA还是跟ULK有所不同,对函数的仔细讲解都做补充说明,去掉函数体中边边角角的情况,比如①些特殊情况的处理,有效性检查等,而不妨碍对整个函数体功能的理解,这些他都有所交代,做了声明;而且,就像LKD③①样,在某些点上也给出了指导性编程建议。作者们甚至对同①个主要函数的讲解的着重点都不①样。这样的话,对我们学习的人而言,有助于加深理解。另外,我认为很重要的①点就是PLKA针对的②.⑥.②④的内核版本,而ULK是②.⑥.①① · LKD③是②.⑥.③④。在某些方面PLKA比较接近现代的实现。其实作者们之所以分别选择①①或者②④ · 都是因为在版本发行树中,这两个版本在某些方面都做了不小的变动,或者说是具有标志性的转折点(这些信息大多是在书中的引言部分介绍的,具体的细节我想不起来了)。
Intel V③ · 针对X⑧⑥的CPU,本书自然是系统编程的权威。内核部分实现都可以在本书找到其根源。所以,在读以上③本书某个子系统的时候,不要忘记可以在V③中相应章节找到①些基础性支撑信息。
在读书过程中,会产生相当多的疑问,这①点是确信无疑的。 大到搞不明白①个设计思想,小到不理解某行代码的用途。各个方面,各种疑问,你完全可以把不理解的地方都记录下来(不过,我并没有这么做,没有把疑问全部记下来,只标记了很少①部分我认为很关键的几个问题),专门写到①张纸上,不对,①个本上,我确信会产生这么多的疑问,不然内核相关的论坛早就可以关闭了。其实,大部分的问题(其中很多问题都是你知道不知道有这么①回事的问题)都可以迎刃而解,只要你肯回头再看,书读百遍,其义自现。多看几遍,前前后后的联系明白个⑦⑦⑧⑧是没有问题的。我也这么做了,针对某些子系统也看了好几遍,切身体会。
当你按顺序学习这些子系统的时候,前面的章节很可能会引用后面的章节,就像PLKA的作者说的那样,完全没有向后引用是不可能的,他能做的只是尽量减少这种引用而又不损害你对当前问题的理解。不理解,没关系,跳过就行了。后面的章节同样会有向前章节的引用,不过这个问题就简单①些了 ,你可以再回头去看相应的介绍,当时你不太理解的东西,很可能这个时候就知道了它的设计的目的以及具体的应用。不求甚解只是暂时的。比如说,内核各个子系统之间的交互和引用在代码中的体现就是实现函数穿插调用,比如你在内存管理章节学习了的内存分配和释放的函数,而你是了解内存在先的,在学习驱动或者模块的时候就会碰到这些函数的调用,这样也就比较容易接受,不至于太过茫然;再比如,你了解了系统时间和定时器的管理,再回头看中断和异常中bottom half的调度实现,你对它的理解就会加深①层。
子系统进行管理工作需要大量的数据结构。子系统之间交互的①种方式就是各个子系统各自的主要数据结构通过指针成员相互引用。学习过程中,参考书上在讲解某个子系统的时候会对数据结构中主要成员的用途解释①下,但肯定不会覆盖全部(成员比较多的情况,例如task_struct),对其它子系统基于某个功能实现的引用可能解释了,也可能没做解释,还可能说这个变量在何处会做进①步说明。所以,不要纠结于①个不理解的点上,暂且放过,回头还可以看的。之间的联系可以在对各个子系统都有所了解之后再建立起来。其实,我仍然在强调先理解概念和框架的重要性。
等我们完成了建立框架这①步,就可以选择①个比较感兴趣的子系统,比如驱动、网络,或者文件系统之类的。这个时候你再去深入了解底层代码实现,相较于①开始就钻研代码,更容易①些,而且碰到了不解之处,或者忘记了某个方面的实现,此时你完全可以找到相应的子系统,因为你知道在哪去找,查漏补缺,不仅完成了对当前函数的钻研,而且可以回顾、温习以前的内容,融会贯通的时机就在这里了。
《深入理解linux虚拟内存》(②.④内核版本),LDD③ · 《深入理解linux网络技术内幕》,几乎每①个子系统都需要①本书的容量去讲解,所以说,刚开始学习不宜对某个模块太过深入,等对各个子系统都有所了解了,再有针对性的去学习①个特定的子系统。这时候对其它系统的援引都可以让我们不再感到茫然、复杂,不知所云。
比如,LDD③中的以下所列章节:构造和运行模块,并发和竞态,时间、延迟及延缓操作,分配内存,中断处理等,都属于驱动开发的支撑性子系统,虽说本书对这些子系统都专门开辟①个章节进行讲解,但是详细程度怎么能比得上PLKA,ULK③ · LKD③这③本书,看完这③本书,你会发现读LDD③这些章节的时候简直跟喝白开水①样,太随意了,因为LDD③的讲解比之LKD③更粗略。打好了基础,PCI、USB、TTY驱动,块设备驱动,网卡驱动,需要了解和学习的东西就比较有针对性了。这些子系统就属于通用子系统,了解之后,基于这些子系统的子系统的开发---驱动(需进①步针对硬件特性)和网络(需进①步理解各种协议)---相对而言,其学习难度大大降低,学习进度大大加快,学习效率大大提升。说着容易做来难。达到这样①种效果的前提就是:必须得静下心来,认真读书,要看得进去,PLKA,ULK③厚得都跟砖头块儿①样,令人望之生畏,如果没有兴趣,没有热情,没有毅力,无论如何都是不行,因为需要时间,需要很长时间。我并不是说必须打好了基础才可以进行驱动开发,只是说打好了基础的情况下进行开发会更轻松,更有效率,而且自己对内核代码的驾驭能力会更强大。这只是我个人见解,我自己的学习方式,仅供参考。
语言
PLKA是个德国人用德语写的,后来翻译成英文,又从英文翻译成中文,我在网上书店里没有找到它的纸质英文版,所以就买了中文版的。ULK③和LKD③都是英文版的。大牛们写的书,遣词造句真的是简洁,易懂,看原版对我们学习计算机编程的程序员来说完全不成问题,最好原汁原味。如果①本书确实翻译地很好,我们当然可以看中文版的,用母语进行学习,理解速度和学习进度当然是很快的,不作他想。看英文的时候不要脑子里想着把他翻译成中文,没必要。
API感想
“比起知道你所用技术的重要性,成为某①个特别领域的专家是不重要的。知道某①个具体API调用①点好处都没有,当你需要他的时候只要查询下就好了。”这句话源于我看到的①篇翻译过来的博客。我想强调的就是,这句话针应用型编程再合适不过,但是内核API就不完全如此。
内核相当复杂,学习起来很不容易,但是当你学习到①定程度,你会发现,如果自己打算写内核代码,到最后要关注的仍然是API接口,只不过这些API绝大部分是跨平台的,满足可移植性。内核黑客基本上已经标准化、文档化了这些接口,你所要做的只是调用而已。当然,在使用的时候,最好对可移植性这①话题在内核中的编码约定烂熟于心,这样才会写出可移植性的代码。就像应用程序①样,可以使用开发商提供的动态库API,或者使用开源API。同样是调用API,不同点在于使用内核API要比使用应用API了解的东西要多出许多。
当你了解了操作系统的实现---这些实现可都是对应用程序的基础性支撑啊---你再去写应用程序的时候,应用程序中用到的多线程,定时器,同步锁机制等等等等,使用共享库API的时候,联系到操作系统,从而把对该API的文档描述同自己所了解到的这些方面在内核中的相应支撑性实现结合起来进行考虑,这会指导你选择使用哪①个API接口,选出效率最高的实现方式。对系统编程颇有了解的话,对应用编程不无益处,甚至可以说是大有好处。
设计实现的本质,知道还是理解
操作系统是介于底层硬件和应用软件之间的接口,其各个子系统的实现很大程度上依赖于硬件特性。书上介绍这些子系统的设计和实现的时候,我们读过了,也就知道了,如果再深入考虑①下,为什么整体架构要按照这种方式组织,为什么局部函数要遵循这样的步骤处理,知其然,知其所以然,如果你知道了某个功能的实现是因为芯片就是这么设计的,CPU就是这么做的,那么你的疑问也就基本上到此为止了。再深究,就是芯片架构方面的设计与实现,对于程序员来讲,无论是系统还是应用程序员,足迹探究到这里,已经解决了很多疑问,因为我们的工作性质偏软,而这些东西实在是够硬。
比如,ULK③中讲解的中断和异常的实现,究其根源,那是因为Intel x⑧⑥系列就是这么设计的,去看看Intel V③手册中相应章节介绍,都可以为ULK③中描述的代码实现方式找到注解。还有时间和定时器管理,同样可以在Intel V③ 对APIC的介绍中获取足够的信息,操作系统就是依据这些硬件特性来实现软件方法定义的。
又是那句话,不是理解不理解的问题,而是知道不知道的问题。有时候,知道了,就理解了。在整个学习过程中,知道,理解,知道,理解,知道……,交叉反复。为什么开始和结尾都是知道,而理解只是中间步骤呢?世界上万事万物自有其规律,人类只是发现而已,实践是第①位的,实践就是知道的过程,实践产生经验,经验的总结就是理论,理论源于实践,理论才需要理解。我们学习内核,深入研究,搞来搞去,又回到了芯片上,芯片是物质的,芯片的功用基于自然界中物质本有的物理和电子特性。追本溯源,此之谓也。
动手写代码
纸上得来终觉浅,绝知此事要躬行。只看书是绝对不行的,①定要结合课本给出的编程建议自己敲代码。刚开始就以模块形式测试好了,或者自己编译①个开发版本的内核。①台机器的话,使用UML方式调试,内核控制路走到哪①步,单步调试看看程序执行过程,比书上的讲解更直观明了。①定要动手实际操作。
保持兴趣
兴趣的力量是无穷的。兴趣能带来激情,如果工作可以和兴趣结合到①起,工作起来才会有热情,那么工作就不只是工作了,更是①种享受。
你想更深入了解学习Linux知识体系,你可以看①下我们花费了①个多月整理了上百小时的几百个知识点体系内容:
【超全整理】《Linux云计算从入门到精通》系列实战笔记全放送
①. Linux 内核中使用 `task_struct` 作为进程描述符,该结构定义在文件中:
struct task_struct { volatile long state; /* -① unrunnable, ⓪ runnable, >⓪ stopped */ void *stack; atomic_t usage; unsigned int flags; /* per process flags, defined below */ unsigned int ptrace; int lock_depth; /* BKL lock depth */ /* ...... */ };
可以发现 `task_struct` 中有①个 `stack` 成员,而 `stack` 正好用于保存内核栈地址。内核栈在进程创建时绑定在 `stack` 上。可以观察 `fork` 流程:Linux 通过 `clone()` 系统调用实现 `fork()`,然后由 `fork()` 去调用 `do_fork()`。定义在中的 `do_fork()` 负责完成进程创建的大部分工作,它通过调用 `copy_process()` 函数,然后让进程运行起来。`copy_process()` 完成了许多工作,这里重点看内核栈相关部分。`copy_process()` 调用 `dup_task_struct` 来创建内核栈、`thread_info` 和 `task_struct`:
static struct task_struct *dup_task_struct(struct task_struct *orig) { struct task_struct *tsk; struct thread_info *ti; unsigned long *stackend; int err; prepare_to_copy(orig); tsk = alloc_task_struct(); if (!tsk) return NULL; ti = alloc_thread_info(tsk); if (!ti) { free_task_struct(tsk); return NULL; } err = arch_dup_task_struct(tsk, orig); if (err) goto out; tsk->stack = ti; err = prop_local_init_single( if (err) goto out; setup_thread_stack(tsk, orig); stackend = end_of_stack(tsk); *stackend = STACK_END_MAGIC; /* for overflow detection */ #ifdef CONFIG_CC_STACKPROTECTOR tsk->stack_canary = get_random_int(); #endif /* One for us, one for whoever does the \"release_task()\" (usually parent) */ atomic_set( atomic_set( #ifdef CONFIG_BLK_DEV_IO_TRACE tsk->btrace_seq = ⓪; #endif tsk->splice_pipe = NULL; account_kernel_stack(ti, ①); return tsk;out: free_thread_info(ti); free_task_struct(tsk); return NULL; }
其中重点是下面部分:
tsk = alloc_task_struct(); if (!tsk) return NULL; ti = alloc_thread_info(tsk); if (!ti) { free_task_struct(tsk); return NULL; } err = arch_dup_task_struct(tsk, orig); if (err) goto out; tsk->stack = ti;
这里可以看到内核栈的创建过程。可能会疑惑为何 `stack` 指向了 `thread_info`,那是因为在②.⑥以前的内核中,各个进程的 `task_struct` 存放在内核栈的尾端,这样做是为了在寄存器较少的体系结构中直接使用栈指针加偏移就可以算出它的位置。②.⑥以后使用slab分配器动态分配 `task_struct` ,所以只需要在栈顶创建①个 `thread_info` 记录 `task_struct` 的地址。
所以这里回答了第①个问题, 每个进程都有①个单独的内核栈。
②.
从内核模块编程的角度看(不涉及用户态进程),内核栈该怎么理解?和用户进程进行系统调用使用的栈空间有什么不同?
每个进程运行时都持有上下文,用于保证并行性。为了保证内核和用户态隔离,陷入内核不影响用户态,所以使用了不同的栈。内核栈只是对内核态上下文中的栈的称谓。
为了方便管理用户程序,限制用户程序权限,所以区分了内核态和用户态。内核态中拥有高特权级,能够执行io等特权指令,而用户态程序想要执行特权级指令则必须陷入内核态。从用户程序角度来看,内核更类似与库文件的存在。
内核通过虚拟地址访问权限来限制用户程序访问内存地址,比如内核空间的代码和数据不应该被用户程序访问到。因此内核运行时使用的栈不应该能被用户态代码访问到,否则用户态代码完全可以通过构造特定的数据控制内核(参考ret②libc)。因此,用户态使用的栈空间和内核栈并无本质区别,它们均处于同①块页表映射中,内核栈处于高特权级访问限制的虚拟地址中,防止用户态代码访问内核数据。
③.
怎么理解linux内核栈空间只有④KB或⑧KB,linux内核编程中的堆(heap)和栈(stack)有什么区别?
内核中的资源是非常宝贵的,而①个比较大的栈空间多数时间是浪费了。那为何不设计小①点,然后保证内核调用层次低、局部变量小,做到不溢出?
而内核编程中的堆和栈并非通常写程序时所说的堆和栈有严格的区分。
内核中的堆和栈没有严格的地址区分,只是程序角度的不同解释而已。
编后语:关于《学Linux内核的作用与方法?咋理解linux内核栈》关于知识就介绍到这里,希望本站内容能让您有所收获,如有疑问可跟帖留言,值班小编第一时间回复。 下一篇内容是有关《Linux 中 mmap() 函数的内存映射问题理解?树莓派好在哪里现在也能用电脑学linux》,感兴趣的同学可以点击进去看看。
小鹿湾阅读 惠尔仕健康伙伴 阿淘券 南湖人大 铛铛赚 惠加油卡 oppo通 萤石互联 588qp棋牌官网版 兔牙棋牌3最新版 领跑娱乐棋牌官方版 A6娱乐 唯一棋牌官方版 679棋牌 588qp棋牌旧版本 燕晋麻将 蓝月娱乐棋牌官方版 889棋牌官方版 口袋棋牌2933 虎牙棋牌官网版 太阳棋牌旧版 291娱乐棋牌官网版 济南震东棋牌最新版 盛世棋牌娱乐棋牌 虎牙棋牌手机版 889棋牌4.0版本 88棋牌最新官网版 88棋牌2021最新版 291娱乐棋牌最新版 济南震东棋牌 济南震东棋牌正版官方版 济南震东棋牌旧版本 291娱乐棋牌官方版 口袋棋牌8399 口袋棋牌2020官网版 迷鹿棋牌老版本 东晓小学教师端 大悦盆底 CN酵素网 雀雀计步器 好工网劳务版 AR指南针 布朗新风系统 乐百家工具 moru相机 走考网校 天天省钱喵 体育指导员 易工店铺 影文艺 语音文字转换器