Unix强大的根本原因
Linux系统的基础:内核、C库、工具集和系统的基本工具
操作系统是指整个系统中负责完成最基本功能和系统管理的那些部分。
其中系统管理的那些部分应该包括:
配置选项可以用来决定哪些文件编译进内核,也可以通过预处理命令处理代码。这些选项要么是二选一,要么是三选一。其中三选一是加多了module选项,编译的时候这部分功能被编译成以模块的形式生成,驱动程序一般都用三选一的配置选项。
配置选项可以是字符串或者整数。
配置工具
命令行工具:
make config
图形界面工具:
make menuconfig #ncurse库
make gconfig #gtk+
.config被放置在内核源码树中根目录下。修改了配置文件之后,或者更新新源码树的时候,应该验证与更新配置:
make oldconfig
减少编译的垃圾信息
make > ../detritus 或
make > /dev/null #永无放回值的黑洞
原因:(速度与大小)保证内核高效和简练。
头文件
基本头文件:内核源代码顶级目录下的include中
体系结构相关头文件:内核源代码树的arch/architecture/include/asm目录下
printk()函数:把格式化好的字符串拷贝到内核日志缓冲区上,syslog程序可以通过读取该缓冲区来获取内核信息。
printk()和printf()的最主要的区别:printk()允许通过设置优先级让syslog决定是否显示这条系统消息。
(1)内联函数
static inline void wolf(unsigned long tail_size);
- static:关键字
- inline:用于限定关键字
内联函数:编译时在它被调用的地方展开。
优点:减少了函数调用的开销,性能较好。
缺点:频繁的使用内联函数也会使代码变长,从而在运行时占用更多的内存。
定义内联函数特点:时间要求高,本身长度较短的函数,一般在头文件中定义。
在内核中,为了类型安全和易读性,优先使用内联函数而不是复杂的宏。
(2)内联汇编
Linux的内核混合使用了C语言和汇编语言。汇编语言用于偏近底层或对执行时间严格要求的地方。
(3)分支声明
对于条件选择语句,在一个条件经常/很少出现时,编译器可通过gcc内建的一条指令对条件分支选择进行优化。比如likely()(条件为真的频率高)和unlikely()(条件为真的频率很低)。内核把这条指令封装成了宏。
内核栈的大小是编译内核时决定的,对于不用的体系结构,内核栈的大小不一样,但都是固定的。每个处理器都有自己的栈。(不像用户空间的栈可以动态增长)
原因:
常用解决方法:自旋锁和信号量
大部分C语言代码与体系结构无关。必须把与体系结构相关的代码从内核代码树的特定目录中适当地分离出来。
进程是出于执行期的程序。线程是进程中活动的对象,每个线程都拥有一个独立的程序计数器、进程栈、和一组进程寄存器。(内核调度的对象是线程,不是进程)
进程提供两种虚拟机制:虚拟处理器和虚拟内存。同一进程中的线程之间可以共享虚拟内存。
进程描述符(一个结构体)中包含的数据能完整地描述一个正在执行的程序:它打开的文件、进程的地址空间、挂起的信号、进程的状态等等。
1、分配进程描述符
Linux通过slab分配器分配task_struct结构,这样能达到对象的复用和缓存着色的目的(通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗)。
2.6以前的内核:各个进程的task_struct存放在他们内核栈的尾端。
2.6及2.6以后:用slab分配器动态生成task_struct,只需要在内核栈的尾端创建一个struct thread_info。
thread_info结构中有个指向该任务实际task_struct的指针。
2、进程描述符的存放
3、进程状态
4、设置当前进程的状态
内核调整某个进程的状态
set_task_state(task,state); /*将任务task的状态设置为state*/
5、进程上下文
用户空间的进程陷入内核空间执行,称内核“代表进程执行”并处于进程上下文中。
系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行—对内核的所有访问都必须通过这些接口。
6、进程家族树
进程的创建分为两个步骤:fork()和exec().
fork()/vfork()/_clone() -> clone() -> do_fork() -> copy_process()
copy_process()实现流程
写时拷贝是一种可以推迟甚至免于拷贝数据的技术。Linux的fork()使用的就是写时拷贝页实现,此时内核并不复制地址空间,以只读的方式共享,资源的复制只有在需要写入(父进程或子进程需要写入)的时候才进行。
内核有意选择子进程首先执行(但是并非总能如此),因为一般子进程都会马上调用exec()函数,这样可以避免写时拷贝的额外开销,如果父进程首先执行的话,有可能开始向地址空间写入。
除了不拷贝父进程页表项外,vfork()系统调用和fork()的功能相同。
在进程创建的过程中,子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,直到子进程通过vfork_done指针像它发送信号。
创建线程和进程的步骤一样,只是最终传给clone()函数的参数不同。
比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0)
创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES |CLONE_SIGHAND, 0)
内核线程和普通线程的区别在于内核线程没有独立的地址空间,它们只运行在内核空间,同样可以被调度,被抢占。
进程终结需要调用do_exit(),具体流程如下:
在调用了do_exit()后,尽管线程已经僵死不能再运行了,但是系统还保留了它的进程描述符。
父进程收尸—wait()
父进程受到子进程发送的exit_notify()信号后,将该子进程的进程描述符和所有进程独享的资源全部删除。
当父进程先于子进程结束,那么必须保证子进程能够找到新的父亲。
子进程在调用exit_notify()时已经考虑到了这点。
如果子进程的父进程已经退出了,那么子进程在退出时,exit_notify()函数会先调用forget_original_parent(),然后再调用find_new_reaper()来寻找新的父进程。
find_new_reaper()函数先在当前线程组中找一个线程作为父亲,如果找不到,就让init做父进程。(init进程是在linux启动时就一直存在的)