Linux 内核学习笔记
2015-01-10 15:25
120 查看
Linux内核学习笔记
《linux内核设计艺术》写的很好,对于理解操作系统的工作原理有很大帮助,以下是我在读这本书时所整理的一些框架,希望可以对读者理解linux操作系统的内核起到一定的作用,由于本人水平有限,不足之处,欢迎广大读者留言指正。
1.linnux启动
计算机在启动时,内存中是没有程序的,所以需要将硬盘中的操作系统加载到内存中,在内存(RAM)中什么程序也没有的时候,是由BIOS来完成加载硬盘中操作系统的任务的.
但是又是由谁来执行BIOS程序呢?
当然不可能由软件来执行!
Intel采用了硬件的解决方案,将所有80x86的CPU的硬件都设计为加电进入16位实模式运行,同时,将CPU的硬件逻辑设计为加电瞬间强行将CS的置为0xffff,IP的值置为0x0000,这样CS:IP指向了0xffff0这个位置.
BIO的第一条指令便在0xffff00地址上.
那么BIOS程序不被放在内存中,又被放在哪里呢,但是BIOS的起始地址是通过寻址找到的,那么寻址便是寻内存中的地址,这两者自相矛盾又作何解释呢?
BIOS程序被固化在计算机主板上的一块很小ROM芯片上.
随着BIOS程序的执行,BIOS会检测显卡,内存......,期间,由一项对启动操作系统至关重要的工作,便是在内存中建立中断向量表和中断服务程序.
中断向量表有256个中断向量,每个中断向量占4个字节,其中两个字节是CS的值,两个字节是IP的值,每个中断向量都指向一个具体的中断服务程序.
2.从main到怠速
怠速的意思就是操作系统已经完成了所有的准备工作,随时可以响应用户激励.
系统达到怠速状态前所做的一切准备工作的核心目的是让用户程序能够以”进程”的方式正常运行,即能够在主机上进行运算,能够与外设进行交互,能够让用户以他为媒介进行人机交互.
创建进程0:
每一个进程都是由已经存在的进程创建的,新建的进程具备创建他的进程应该具备的一切能力.因此需要创建一个最原始的进程,进程0,该进程能在主机中进行运算.
创建进程1:
进程1在进程0的基础上能以文件的形式与外设进行数据交互.
创建进程2:
进程2将构建一套人机交互界面,以支持用户与计算机的交互,最终进入怠速状态,进程2将成为创建地一个用户进程的母本.
3.用户进程与内存管理
在shell输入指令./str1,shell程序将调用fork函数创建进程.然后产生软中断,然后调用system_call函数,并通过call_system_cal_table映射到sys_fork函数.
调用find_empty_process函数,为进程申请进程号和在进程槽中为该进程申请一个空闲位置.
进程的退出
进程通过exit函数退出,最终会映射到sys_exit函数执行.
第一步:释放str1进程代码与数据所占用的物理内存并解除其与str1这个可执行文件的关系,有str1进程自己负责.
第二部:shell进程由于接受都str1发送的信号而被唤醒,于是切换到shell进程执行,释放str1进程的管理结构所占用的物理内存(页面)并接触与进程槽的关系.
多个用户进程同时运行
依次创建3个用户进程.进程号为5,6,7进程槽中的项号为4,5,6,线性地址空间的位置是4*64M~5*64M,5*64M~6*64M,6*64M~7*64M.
Str1执行过程中,str1每10ms产生一次始终中断;
在中断处理函数中调用schedule函数;
开始执行str2;
shell在创建玩新进程后会进入task_interruptible状态.shell进程调用schedule函数.
在schedule函数中,从进程槽task[64]的数组末端开始扫描当前系统中已存在的进程
之后开始第二次遍历.
4.文件系统
文件系统首先设计了一个数据结构,管理属于同一个文件所有的块,这个数据结构就是i节点。
5.IPC(进程间通讯)
管道机制
在父进程中创建一个子进程,然后在父进程中创建一个管道,父进程中不断往管道写数据,子进程不断从管道读数据.
假设读管道的进程开始运行,由于此时管道中没有数据,系统会将读管道进程挂起,切换到写管道进程中去.
管道默认规则:每次写管道执行哇比后,管道中就已经拥有了可以读出的数据,所以就会唤醒读管道进程;每次读管道操作执行完毕后,管道中就已经拥有了可以写入的空间,所以就会唤醒写管道进程.
信号机制
创建两个进程,一个发送信号sebdsig,一个接受及处理信号processig.
运行processig进程,在该进程中实现信号与处理函数的绑定.
single(SIGUSR1,sig_usr);
第一个参数是信号类型,第二个参数是信号处理函数的函数指针.
会调用系统的sys_signal函数.
sigaction[32]有32个成员,对应默认的32种信号.
然后调用pause函数,使该进程进入可中断等待状态.
执行sendsig进程
在sendsig函数中执行kill(pid,signo)函数.映射到sys_kill函数.
参数1:表示信号
参数2:接收该信号的进程的进程号.
err = send_sig(sig,*p,0)执行了发送信号的动作,他会在processig进程的信号位图signal中找到SIGUSR1这一信号对应的位置,然后将其置1,以此来发送信号.
然后返回用户空间,直至时间片用完.
调用schedual函数;
从这里可以反映出为什么要遍历两次进程槽.
当前的prodesig进程是处于可中断等待状态的,第一次遍历到prodesig的时候,他的信号位图四个哪里上就会有所体现,所以该进会被改为就绪状态.
第二次遍历时,便可以切换到prodesig进程执行.
然后同样会执行pause函数,但这次执行没有意义,有意义的是该函数会调用系统函数,当从系统函数返回时,会调用ret_from_sys_call函数.
6.编译
6.1 Linux中头文件与同名源文件的关系
系统自带的头文件用尖括号括起来,这样编译器会在系统文件目录下查找, #include <xxx.h> 。
用户自定义的文件用双引号括起来,编译器首先会在用户目录下查找,然后在到C++安装目录(比如VC中可以指定和修改库文件查找路径,Unix和 Linux中可以通过环境变量来设定)中查找,最后在系统文件中查找。 #include “xxx.h” 6.2 头文件如何来关联源文件
这个问题实际上是说,已知头文件“a.h”声明了一系列函数,“b.cpp”中实现了这些函数,那么如果我想在“c.cpp”中使用“a.h”中声明的这 些在“b.cpp”中实现的函数,通常都是在“c.cpp”中使用#include “a.h”,那么c.cpp是怎样找到b.cpp中的实现呢?
其实.cpp和.h文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。比如偶现在看到偶们公司的源代码,.cpp文件由.cc文件替代了。
在Turbo C中,采用命令行方式进行编译,命令行参数为文件的名称,默认的是.cpp和.h,但是也可以自定义为.xxx等等。
谭浩强老师的《C程序设计》一书中提到,编译器预处理时,要对#include命令进行“文件包含处理”:将file2.c的全部内容复制 到#include “file2.c”处。这也正说明了,为什么很多编译器并不care到底这个文件的后缀名是什么----因为#include预处理就是完成了一个“复制 并插入代码”的工作。
编译的时候,并不会去找b.cpp文件中的函数实现,只有在link的时候才进行这个工作。我们在b.cpp或c.cpp中用#include “a.h”实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。源文件编译后成生了目标文件(.o或.obj文件),目标 文件中,这些函数和变量就视作一个个符号。在link的时候,需要在makefile里面说明需要连接哪个.o或.obj文件(在这里是b.cpp生成 的.o或.obj文件),此时,连接器会去这个.o或.obj文件中找在b.cpp中实现的函数,再把他们build到makefile中指定的那个可以 执行文件中。
在Unix下,甚至可以不在源文件中包括头文件,只需要在makefile中指名即可(不过这样大大降低了程序可读性,是个不好的习惯哦^ ^)。在VC中,一般情况下不需要自己写makefile,只需要将需要的文件都包括在project中,VC会自动帮你把makefile写好。
通常,编译器会在每个.o或.obj文件中都去找一下所需要的符号,而不是只在某个文件中找或者说找到一个就不找了。因此,如果在几个不同文件中实现了同一个函数,或者定义了同一个全局变量,链接的时候就会提示“redefined”。
《linux内核设计艺术》写的很好,对于理解操作系统的工作原理有很大帮助,以下是我在读这本书时所整理的一些框架,希望可以对读者理解linux操作系统的内核起到一定的作用,由于本人水平有限,不足之处,欢迎广大读者留言指正。
1.linnux启动
计算机在启动时,内存中是没有程序的,所以需要将硬盘中的操作系统加载到内存中,在内存(RAM)中什么程序也没有的时候,是由BIOS来完成加载硬盘中操作系统的任务的.
但是又是由谁来执行BIOS程序呢?
当然不可能由软件来执行!
Intel采用了硬件的解决方案,将所有80x86的CPU的硬件都设计为加电进入16位实模式运行,同时,将CPU的硬件逻辑设计为加电瞬间强行将CS的置为0xffff,IP的值置为0x0000,这样CS:IP指向了0xffff0这个位置.
BIO的第一条指令便在0xffff00地址上.
那么BIOS程序不被放在内存中,又被放在哪里呢,但是BIOS的起始地址是通过寻址找到的,那么寻址便是寻内存中的地址,这两者自相矛盾又作何解释呢?
BIOS程序被固化在计算机主板上的一块很小ROM芯片上.
随着BIOS程序的执行,BIOS会检测显卡,内存......,期间,由一项对启动操作系统至关重要的工作,便是在内存中建立中断向量表和中断服务程序.
中断向量表有256个中断向量,每个中断向量占4个字节,其中两个字节是CS的值,两个字节是IP的值,每个中断向量都指向一个具体的中断服务程序.
2.从main到怠速
怠速的意思就是操作系统已经完成了所有的准备工作,随时可以响应用户激励.
系统达到怠速状态前所做的一切准备工作的核心目的是让用户程序能够以”进程”的方式正常运行,即能够在主机上进行运算,能够与外设进行交互,能够让用户以他为媒介进行人机交互.
创建进程0:
每一个进程都是由已经存在的进程创建的,新建的进程具备创建他的进程应该具备的一切能力.因此需要创建一个最原始的进程,进程0,该进程能在主机中进行运算.
创建进程1:
进程1在进程0的基础上能以文件的形式与外设进行数据交互.
创建进程2:
进程2将构建一套人机交互界面,以支持用户与计算机的交互,最终进入怠速状态,进程2将成为创建地一个用户进程的母本.
3.用户进程与内存管理
在shell输入指令./str1,shell程序将调用fork函数创建进程.然后产生软中断,然后调用system_call函数,并通过call_system_cal_table映射到sys_fork函数.
调用find_empty_process函数,为进程申请进程号和在进程槽中为该进程申请一个空闲位置.
进程的退出
进程通过exit函数退出,最终会映射到sys_exit函数执行.
第一步:释放str1进程代码与数据所占用的物理内存并解除其与str1这个可执行文件的关系,有str1进程自己负责.
第二部:shell进程由于接受都str1发送的信号而被唤醒,于是切换到shell进程执行,释放str1进程的管理结构所占用的物理内存(页面)并接触与进程槽的关系.
多个用户进程同时运行
依次创建3个用户进程.进程号为5,6,7进程槽中的项号为4,5,6,线性地址空间的位置是4*64M~5*64M,5*64M~6*64M,6*64M~7*64M.
Str1执行过程中,str1每10ms产生一次始终中断;
void do_timer(long cpl) { extern int beepcount; extern void sysbeepstop(void); if (beepcount) if (!--beepcount) sysbeepstop(); if (cpl) current->utime++; else current->stime++; if (next_timer) { next_timer->jiffies--; while (next_timer && next_timer->jiffies <= 0) { void (*fn)(void); fn = next_timer->fn; next_timer->fn = NULL; next_timer = next_timer->next; (fn)(); } } if (current_DOR & 0xf0) do_floppy_timer(); if ((--current->counter)>0) return; 削减时间片的动作 current->counter=0; if (!cpl) return; schedule(); }
在中断处理函数中调用schedule函数;
开始执行str2;
shell在创建玩新进程后会进入task_interruptible状态.shell进程调用schedule函数.
在schedule函数中,从进程槽task[64]的数组末端开始扫描当前系统中已存在的进程
之后开始第二次遍历.
4.文件系统
文件系统首先设计了一个数据结构,管理属于同一个文件所有的块,这个数据结构就是i节点。
5.IPC(进程间通讯)
管道机制
在父进程中创建一个子进程,然后在父进程中创建一个管道,父进程中不断往管道写数据,子进程不断从管道读数据.
假设读管道的进程开始运行,由于此时管道中没有数据,系统会将读管道进程挂起,切换到写管道进程中去.
管道默认规则:每次写管道执行哇比后,管道中就已经拥有了可以读出的数据,所以就会唤醒读管道进程;每次读管道操作执行完毕后,管道中就已经拥有了可以写入的空间,所以就会唤醒写管道进程.
信号机制
创建两个进程,一个发送信号sebdsig,一个接受及处理信号processig.
运行processig进程,在该进程中实现信号与处理函数的绑定.
single(SIGUSR1,sig_usr);
第一个参数是信号类型,第二个参数是信号处理函数的函数指针.
会调用系统的sys_signal函数.
int sys_signal(int signum, long handler, long restorer) { struct sigaction tmp; if (signum<1 || signum>32 || signum==SIGKILL) return -1; tmp.sa_handler = (void (*)(int)) handler; tmp.sa_mask = 0; tmp.sa_flags = SA_ONESHOT | SA_NOMASK; tmp.sa_restorer = (void (*)(void)) restorer; handler = (long) current->sigaction[signum-1].sa_handler; current->sigaction[signum-1] = tmp; return handler; } tmp.sa_handler = (void (*)(int)) handler; 执行了绑定工作. current->sigaction[signum-1] = tmp; 给sigaction这个管理结构赋值.
sigaction[32]有32个成员,对应默认的32种信号.
然后调用pause函数,使该进程进入可中断等待状态.
int sys_pause(void) { current->state = TASK_INTERRUPTIBLE; schedule(); return 0; }
执行sendsig进程
在sendsig函数中执行kill(pid,signo)函数.映射到sys_kill函数.
参数1:表示信号
参数2:接收该信号的进程的进程号.
int sys_kill(int pid,int sig) { struct task_struct **p = NR_TASKS + task; int err, retval = 0; if (!pid) while (--p > &FIRST_TASK) { if (*p && (*p)->pgrp == current->pid) if (err=send_sig(sig,*p,1)) retval = err; } else if (pid>0) while (--p > &FIRST_TASK) { if (*p && (*p)->pid == pid) if (err=send_sig(sig,*p,0)) retval = err; } else if (pid == -1) while (--p > &FIRST_TASK) if (err = send_sig(sig,*p,0)) retval = err; else while (--p > &FIRST_TASK) if (*p && (*p)->pgrp == -pid) if (err = send_sig(sig,*p,0)) retval = err; return retval; } static void tell_father(int pid) { int i; if (pid) for (i=0;i<NR_TASKS;i++) { if (!task[i]) continue; if (task[i]->pid != pid) continue; task[i]->signal |= (1<<(SIGCHLD-1)); return; } /* if we don't find any fathers, we just release ourselves */ /* This is not really OK. Must change it to make father 1 */ printk("BAD BAD - no father found\n\r"); release(current); }
err = send_sig(sig,*p,0)执行了发送信号的动作,他会在processig进程的信号位图signal中找到SIGUSR1这一信号对应的位置,然后将其置1,以此来发送信号.
然后返回用户空间,直至时间片用完.
调用schedual函数;
从这里可以反映出为什么要遍历两次进程槽.
当前的prodesig进程是处于可中断等待状态的,第一次遍历到prodesig的时候,他的信号位图四个哪里上就会有所体现,所以该进会被改为就绪状态.
第二次遍历时,便可以切换到prodesig进程执行.
然后同样会执行pause函数,但这次执行没有意义,有意义的是该函数会调用系统函数,当从系统函数返回时,会调用ret_from_sys_call函数.
ret_from_sys_call: movl _current,%eax # task[0] cannot have signals cmpl _task,%eax je 3f cmpw $0x0f,CS(%esp) # was old code segment supervisor ? jne 3f cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ? jne 3f movl signal(%eax),%ebx movl blocked(%eax),%ecx notl %ecx andl %ebx,%ecx bsfl %ecx,%ecx je 3f btrl %ecx,%ebx movl %ebx,signal(%eax) incl %ecx pushl %ecx call _do_signal popl %eax 3: popl %eax popl %ebx popl %ecx popl %edx pop %fs pop %es pop %ds iret 会调用do_signal函数. void do_signal(long signr,long eax, long ebx, long ecx, long edx, long fs, long es, long ds, long eip, long cs, long eflags, unsigned long * esp, long ss) { unsigned long sa_handler; long old_eip=eip; struct sigaction * sa = current->sigaction + signr - 1; int longs; unsigned long * tmp_esp; sa_handler = (unsigned long) sa->sa_handler; if (sa_handler==1) return; if (!sa_handler) { if (signr==SIGCHLD) return; else do_exit(1<<(signr-1)); } if (sa->sa_flags & SA_ONESHOT) sa->sa_handler = NULL; *(&eip) = sa_handler; longs = (sa->sa_flags & SA_NOMASK)?7:8; *(&esp) -= longs; verify_area(esp,longs*4); tmp_esp=esp; put_fs_long((long) sa->sa_restorer,tmp_esp++); put_fs_long(signr,tmp_esp++); if (!(sa->sa_flags & SA_NOMASK)) put_fs_long(current->blocked,tmp_esp++); put_fs_long(eax,tmp_esp++); put_fs_long(ecx,tmp_esp++); put_fs_long(edx,tmp_esp++); put_fs_long(eflags,tmp_esp++); put_fs_long(old_eip,tmp_esp++); current->blocked |= sa->sa_mask; }
6.编译
6.1 Linux中头文件与同名源文件的关系
系统自带的头文件用尖括号括起来,这样编译器会在系统文件目录下查找, #include <xxx.h> 。
用户自定义的文件用双引号括起来,编译器首先会在用户目录下查找,然后在到C++安装目录(比如VC中可以指定和修改库文件查找路径,Unix和 Linux中可以通过环境变量来设定)中查找,最后在系统文件中查找。 #include “xxx.h” 6.2 头文件如何来关联源文件
这个问题实际上是说,已知头文件“a.h”声明了一系列函数,“b.cpp”中实现了这些函数,那么如果我想在“c.cpp”中使用“a.h”中声明的这 些在“b.cpp”中实现的函数,通常都是在“c.cpp”中使用#include “a.h”,那么c.cpp是怎样找到b.cpp中的实现呢?
其实.cpp和.h文件名称没有任何直接关系,很多编译器都可以接受其他扩展名。比如偶现在看到偶们公司的源代码,.cpp文件由.cc文件替代了。
在Turbo C中,采用命令行方式进行编译,命令行参数为文件的名称,默认的是.cpp和.h,但是也可以自定义为.xxx等等。
谭浩强老师的《C程序设计》一书中提到,编译器预处理时,要对#include命令进行“文件包含处理”:将file2.c的全部内容复制 到#include “file2.c”处。这也正说明了,为什么很多编译器并不care到底这个文件的后缀名是什么----因为#include预处理就是完成了一个“复制 并插入代码”的工作。
编译的时候,并不会去找b.cpp文件中的函数实现,只有在link的时候才进行这个工作。我们在b.cpp或c.cpp中用#include “a.h”实际上是引入相关声明,使得编译可以通过,程序并不关心实现是在哪里,是怎么实现的。源文件编译后成生了目标文件(.o或.obj文件),目标 文件中,这些函数和变量就视作一个个符号。在link的时候,需要在makefile里面说明需要连接哪个.o或.obj文件(在这里是b.cpp生成 的.o或.obj文件),此时,连接器会去这个.o或.obj文件中找在b.cpp中实现的函数,再把他们build到makefile中指定的那个可以 执行文件中。
在Unix下,甚至可以不在源文件中包括头文件,只需要在makefile中指名即可(不过这样大大降低了程序可读性,是个不好的习惯哦^ ^)。在VC中,一般情况下不需要自己写makefile,只需要将需要的文件都包括在project中,VC会自动帮你把makefile写好。
通常,编译器会在每个.o或.obj文件中都去找一下所需要的符号,而不是只在某个文件中找或者说找到一个就不找了。因此,如果在几个不同文件中实现了同一个函数,或者定义了同一个全局变量,链接的时候就会提示“redefined”。
相关文章推荐
- 学习笔记之Linux内核同步方法
- Linux 内核入门学习笔记(一) AT&T汇编基础
- Linux学习笔记--内核编译
- linux内核与驱动学习笔记(一)---内核的划分
- 【学习笔记】编译Linux内核(上)--编译基于x86平台的Linux内核的过程
- Linux学习笔记:内核代码编码风格
- linux内核学习笔记:中断与异常
- Linux内核学习笔记三——进程调度
- Linux学习内核移植相关笔记第2部分
- linux内核学习初笔记(3)u-boot执行第二阶段typedef int (init_fnc_t) (void)解释
- linux 0.12 源码学习笔记(一)内核引导
- Linux内核学习笔记——预备知识
- linux内核学习笔记1——进程的表示
- linux学习笔记-读《Linux设备驱动开发详解》~第三章 Linux内核及内核编程
- Linux学习笔记 - 内核模块
- LINUX学习笔记19——内核简介
- Linux内核学习笔记之进程管理2—进程的组织形式
- Linux学习内核移植相关笔记第4部分
- 深入LInux内核结构学习笔记---进程表示
- Linux下USB内核之学习笔记(一)