• 欢迎访问少将全栈,学会感恩,乐于付出,珍惜缘份,成就彼此、推荐使用最新版火狐浏览器和Chrome浏览器访问本网站。
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏少将全栈吧
  • 欢迎加博主微信:jiang_shaobo

Linux内核技术 ( 启动过程,dmesg分析等)

点滴 admin 6年前 (2014-05-01) 354次浏览 已收录 扫描二维码

linux
的启动过程便十分重要,先用dmesg命令察看一下linux启动时打出的消息,内核的启动最后是到 start_kernel ( in
/init/main.c )也就是说启动的过程是从 head.S ( arch/i386/boot/ ) 一直运行到
main.c(start_kernel)
.它的作用是完成开机后的设置与内核的初始化,然后,系统究竟入一个无限的循环中等待用户的输入,调用fork来产生子进程.从而达到交互式操作系统的设
计要求.

第一部分 : 内核初始化以及启动.

.
启动系统.当PC机加电开始启动时,80X86的处理器(CPU)在实模式下自检,开始执行物理地址0xFFFF0即ROM-BIOS的起始地址处的代
码。PC机的BIOS进行系统自检,初始化中断向量表到物理地址0x0。然后把引导设备的第一个扇区加载到地址0x7C00,执行此处的指令。到这里与
linux无关,x86系列的硬件设置如此.

linux的内核本身是不能自举的,所以lilo和loadlin的作用就是加载系统内核.有关lilo的原理可以参考lilo的readme.从加电到内核加载的过程是:加电-执行BIOS-加载第一扇区-lilo-加载内核

Linux
内核的最初部分代码是用汇编语言写的(文件是boot/bootsect.s)。(我的汇编水平有限,暂且不看),它首先把自身这部分代码移到绝对地址
0x90000,把下面的2K代码从引导设备加载到地址0x90200上,内核的其余部分加载到地址0x10000处。在加载系统时显示
“loading…”.
然后,程序控制权交给另一个实模式汇编程序(boot/Setup.S)。接下来,此程序把整个系统从地址0x10000移到地址0x1000,进入保护
模式。程序控制转给系统的其余部分即地址0x1000。


一个步骤是系统内核的解压过程,这部分代码在地址0x1000(文件/Boot/head.S),该段程序初始化寄存器,然后执行
decompress_kernel(),这个函数源于zBoot/inflate.c、zBoot/unzip.c和zBoot/misc.c三个文件

Loading ….[ bootsect.S ]

uncompress …..[ decompress_kernel() ]

main.c — start_kernel() 开始.

开始 printk(banner);

Linux version 2.2.6 (root@lance) (gcc version 2.7.2.3) (检查一下GCC 的版本号, 在/init/main.c 中如果gcc 的版本号不够,时不允许编译内核的)

#40 Sun Apr 18 17:44:20 CST 1999

调用init_time()打印出以下内容:

Detected 199908264 Hz processor.

然后运行 console_init() — drivers/char/tty_io.c */

Console: colour VGA+ 80×25

运行一个循环,测量一下 MIPS ? 据说是要用一个确定的机器指令周期来实现实时的延迟.

Calibrating delay loop… 199.48 BogoMIPS

初始化内存/* init_mem */

