系统调用
操作系统提供了一些方法,让用户层可以调用,而为了安全起见,这些方法调用,都是在内核空间。于是,用户调用的时候,就会有个动作,叫做陷入内核。
当用户调用系统方法的时候,系统方法每一个都有一个具体的编号,调用的时候,会约定放在具体的一个寄存器里面,然后调用 TRAP 指令,陷入内核。
内核会从寄存器中读取编号,然后获知具体的调用方法,跳到对应位置执行,执行完后返回结果。
对我们使用者来说,系统调用跟普通的调用函数没差别,我们用户层是感知不到这个部分,这块是由操作系统帮我们完成的。
线程
前面描述了进程,操作系统把程序加载起来,加载到内存,操作系统把这个称之为进程,然后默认情况下,进程会运行 main 方法,开始执行起来。
但是,因为我们的程序开发起来,大多时候都不是单一的任务,需要多个任务执行,所以如果进程里面只有一个运行流,则在开发过程中,会让多个任务混淆在一起,没办法优雅的实现。
于是,操作系统便在进程里面,再细化了一个概念,叫做线程。而把默认进程起来运行的main 这个运行流,就喊成了主线程。(实际就是进程起来后,它运行的这个主流程)
然后我们在进程里面,创建一些线程,让每个线程完成自己的任务,通过线程之间的配合,最终实现我们想要的功能。
线程之间共享进程的数据,线程更加轻量,方便。所以我们默认不会启动子进程的方式去做配合。
如果我们开发过 DOS 系统,就知道它的流程是一个大循环,然后如果说这时候我们有按键发生,我们是需要执行到获取按键事件的地方,才能真正获取按键信息。
但是如果我们是多线程的系统,当我们按键发生的时候,这时候会直接进入中断处理函数,将按键信息获取,同时还能更快的让程序响应。
下面这个图可以简单的看下,进程和线程的关系。
我们的学习技巧,一直是遵循提供引子,细节化的东西,看相关的书籍,然后有任何疑问,提问这边答疑。
知识的掌握,一定是有主观性的。当我们理解不了的时候,可以来语音解惑,帮助大家打通串联起来,能够快速的厘清线索。
在操作系统的这个体系里面,核心掌握的是什么?
进程管理
进程间通信机制
内存管理
文件系统
CPU 调度切换机制
网络
驱动开发
当我们在看到这些内容的时候,为什么需要这个?它在解决什么问题。以及依据我们之前讲的,每个模块都是由几部分组成的。
数据结构,算法。
具体到进程管理就是:如何描述一个进程,操作系统如何对进程管理的,如何切换的。
这两部分就是,设计和实现。
每个模块都可以这样子去思考。同时每个模块都可以用示例来进行初步体验,理解。进程管理,深入到操作系统里面,我们该如何学习?
如果上来就是 Linux 系统,大家一定学的吃力,又担心学不会。所以这里我们会先给大家推荐的是,小型操作系统。
比如 ucos2,比如 freertos,小型操作系统,掌握了之后,我们学习什么是启动代码,uBoot,以及复杂的嵌入式操作系统 ucLinux。
这些掌握了,进入安卓就非常容易。
中断
中断的理解很简单,比如一个老师在讲课,突然校长喊话让去下办公室,这时候老师就要中断讲课,去处理事情。处理完后回来要继续接着讲。
这里就是中断。
在程序世界里面,就是当我们程序正在运行的时候,突然有外部消息进来,系统要去处理,这时候当前的进程就需要中断,等待中断事情处理完,再继续运行。
这里面因为 CPU 内部的寄存器只有一份,于是当我们进程被打断的时候,为了等会回来从正确的位置运行,所以需要保存这些寄存器。
这个就是由操作系统实现,叫做现场保护。
而大家一直不明白的是,中断是在什么时间监测的?
这里来说一下,CPU 执行的是一条一条指令,在每条指令执行完后,CPU 都会检测是否有外部中断发生,如果有就会切换,保存线程,进入对应的中断函数。
这里把所有的中断跳转函数,系统称之为中断向量表,而每个中断信号上来,都有一个中断号,依据这个会跳转到对应的中断函数上去。
当我们知道了,中断是在每一条指令完成后会发生,那么我们就能知道,一条指令在执行过程中不会被打断。
于是,我们在开发多线程项目的时候,对于全局变量如果要多个线程操作的时候,要做一个加锁操作。
也就是线程间的同步处理。
为什么要加锁,核心逻辑就是,对于全局多个线程要去访问的数据,一条指令执行不完,在这个访问的过程中,不能中断,于是需要加锁进行保护。
只有理解到,加锁是为了全局变量在操作的时候不被中断,理解了中断发生在一条指令完成后,这时候我们就能知道,如果我们设计一条指令,这条指令就是做一个变量检测,用于加锁操作,就是没问题的。
在这里说下,大家学习进程线程开发,一般建议大家使用 ubuntu Linux 桌面,然后使用默认的 GCC 编译器,同时在学习的时候可以多了解下 MakeFile 的编写,以及编译的整个过程。
编译整个过程,这块后面我们会再讲,如果感兴趣,直接找我就好了。我们简单聊下这块该如何开展。
对于互斥信号量,管道,socket 通信,以及消息传输,大家可以参考《Unix 系统编程》,来进行掌握。
一切皆文件的逻辑
在 Linux 里面,将一切的事物都当成文件来理解,那么这个具体指的什么呢?为什么可以当成文件。
这里其实我们从一个朴素的逻辑来讲。
对于每一个外设,或者是驱动,或者是一套 SDK。我们常规的动作就几个:
1 找到它
2 使用它
所以统一的都可以归类了。
默认情况下,找到它就是 open ,默认的使用它,就是 write,如果发现使用它的业务很多,系统做了一个 ioctl 来完善补充。
所以,我们的外设,驱动,都可以通过这些方法来进行操作。而这些方法常规上我们理解是打开文件,写入数据,操作数据的。
所以,就有一切都是文件的概念了。
而这里面,我们再说个更加普遍的概念,我们拿到一堆数据,必须有对应的解析格式,否则这堆数据具体的含义我们是不知道的。
举例,电报的加密数据,如果说我们没有对应的解密字典,这个加密数据就不晓得什么意思。
还有不加密的,摩斯密码,这个也是约定好的,俗称查字典。
所以,一切皆是文件。可以说是,一切都是数据和解析组成,这两个我们对应的就是 Linux中文件的通用接口,和对应的接口实现方法(解析)。
Android 操作系统
在我们的《Android 深入系统完全讲解》第一册里面,有一张图,完整的画出来了安卓操作系统的概况。
这里怎么来学习,一个是掌握启动流程,init 进程初始化过程,以及系统服务启动过程,这
些在我们的第一册里面做了详解。
那么,我们这里补充一些知识。就是 ps 命令里面,我们查找进程之间的关系链,用的是 ps
查看。
root 1433 1 54936 4696 ffffffff b7f1e875 S /system/bin/surfaceflinger
第三个值 1 就是父类进程,是由谁创建的。
通过开机后,adb shell 命令使用这个 ps 命令,查看进程 id 和父进程 pid,通过这个可以快速的梳理出来,进程的关系网。
能够让我们对系统有了一个完整的理解。
同时再一个就是查看每个进程的 maps 信息,从这个里面可以看到每个进程的内存映射,通过这个,可以获取的就是进程中存在的一些 jar,so 库。对于我们理解调用系统库,方法的时候,有很大的帮助。
像深入下去的 Hook,还有注入编程这些,大家可以关注,这类使用场景比较特殊,但是像patch 这种功能,其实市场需求很大,于是我们可以研究下它的实现原理。
在这里给出一个引子,就是 odex 的加载器。
我们实际运行的时候,就是 odex 里面找到对应的函数,去执行。那么如果说我们自己设计一个机制,在调用之前,我们判断一下,是不是我们要篡改的函数。
如果是,那么改掉它,替换成我们的函数,是不是就等于是动态的 patch 修复了?
这块知识点,后续我们从进程的格式开始讲起,把安卓里面的这些注入技巧,串一遍。
在学习操作系统的时候,一定记得要去看计算机组成原理,这个是硬性要求。大家在不懂得地方,及时找我沟通,了解。