linux内核篇-操作系统综述和系统初始化

为什么要学linux内核?
1、应用很广。什么云计算,虚拟化,docker,k8s,大数据,都是基于Linux的;以及我向阿里的长辈请教,他说linux是他们最常用的。
2、学习优秀数据结构和设计模式的落地实践。Linux开源,是智慧的结晶,比如一个简单的文件操作就涉及到从应用层、系统调用层、进程文件操作抽象层、虚拟文件系统层、具体文件系统层、缓存层、设备 I/O 层的完美分层机制。我们可能需要根据公司的现实需求定制组件,就需要从这里面学习借鉴了;
3、能让你事半功倍地学会新技术。Linux 是一个生态,里面丰富多彩。很多大牛都是基于 Linux 来开发各种各样的软件。数据库 MySQL、PostgreSQL,消息队列 RabbitMQ、Kafka,大数据 Hadoop、Spark,虚拟化 KVM、Openvswitch,容器 Kubernetes、Docker,这些软件都会默认提供 Linux 下的安装、使用、运维手册,都会默认先适配 Linux。

我总结了一下,在整个Linux的学习过程中,要爬的坡有六个,分别是:熟练使用Linux命令行、使用Linux进行程序设计、了解Linux内核机制、阅读Linux内核代码、实验定制Linux组件,以及最后落到生产实践上。以下是我为你准备的爬坡秘籍以及辅助的书单弹药。

关于linux命令行的学习,就不用多说了,需要自己多了解。以及还有灵活的管道,sed,awk正则表达式,周期性任务等。

第二个坡:通过系统调用或者glibc,学会自己进行程序设计
Linux有哪些系统调用,每一模块的第一节,我还会讲解这一模块的常用系统调用,以及如何编程调用这些系统调用。

了解Linux内核机制,反复研习重点突破。
进一步了解内核的原理,有助于你更好地使用命令行和进行程序设计,能让你的运维和开发水平上升一个层次,但是我不建议你直接看代码,因为Linux代码量太大,很容易迷失,找不到头绪。最好的办法是,先了解一下Linux内核机制,知道基本的原理和流程就可以了。
一旦学起来的时候,你会发现,Linux内核机制也非常复杂,而且其中相互关联。比如说,进程运行要分配内存,内存映射涉及文件的关联,文件的读写需要经过块设备,从文件中加载代码才能运行起来进程。这些知识点要反复对照,才能理清。
但是一旦爬上这个坡,你会发现Linux这个复杂的系统开始透明起来。无论你是运维,还是开发,你都能大概知道背后发生的事情,并在出现异常的情况时,比较准确地定位到问题所在。

阅读Linux内核代码,聚焦核心逻辑和场景

在了解内核机制的时候,你肯定会遇到困惑的地方,因为理论的描述和提炼虽然能够让你更容易看清全貌,但是容易让你忽略细节。例如你是研究虚拟化的,就重点看KVM的部分;如果你是研究网络的,就重点看内核协议栈的部分。

第五个坡:实验定制化Linux组件,已经没人能阻挡你成为内核开发工程师了
纸上得来终觉浅,绝知此事要躬行。从只看内核代码,到上手修改内核代码,这又是一个很大的坎。这相当于蒸馒头的人为了定制口味,要开始修改面粉生产流程了。

最后一个坡:面向真实场景的开发,实践没有终点。

这里我们只要学习的就是系统调用以及内核整体架构的介绍。

最基础的一些命令介绍
用户和登录
切换用户 su+用户名 (普通切换到root或者另一个要密码)
Root 是 Linux 下的系统管理员,有最高的操作权限。对应的密码是在安装操作系统的过程中设置的;假如要修改密码,使用 passwd 命令;
Linux 下创建其他用户用 useradd 命令
useradd Leooel
创建后需要自己调用 passwd Leooel 来设置密码,再进行登录。

文件浏览相关
cd,ls,硬链接软连接等等基础知识。
安装文件相关
CentOS 下面使用 rpm -i jdk-XXX_linux-x64_bin.rpm进行安装
凭借rpm -qa就可以查看安装的软件列表,-q 就是 query,a 就是 all,-l 就是list 如果真的去运行的话,你会发现这个列表很长很长,很难找到你想找的东西,你可以用一个很好用的搜索工具 grep,通过关键词进行搜索。
rpm -qa | grep jdk
如果你不知道关键词,可以使用rpm -qa | more和rpm -qa | less这两个命令,它们可以将很长的结果分页展示出来。这样你就可以一个个来找了。

如果要删除,可以用rpm -e
Linux 也有自己的软件管家,CentOS 下面是 yum,Ubuntu 下面是 apt-get。