Memory: 63396k/65536k available (848k kernel code, 408k reserved, 856k data

, 28k

/** dquote_init() **/

VFS: Diskquotas version dquot_6.4.0 initialized

察看cpu 的类型(在2.2.14 以后听说增加了对多种cpu 的支持, 以后我可得用看看,if I can find a bug of intel then ……)

CPU: Intel Pentium Pro stepping 09

初始或处理器与协处理器,对于比较老的处理器, linux 会用软件模拟协处理器?

Checking 386/387 coupling… OK, FPU using exception 16 error reporting.

检查治理的合法性

Checking ’hlt’ instruction… OK.

POSIX conformance testing by UNIFIX

此后调用 linux_thread(init ,..,..,)(arch/i386/kernel/process.c)

创建一个运行 init 的进程.

进入了第二阶段用户模式 ( user_mode )End of start_kerne最后进入cpu_idle ( arch/i386/kernel/process.c )

第二部分 设备的初始化

对设备的初始化调用. init()—do_basic_init()–+

pci_init() 对pci 设备的初始化( 在main.c文件中有这样一段 ifdef PCI …..需要看一下)下面打印出结果:

PCI: PCI BIOS revision 2.10 entry at 0xfd8d1

PCI: Using configuration type 1

PCI: Probing PCI hardware

对Socket的初始化,socket_init() (这里也许就是linux 的网络秘密所在吧,以后我的注意) –Linux NET4.0 for Linux 2.2

Based upon Swansea University Computer Society NET3.039

NET4: Unix domain sockets 1.0 for Linux NET4.0.

NET4: Linux TCP/IP 1.0 for NET4.0

IP Protocols: ICMP, UDP, TCP

Starting kswapd v 1.5 kswapd_setup()

调用 device_setup()

Detected PS/2 Mouse Port.

初始化 声卡

Sound initialization started

Sound initialization complete

初始化 软驱

Floppy drive(s): fd0 is 1.44M

FDC 0 is a National Semiconductor PC87306

SCSI 设备的初始化

(scsi0)found at PCI 13/0

(scsi0) Wide Channel, SCSI ID=7, 16/255 SCBs

(scsi0) Downloading sequencer code… 419 instructions downloaded

scsi0 : Adaptec AHA274x/284x/294x (EISA/VLB/PCI-Fast SCSI) 5.1.10/3.2.4

scsi : 1 host.

Vendor: SEAGATE Model: ST32155W Rev: 0596

ype: Direct-Access ANSI SCSI revision: 02

Detected scsi disk sda at scsi0, channel 0, id 0, lun 0

Vendor: SEAGATE Model: ST32155W Rev: 0596

Type: Direct-Access ANSI SCSI revision: 02

Detected scsi disk sdb at scsi0, channel 0, id 1, lun 0

scsi : detected 2 SCSI disks total.

(scsi0:0:0:0) Synchronous at 40.0 Mbyte/sec, offset 8.

SCSI
device sda: hdwr sector= 512 bytes. Sectors= 4197405 [2049 MB] [2.0 GB
](scsi0:0:1:0) Synchronous at 40.0 Mbyte/sec, offset 8.

SCSI device sdb: hdwr sector= 512 bytes. Sectors= 4197405 [2049 MB] [2.0 GB]Partition check: sda: sda1 |

sdb: sdb1 sdb2|

安装 文件系统 filesystem_setup()

安装设备驱动程序 mount_root()

VFS: Mounted root (ext2 filesystem) readonly.

Freeing unused kernel memory: 28k freed

Adding Swap: 66540k swap-space (priority -1)

Soundblaster audio driver Copyright (C) by Hannu Savolainen 1993-1996

SB 3.01 detected OK (220)

at 0x220 irq 5 dma 1

YM3812 and OPL-3 driver Copyright (C) by Hannu Savolainen, Rob Hooft 1993-1

996 at 0x388

NET4: AppleTalk 0.18 for Linux NET4.0

eth0: Intel EtherExpress Pro 10/100 at 0xf800, 00:A0:C9:49:2F:FF, IRQ 9.

Board assembly 645520-034, Physical connectors present: RJ45

Primary interface chip DP83840 PHY #1.

DP83840 specific setup, setting register 23 to 8462.

General self-test: passed.

Serial sub-system self-test: passed.

Internal registers self-test: passed.

ROM checksum self-test: passed (0x49caa8d6).

Receiver lock-up workaround activated.

NET4: AppleTalk 0.18 for Linux NET4.0

结束 do_basic_setup()

open(“/dev/console”, O_RDWR, 0)

开始执行 /sbin/init ( execv(…) )

内核就此启动完毕…

这样一来系统在启动时需要的文件一目了然了.要读启动这方面的源码,大概也就可以这样行动即:

Boot/head.S

decompress_kernel(),然后按照个功能块来读取相应的函数.

其实对于整个kernel 来说,还应该解读一下makefile ,说实话在编程来讲我是很爱写 makefile ,然而解读内核,她就是工作十分难以进行.好在有了souce insight 可以让我们轻松的找到,函数和常量的定义, 方便不少.


开main.c (其实我是将2.2.14解到了windows 下).发现有n 个函数与2n 个常量的定义,只有几个 init
函数在.对于其他的外部函数也定义了不少.这里却没有主函数?我现在怀疑linux 程序是否和vc
一样不知道从哪里开始进行:__).不过马上明白了, 整个内核事变在一块的. 还包括.存储管理, 处理机管理, 内存管理等许多模块,
她该不会启动以后就没有时刻干了吧

好了, 一口吃不成胖子.我还是先从启动读起.不过我开始对是否要加中文祝时产生怀疑, 写那些代码的本身都是高手, 他们应知道怎样是最好的注释. 难道读代码的人连这点e 文也看不了?