其实无论是先下载再安装,还是通过软件管家进行安装,都是下载一些文件,然后将这些文件放在某个路径下,然后在相应的配置文件中配置一下。
对应 Linux,主执行文件会放在 /usr/bin 或者 /usr/sbin 下面,其他的库文件会放在 /var 下面,配置文件会放在 /etc 下面。
(rpm命令和yum命令都可以用来安装软件。但与yum命令最大的区别为yum命令在安装软件时如果碰到了依赖性的问题,yum会去主动尝试解决依赖性,如果解决不了才会反馈给用户。而rpm命令一旦遇到了依赖性的问题不会去解决依赖性,而是直接反馈给用户,让用户自行解决,所以yum好一点,经常遇到安装某个库需要很多依赖,那么yum会自动下载很方便

运行程序相关
前台运行
Linux 不是根据后缀名来执行的。它的执行条件是这样的:只要文件有 x 执行权限,都能到文件所在的目录下,通过./filename运行这个程序。当然,如果放在 PATH 里设置的路径下面,就不用./ 了,直接输入文件名就可以运行了,Linux 会帮你找。
这是Linux 执行程序最常用的一种方式,通过 shell 在交互命令行里面运行。这种模式的缺点是,一旦当前的交互命令行退出,程序就停止运行了,不能用来运行那些需要“永远“在线的程序,适合运行一些简单的命令

后台运行
要学会一些运行的基本命令(bg,fg,&,nohup,jobs等等)
如何让一个任务在后台执行:&加在一个命令的最后,可以把这个命令放在后台执行
jobs用于查看当前终端后台运行的任务
ctrl+Z也可以将一个正在前台执行的命令暂停,并且放到后台(不过在后台是暂停stop的)
fg命令 将后台中的命令调至前台继续运行 如果后台中有多个命令,可以先用jobs查看jobnun,然后用 fg %jobnum 将选中的命令调出。
bg命令 将一个在后台暂停的命令,变成在后台继续执行,比如crtl+Z之后的那个进程。
kill命令:结束进程 通过jobs命令查看jobnum,然后执行 kill %jobnum 或者通过ps命令查看进程号PID,然后执行 kill %PID 如果前台进程的话直接crtl+C就可以终止了。
我们往往使用 nohup 命令。这个命令的意思是 no hang up(不挂起),也就是说,当前交互命令行退出的时候,程序还要在。

以服务的方式运行
例如常用的数据库 Mysql 就是以这种方式运行。
例如在 Ubuntu 中,我们可以通过 apt-get install mysql-server 的方式安装 MySQL,然后通过命令systemctl start mysql启动 MySQL,通过systemctl enable mysql 设置开机启动。之所以成为服务并且能够开机启动,是因为在 /lib/systemd/system 目录下会创建一个 XXX.service 的配置文件,里面定义了如何启动、如何关闭。

几个常见的系统调用
创建进程的系统调用叫作 fork。在 Linux 里,当父进程调用 fork 创建进程的时候,子进程将各个子系统为父进程创建的数据结构也全部拷贝了一份,甚至连程序代码也是拷贝过来的。按理说,如果不进行特殊的处理,父进程和子进程都按相同的程序代码进行下去,这样就没有意义了。

所以,我们往往会这样处理:对于 fork 系统调用的返回值,如果当前进程是子进程,就返回 0;如果当前进程是父进程,就返回子进程的进程号。这样首先在返回值这里就有了一个区分,然后通过 if-else 语句判断,如果是父进程,还接着做原来应该做的事情;如果是子进程,需要请求另一个系统调用execve来执行另一个程序,这个时候,子进程和父进程就彻底分道扬镳了,也即产生了一个分支(fork)了。

有时候,父进程要关心子进程的运行情况,有个系统调用waitpid,父进程可以调用它,将子进程的进程号作为参数传给它,这样父进程就知道子进程运行完了没有,成功与否。

会议室管理与内存管理
在操作系统中,每个进程都有自己的内存,互相之间不干扰,有独立的进程内存空间。

对于进程的内存空间来讲,放程序代码的这部分,我们称为代码段;放进程运行中产生数据的这部分,我们称为数据段,其中局部变量的部分,在当前函数执行的时候起作用,当进入另一个函数时,这个变量就释放了;也有动态分配的,会较长时间保存,指明才销毁的,这部分称为堆(Heap)。

这里我们介绍两个在堆里面分配内存的系统调用,brk和mmap。
当分配的内存数量比较小的时候,使用 brk,会和原来的堆的数据连在一起,这就像多分配两三个工位,在原来的区域旁边搬两把椅子就行了。当分配的内存数量比较大的时候,使用 mmap,会重新划分一块区域,也就是说,当办公空间需要太多的时候,索性来个一整块。(malloc分配就是这样的,原因是因为brk申请的释放其实是到内存池,下一次可以继续使用,如果每次都用mmap,每次都会有运行态的切换,而且第一次访问虚拟地址还会有缺页中断产生;为什么不都用brk,因为brk频繁调用,会产生内存碎片的)

档案库管理与文件管理
文件管理其实花样不多,无非是创建、打开、读、写等。对于文件的操作,下面这六个系统调用是最重要的:
对于已经有的文件,可以使用open打开这个文件,close关闭这个文件;
对于没有的文件,可以使用create创建文件;
打开文件以后,可以使用lseek跳到文件的某个位置;
可以对文件的内容进行读写,读的系统调用是read,写是write。
Linux 里有一个特点,那就是一切皆文件。文件操作是贯穿始终的,这也是“一切皆文件”的优势,就是统一了操作的入口,提供了极大的便利。

项目异常处理与信号处理
当项目遇到异常情况,例如项目中断,做到一半不做了。这时候就需要发送一个信号(Signal)给项目组。经常遇到的信号有以下几种:

在执行一个程序的时候,在键盘输入“CTRL+C”,这就是中断的信号,正在执行的命令就会中止退出;

如果非法访问内存,例如你跑到别人的会议室,可能会看到不该看的东西;
硬件故障,设备出了问题,当然要通知项目组;
用户进程通过kill函数,将一个用户信号发送给另一个进程。
当项目组收到信号的时候,项目组需要决定如何处理这些异常情况。
对于一些不严重的信号,可以忽略,该干啥干啥,但是像 SIGKILL(用于终止一个进程的信号)和 SIGSTOP(用于中止一个进程的信号)是不能忽略的,可以执行对于该信号的默认动作。每种信号都定义了默认的动作,例如硬件故障,默认终止;也可以提供信号处理函数,可以通过sigaction系统调用,注册一个信号处理函数。
提供了信号处理服务,项目执行过程中一旦有变动,就可以及时处理了。

项目组间沟通与进程间通信(管道、消息队列、共享内存、信号量、信号、socket)
当某个项目比较大的时候,可能分成多个项目组,不同的项目组需要相互交流、相互配合才能完成,这就需要一个项目组之间的沟通机制。项目组之间的沟通方式有很多种,我们来一一规划。
首先就是发个消息,不需要一段很长的数据,这种方式称为消息队列(Message Queue)。由于一个公司内的多个项目组沟通时,这个消息队列是在内核里的,我们可以通过msgget创建一个新的队列,msgsnd将消息发送到消息队列,而消息接收方可以使用msgrcv从队列中取消息。
当两个项目组需要交互的信息比较大的时候,可以使用共享内存的方式,可以通过shmget创建一个共享内存块,通过shmat将共享内存映射到自己的内存空间,然后就可以读写了。
但是,两个项目组共同访问一个会议室里的数据,就会存在“竞争”问题。如果大家同时修改同一块数据咋办?这就需要有一种方式,让不同的人能够排他地访问,这就是信号量的机制 Semaphore。
这个机制比较复杂,我这里说一种简单的场景。对于只允许一个人访问的需求,我们可以将信号量设为 1。当一个人要访问的时候,先调用sem_wait。如果这时候没有人访问,则占用这个信号量,他就可以开始访问了。如果这个时候另一个人要访问,也会调用sem_wait。由于前一个人已经在访问了,所以后面这个人就必须等待上一个人访问完之后才能访问。当上一个人访问完毕后,会调用sem_post将信号量释放,于是下一个人等待结束,可以访问这个资源了。

中介与 Glibc
为了对用户更友好,我们还可以使用中介Glibc,有事情找它就行,它会转换成为系统调用,帮你调用。
Glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库。Glibc 为程序员提供丰富的 API,除了例如字符串处理、数学运算等用户态服务之外,最重要的是封装了操作系统提供的系统服务,即系统调用的封装。
每个特定的系统调用对应了至少一个 Glibc 封装的库函数,比如说,系统提供的打开文件系统调用 sys_open 对应的 open 函数。

从BIOS到bootloader(内核加载的过程)

linux开机过程:
1、BIOS时期。它是固化在ROM芯片上的程序,这个阶段主要完成的是1、POST自检,检查CPU和内存硬件;2.检查正常就加载BIOS到内存中去,BIOS会按照启动顺序去查找第一个磁盘头的MBR信息,并加载和执行MBR中的Bootloader程序,若第一个磁盘不存在MBR,则会继续查找第二个磁盘(可以自己设置启动顺序,比如装系统就可以U盘启动)

2、Bootloader时期
(操作系统在哪儿呢?一般都会在安装在硬盘上,在 BIOS 的界面上。你会看到一个启动盘的选项。启动盘有什么特点呢?它一般在第一个扇区,占 512 字节,而且以 0xAA55 结束。这是一个约定,当满足这个条件的时候,就说明这是一个启动盘,在 512 字节以内会启动相关的代码。 )
Boot Loader只是一个概念,代表引导装载程序的意思,操作系统必须要有Boot Loader才能加载操作系统的内核;
而LILO和grub都是都是实实在在的、使用广泛的引导装载程序
MBR存在于可启动磁盘的0磁道0扇区,占用512字节,它主要用来告诉计算机从选定的可启动设备的哪个分区来加载引导加载程序(Boot loader),MBR中存在如下内容:

3、启动init进程(内核初始化过程),还包括中断。内存,调度器初始化等
4、1号进程启动,完成文件系统驱动加载,运行各种系统初始化脚本,比如你就看到登录界面了。

内核初始化
开始内核初始化工作了,源码位置位于init/main.c中的**start_kernel()**相当于main函数了。
创建0号进程:INIT_TASK(init_task)
异常处理类中断服务程序挂接:trap_init()
内存初始化:mm_init()
调度器初始化sched_init()
剩余初始化:rest_init()

0号进程的创建
  start_kernel()上来就会运行 set_task_stack_end_magic(&init_task)创建初始进程。这是唯一一个没有通过 fork 或者 kernel_thread产生的进程,是进程列表的第一个。
init_task采用了gcc的结构体初始化方式为其进行了直接赋值生成。
而 set_task_stack_end_magic(&init_task)函数的源码如下(只有两行),主要是通过end_of_stack()获取栈边界地址,然后把栈底地址设置为STACK_END_MAGIC**,作为栈溢出的标记**。每个进程创建的时候,系统会为这个进程创建2个页大小的内核栈。

init_task是静态定义的一个进程,也就是说当内核被放入内存时,它就已经存在,它没有自己的用户空间,一直处于内核空间中运行,并且也只处于内核空间运行。0号进程用于包括内存、页表、必要数据结构、信号、调度器、硬件设备等的初始化。当它执行到最后(剩余初始化)时,将start_kernel中所有的初始化执行完成后,会在内核中启动一个kernel_init内核线程和一个kthreadd内核线程,kernel_init内核线程执行到最后会通过execve系统调用执行转变为我们所熟悉的init进程,而kthreadd内核线程是内核用于管理调度其他的内核线程的守护线程。在最后init_task将变成一个idle进程,用于在CPU没有进程运行时运行它,它在此时仅仅用于空转。

总结:init进程的创建过程,首先是调用start_kernel函数,会调用一个xxx函数,参数是init_task。这时静态的进程就是在内核放到内存就有的,它会完成一些初始化工作(内存,数据结构 信号等等),然后会启动两个线程,一个线程execve变成init进程,另一个就变成守护进程了。initstack最后会变成idle进程,CPU空转时运行。 xxx函数就是获取栈边界并溢出标记。

中断初始化
  由代码可见,trap_init()设置了很多的中断门(Interrupt Gate),用于处理各种中断,如系统调用的中断门set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_32)。

内存初始化
 内存相关的初始化内容放在mm_init()中进行。此处不展开说明这些函数,留待后面内存管理部分详细分析各个部分。

调度器初始化
  调度器初始化通过sched_init()完成。举个例子,比如前面的init_task转变为idle进程就是一个调度策略,需要调度器初始化的。

剩余初始化
  rest_init是非常重要的一步,主要包括了区分内核态和用户态、初始化1号进程和初始化2号进程
 x86 提供了分层的权限机制,把区域分成了四个 Ring,越往里权限越高,越往外权限越低。操作系统很好地利用了这个机制,将能够访问关键资源的代码放在 Ring0,我们称为内核态(Kernel Mode);将普通的程序代码放在 Ring3,我们称为用户态(User Mode)。中间是设备驱动。

rest_init() 的一大工作是,用 kernel_thread(kernel_init, NULL, CLONE_FS)创建第二个进程,这个是 1 号进程。1 号进程对于操作系统来讲,有“划时代”的意义,因为它将运行一个用户进程,并从此开始形成用户态进程树。(加载文件系统驱动,完成系统初始化)
 kernel_thread()代码如下所示,可见其中最主要的是第一个参数指针函数fn决定了栈中的内容,根据fn的不同将生成1号进程和后面的2号进程。

1号进程有一系列的系统调用,实现的是从内核态到用户态的转变,具体不说了。
注意的是,1号进程运行的是一个文件(linux一切皆文件)。
文件系统一定是在一个存储设备上的,例如硬盘。Linux 访问存储设备,要有驱动才能访问。如果存储系统数目很有限,那驱动可以直接放到内核里面,反正前面我们加载过内核到内存里了,现在可以直接对存储系统进行访问。但是存储系统越来越多了,如果所有市面上的存储系统的驱动都默认放进内核,内核就太大了。这该怎么办呢?
  我们只好先弄一个基于内存的文件系统。内存访问是不需要驱动的,这个就是 ramdisk。这个时候,ramdisk 是根文件系统。然后,我们开始运行 ramdisk 上的 /init。等它运行完了就已经在用户态了。/init 这个程序会先根据存储系统的类型加载驱动,有了驱动就可以设置真正的根文件系统了。有了真正的根文件系统,ramdisk 上的 /init 会启动文件系统上的 init。接下来就是各种系统的初始化。启动系统的服务,启动控制台,用户就可以登录进来了。