清楚了 linux的启动过程,对于整个系统分析来说.连一个开头都不算.所做的只是知道了系统启动是一些函数的条用顺序.恰如某位朋友来信建议的那样,
如果这样照顺序硬读,实是自残.好在sunmoon
有一定的自知之明.如此这样下去,等不懂得东西多了.那我就不能坚持了,所以我的原则是?:准备不充分决不轻易动手.

身边有意高手建议,应从应用程序入手,先写一些和系统相关的有水平的程序, 在对程序尽跟踪.了解整个系统的体系结构和调用体系.再去分析源吗.

着手分析linux 的体系结构:

linux
的内核采用的并不是现在流行(理论上应该更先进的)为内核结构.而采用的是由很多过程组成的一个整合体. 每一个过程可以相互调用.
看起来停乱.这与它的开发历程有关.但我个人感觉, linux 整体如同一个结构化的程序.(决非opp).是由不同的模块完成不同的功能,
然后经过整体的调度,整合而变成一整体.

在中分出了5个子系统: 进程调度, 内存管理,虚拟文件系统,进程通信,与网络接口.

具体的每个子系统的功能, 接口, 子系统结构, 数据结构的描述, 可以从这本书上详细的看一看.力争对总体有一个把握.

分清楚在linux 的近50 兆源吗中. 那一部分,实现什么功能, 以便以后阅读是不至于混乱.

再下一步, 应该分析Makefile

在linux 中每一个进程都由task_struct 数据结构来定义. task_struct就是我们通常所说的PCB.

她是对进程控制的唯一手段也是最有效的手段.

当我们调用fork() 时, 系统会为我们产生一个task_struct结构.然后从父进程,那里继承一些数据,

并把新的进程插入到进程树中, 以待进行进程管理.因此了解task_struct的结构对于我们理解任务

调度(在linux 中任务和进程是同一概念)的关键.在进行剖析task_struct的定义之前. 我们先按照

我们的理论推一下它的结构.

1, 进程状态 ,将纪录进程在等待,运行,或死锁

2, 调度信息, 由哪个调度函数调度,怎样调度等

3, 进程的通讯状况

4,因为要插入进程树,必须有联系父子兄弟的指针, 当然是task_struct型

5,时间信息, 比如计算好执行的时间, 以便cpu 分配

6,标号 ,决定改进程归属

7,可以读写打开的一些文件信息

8, 进程上下文和内核上下文

9,处理器上下文

10,内存信息

因为每一个PCB都是这样的, 只有这些结构, 才能满足一个进程的所有要求.

打开/include/linux/sched.h 找到task_struct 的定义

struct task_struct {

/* these are hardcoded – don’t touch */

这里是一些硬件设置对程序原来说是透明的. 其中state 说明了该进程是否可以执行,

还是可中断等信息. Flage 是进程号, 在调用 fork() 时给出,addr_limit 是区分内核进程

与普通进程在内存存放的位置不同

volatile long state; /* -1 unrunnable, 0 runnable, 0 stopped */

unsigned long flags; /* per process flags, defined below */

int sigpending;

mm_segment_t addr_limit; /* thread address space:

0-0xBFFFFFFF for user-thead

0-0xFFFFFFFF for kernel-thread

*/

struct exec_domain *exec_domain;

long need_resched;

/* various fields */

count 是 计数器 priorrity 是优先级

long counter;

long priority;

cycles_t avg_slice;

/* SMP and runqueue state */

为多处理机定义的变量.

int has_cpu;

int processor;

int last_processor;

int lock_depth;

/* Lock depth. We can context switch in and out of holding a syscall kernel lock… */

为了在进程树中排序, 定义的父子,兄弟指针

struct task_struct *next_task, *prev_task;

struct tas74k_struct *next_run, *prev_run;

/* task state */

定义可 task 运行的状态, 以及信号

struct linux_binfmt *binfmt;

int exit_code, exit_signal;

int pdeath_signal; /* The signal sent when the parent dies */

/* 定义可进程的用户号,用户组以及进程组*/

unsigned long personality;

int dumpable:1;

int did_exec:1;

pid_t pid;

pid_t pgrp;

pid_t tty_old_pgrp;

pid_t session;

/* boolean value for session group leader */

是不是进程组的头文件

int leader;

/*

* pointers to (original) parent process, youngest child, younger sibling,

* older sibling, respectively. (p-father can be replaced with

* p-p_pptr-pid)

*/

父子进程的一些指针

struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;

/* PID hash table linkage. */

在调度中用的一些hash 表

struct task_struct *pidhash_next;

struct task_struct **pidhash_pprev;

/* Pointer to task[] array linkage. */

struct task_struct **tarray_ptr;

struct wait_queue *wait_chldexit; /* for wait4() 等待队列 */

struct semaphore *vfork_sem; /* for vfork() */

unsigned long policy, rt_priority;

unsigned long it_real_value, it_prof_value, it_virt_value;

进程的性质因为实时进程与普通进程的调度算法不一样所以应有变量区分

下面是进程的一些时间信息

unsigned long it_real_incr, it_prof_incr, it_virt_incr;

struct timer_list real_timer;

struct tms times;

unsigned long start_time;

long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS];定义了时间片的大小