初始化2号进程
函数名 thread 可以翻译成“线程”,这也是操作系统很重要的一个概念。从内核态来看,无论是进程,还是线程,我们都可以统称为任务(Task),都使用相同的数据结构,平放在同一个链表中。这里的函数kthreadd,负责所有内核态的线程的调度和管理,是内核态所有线程运行的祖先。
第2号进程kthreadd进程由第0号进程通过kernel_thread()创建,并始终运行在内核空间, 负责所有内核线程的调度和管理。(循环检查全局内核线程链表,当我们调用kernel_thread创建内核线程时,新线程会被加入到此链表中)
还有一个特点就是kernel_thread创建内核线程不会立刻运行,需要手工 wake up.

总结一下,内核初始化过程;
首先就是区分一下用户态和内核态(基于X86的ring结构);然后就是0号进程(具体是xxx(init_task),xxx用来获取栈边界做标记的,init_task是固化的加载到内存就执行,初始化一些必要的数据结构,完成后,就启动两个线程,第一个就是kernel_init所谓的0号进程,另一个就是通过kernel_thread创建kthreadd内核线程,他负责管理所有内核线程的调度。比如1号线程就是通过kernel_thread创建的,完成了用户进程,也就是内核态到用户态转换,其中由于文件必须存储在硬盘,所以必须有驱动,但是内核占据空间小,容不下各种各样的驱动,所以有了基于内存的文件系统,也有一个init程序用来加载实际的驱动和根文件系统,完成用户态各种任务),第2号进程kthreadd进程由第0号进程通过kernel_thread()创建,并始终运行在内核空间, 负责所有内核线程的调度和管理。 还有其他中断,内存,调度等初始化。当完成了1号2号进程的创建后,我们将0号进程真正归位idle进程,结束rest_init()。

0号进程作用是:通过kernel_thread创建了1号进程(用户态的第一个)和2号进程(管理所有内核线程)。

系统调用(用户态的open如何到内核的open的)
相当于办事大厅。通过系统调用和内核交互。 Linux 还提供了glibc 这个中介,它更熟悉系统调用的细节,可以封装成更好的接口方便使用。
这一节解析到glibc 如何调用到内核的 open :主要分成两层
第一就是gildc对系统调用进行封装:syscall.list就是把open转换为xxxxxxopen,简化了我们的函数名;然后脚本文件会生成一个文件,里面就是执行系统调用的代码(以宏的形式),通过观察可知,几乎所有的系统调用最终都会调用DO_CALL这个宏,但是这个宏在32位和64位的定义是不一样的。
do_call做以下的事情:
第二阶段: 1、会把参数放到寄存器中,系统调用号放到eax里面 2、然后调用int 80陷入内核(中断处理程序在内核初始化时候完成了) 3、保存当前用户态的寄存器,保存在pt_regs结构里面;(保护现场)4、然后调用do_int80_syscall_32函数 ,进行系统调用:(进入内核),根据eax,在系统调用表中找到对应执行,5、完成后iret指令 返回用户态,根据前面的pt_regs恢复现场。

总结一下就是gilbc先解封装调用do_call,然后把参数和调用号保存,陷入中断(int 80),保存现场,真正进入内核,找到内核的open执行,结构保存,最后iret恢复现场;64位好像没有int 80陷入中断,有syscall指令陷入中断。
解封装-do_call-int 80陷入内核-保存现场-执行内核函数-返回现场。
(系统调用号就是内核里面函数数组的下标)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值