/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */

内存信息

unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;

int swappable:1;

/* process credentials */

uid_t uid,euid,suid,fsuid;

gid_t gid,egid,sgid,fsgid;

int ngroups;

gid_t groups[NGROUPS];

kernel_cap_t cap_effective, cap_inheritable, cap_permitted;

struct user_struct *user;

以下英文注释很清楚

/* limits */

struct rlimit rlim[RLIM_NLIMITS];

unsigned short used_math;

char comm[16];

/* file system info */

int link_count;

struct tty_struct *tty; /* NULL if no tty */

/* ipc stuff */

struct sem_undo *semundo;

struct sem_queue *semsleeping;

/* tss for this task */

struct thread_struct tss;

/* filesystem information */

struct fs_struct *fs;

/* open file information */

struct files_struct *files;

/* memory management info */

struct mm_struct *mm;

/* signal handlers */

spinlock_t sigmask_lock; /* Protects signal and blocked */

struct signal_struct *sig;

sigset_t signal, blocked;

struct signal_queue *sigqueue, **sigqueue_tail;

unsigned long sas_ss_sp;

size_t sas_ss_size;

};

在分析完 这个结构之后, 还有很多问题要想, 也许不能读 但框架要搞好.需要向的问题有以下几个

1,在task_struct 中用的常量在那里定义呢, 如最大进程个数, 最多支持的cpu 个数,等等

2,在调用fork() 时, 系统是分配一块内存 会是这样么

malloc(1,sizeof(struct task_struck))

拷贝一些变量,还是和服进程公用一部分内存.malloc 函数怎么实现(在内存管理那一部分,但此处我认为不能不想)

3,.对于线程来说, 又如何实现呢?

4, 调度策略函数 schedul()

有几种形势, 时间片轮转, 抢占式,优先级抢占式, 多级反馈制.除了时间片轮转外都要对进程树进行遍历,(对于实时进程

的fifo机制不用)linux 是怎样保证了高效呢?如果把最大线成数修改, 效率会不会降低

5, 进程通讯用到的管道,信号结构如何

整个linux内核之所以这样艰涩,难懂就在于它的整体性.想我们这样很少写万行以上程序的人来说,它就好像一个庞大的肉球.让你不知道如何下嘴.不过一但咬破.就非常香美了.毕竟,读这么优秀的代码也是一种享受.


个人认为linux 的内核难在这几点:1,系统庞大,太多的变量,结构,
以及typedef定义的东西不容易找到.2,作为操作系统,它的函数调用时动态的,读不懂大量的makefile 你根本就不知道这50
M的东西是怎么组织起来的,况且,你绝对不可以像跟踪程序一样用debug走一下.3, 庞大的数据结构,可能是比较简单的运算变得不容易读.


此在读核的初级阶段.我们应该善于想象,善于将不容易理解的部分用伪码的理解方式走过,当我们对大局把握好了,将整个结构拆解清楚了,在读不迟.况且,虽
内核本身来说,它所涉及到的运算,结构. 比如说进程调度这一部分,说白了,就是在调用fork()的时候,就产生一task_strut
类型的指针,它包含进程调度所用到的一切信息.然后将这个指针插到队列中去就行了,然后cpu 一次总队类中取出指针,分配给他们时间片.


这个指针如何插入呢?说白了就是看它的weight,weight
的计算方法,有根据进程类型的不同由不同的算法(实时进程,内核进程,普通进程).好了,这样我们想一下关于队列的操作,插入,删除,插到队头,
置于队尾.再想一下,这些操作如何同操作系统的应用结合在一块.例如;好队进程正在运行,突然,由于一硬件中断.产生一进程,它必须马上处理.系统应把它
插入到队头.

好了.你可以读一下/usr./src/linux/kenrel/sched.c,不要过那么多全局变量,现在数据结构上走过去,如下面的代码:

static inline void move_last_runqueue(struct task_struct * p)

{

struct task_struct *next = p-next_run;

struct task_struct *prev = p-prev_run;

/* remove from list */

next-prev_run = prev;

prev-next_run = next;

/* add back to list */

p-next_run = init_task;

prev = init_task.prev_run;

init_task.prev_run = p;

p-prev_run = prev;

prev-next_run = p;

}

static inline void move_first_runqueue(struct task_struct * p)

{

struct task_struct *next = p-next_run;

struct task_struct *prev = p-prev_run;

/* remove from list */

next-prev_run = prev;

prev-next_run = next;

/* add back to list */

p-prev_run = init_task;

next = init_task.next_run;

init_task.next_run = p;

p-next_run = next;

next-prev_run = p;

}


果你还不懂,你可能要先,在c语言和数据结构上下一点功夫.其他的模块,我想也是大同小异,
不过,也修补会这么简.如内存管理中用到了好多平衡二叉树的排序,遍历等等.但总的结构时不变的.只要可以通栏全局,在不开定义的情况下,可以读懂全局
变量的意思(其实,猜个八九不成问题),看懂是不成问题的.起码我是信十足.

目前,linux 被移植到了各种机器上.如apple 等.但我个人认为linux的真正魅力.还是在i386机器上.因为llinus前辈在写linux 时的初衷,就是在386 平台上实现类unix的os

linux使用了intel 80386系列处理器的”保护模式”.操作系统的资源的管理和分配.由80386硬件存储管理和保护机制实现

虚拟存储器,是一种扩种内存的设计方案.他来源于当初主存非常昂贵的年代.用到了程序的局部性原则,即程序在运行时没有必要全部装入内存.支部当前要运行的那一部分调入内存即可

实际上,整个存储系统是 由 高速缓存— 内存— 硬盘 等多级存储介质构成的,但这对程序原始透明的,比如我们在程序中执行这样一天指令

mov bx ,1997

mov ax ,[bx]

这样地址是1997 的内容背拷贝到了ax ,这样由程序产生的地址时虚地址.这个地址与实际的物理地址是不同的.要有这个地址转换到实际的物理地址,就需要有一个转换机制.通常叫做MMU的硬件单元完成这个任务


谓的保护机制就是在这个基础上进行的.它的目的是要使不同的程序段互不干涉.系统进程与用户进程严格分开已达到系统安全与多用户多进程的要求.在
linux 中通过给不同的任务分配不同的虚拟地址到物理地址的映射.来实现不同任务的切换与保护.同时.linux
“可能”是把系统进程与用户进程分开(我不太确定,可能是把系统进程的地址控制在100000以内?,当然在iipv通行中的共享内存,由于操作不当可能
产生不可预料的后果).

另外,linux对统一任务也进行了不同程度的保护.它使用优先级来决定的.比如内核的优先级是0,系统调用 :1 库:2 用户进程3.在程序对某一个数据段进行读写的时候,.应县检查优先级,.在决定它运行的优先级或存取权限.

我想,这个优先级一定会和struct_task 的某些表示调度优先级的参数关联.

对于内存管理,通常是有段式,页式和段页式三种方式.在这里讨论的使者两种方式的虚拟-物理转换机制的不同.因为linux实行的是段页式内存管理.因此这两种映射机制,也就必须都存在

段式管理,使用了一系列的可改变大小的地址集合进行管理.它的好处是,可以充分利用物理内存.缺点是难以管理,

通常在c 中 我们可以这样定义一个段(此定义只是解释段的概念,linux中绝不是如此)

typedef stucrt duan

{

struct duan * next ,*pre ;/*所所需指针*/

int tag;

iint begin ,end /*始末点*/

int size;

data data ; /*内容*/

……..

}


上实在是实际内存中可能用到的段的数据结构.而我们在保护模式中,所谓的段是保护管理.大体上和汇编语言中基址寻址有些相似:他是实现虚拟-物理地址转换
的基础(说白了,我个人认为,把所有的段定义成一样大就是页,不过实际上我还没看linux 是怎样做的,所以千万不要被我误导)

段 有一个基址 (base address)规定了在线性物理内存中的开始地址

有一个限制位,(limit) 表示段内最大偏移量,(也就是大小)

段的属性 (attribute) 表示该段是否会被读写

这3 个属性,包含在段的描述符中

所谓的描述符.是一个8个子节的存储单元,其结构大概如下:

字节0 —–0?7 位描述苻

字节1 ——8?15位描述符

字节2 —–0?7 位基址

字节3 —–8?15 位基址

字节4 —–16?24 段基址

字节5 —–存储权限的字节

字节6 —–G| D|0|0| 16-19位段界限

字节7 —–31-?24 段基址

其中第五个字节,是存取权字节,它包含有好几个标志位,用来标志该段是在内存中,还是没有.后者该段是用户段或者是系统段之类的.被人