linux学习笔记

操作体系结构与功能流程

一、操作系统的结构

自顶向下的流程结构
用户应用程序
操作系统的服务层(sys_call)
操作系统内核
硬件系统(驱动)
硬件

二、操作系统的工作方式

1.把操作系统从用户态 切换到 内核态(用户应用程序 到 内核的流程)

2.实现操作系统的系统调用sys_call(操作系统服务层)

3.应用操作系统提供的底层函数,进行功能实现

​ 3.1 操作系统的驱动结构

4.退出后从内核态切换到用户态

三、操作系统内核中各级模块的相互关联

Linux内核的整体模块:进程调度模块、内存管理模块、文件系统模块、进程间通信模块、驱动管理模块


每个模块间的关系:

linux学习笔记_第1张图片

四、操作系统结构的独立性

管理层——实现层

易于升级和维护 1991-2022年,低版本的内核与高版本内核,它们之间的区别——内核驱动的种类大大增多,内核驱动的管理模式没有巨大的改变(也有3个阶段的跳跃:零散型 分层型 设备树);进程的调度算法发生了改变,进程的管理方式没有巨大的改变。

内核中断体系结构

学习中断的目的:

  1. 硬件的中断响应----->内核驱动中的中断
  2. 系统调用的函数响应(sys_call)----->系统调用
  3. 自定义中断----->软件的软中断模式
  4. 信号中断(kill -signalnum)----->对了解信号的使用 创建 等
  5. 系统的异常和错误----->系统的异常获取,了解系统异常的作用

一、Linux的中断机制

1.分类:硬中断 软中断

硬中断:由电脑主机的8259A类似的硬件中断控制芯片发出的中断;ARM中断控制器发出的中断

软中断:又可称为异常 或 错误。CPU自行保留的中断、系统调用异常。

2.代码结构

中断前的处理过程,中断回复过程 中断的执行过程
硬件中断的处理过程 asm.s trap.c
软件及系统调用的处理过程 system_call.s fork.c signal.c exit.c sys.c

二、中断的工作流程

1.简述中断工作流程

做CPU工作模式的转化

进行寄存器的拷贝和压栈

设置中断异常向量表

保存正常运行的函数返回值

跳转到对应的中断服务函数上运行

进行模式和寄存器的复原

跳转回正常工作函数地址继续运行

2.Linux中中断的工作流程

  1. 将所有的寄存器值入栈 (8086中)SS EFLAGS ESP CS EIP (错误码) ARM中的(r0-r15)

  2. 将异常码入栈(中断号)

  3. 将当前的函数返回值进行入栈(为了在中断执行后能够找到在哪中断的,能够复原)

  4. 调用对应的中断服务函数

  5. 出栈函数返回值

  6. 出栈复原寄存器值

三、中断代码的实现过程

首先将对应异常的C语言函数入栈:

_divide_error:
	pushl $_do_divide_error	//把一个c语言函数入栈

如以下这些C语言函数:

_debug://这是一个中断
	pushl $_do_int3		# _do_debug
	jmp no_error_code//跳转

_nmi:
	pushl $_do_nmi
	jmp no_error_code

_int3:
	pushl $_do_int3
	jmp no_error_code

_overflow:
	pushl $_do_overflow
	jmp no_error_code

_bounds:
	pushl $_do_bounds
	jmp no_error_code

然后把中断处理服务函数的入口地址存给EAX

xchgl %eax,(%esp)

再压入所有寄存器

	pushl %ebx
	pushl %ecx
	pushl %edx
	pushl %edi
	pushl %esi
	pushl %ebp
	push %ds
	push %es
	push %fs

有错误码压错误码,没错误码压0

	pushl %eax			# error code
	pushl $0			# no error code

取原来的ESP(EIP)存给压进ESP(中断的返回地址)

	lea 44(%esp),%edx		//取原来的ESP(EIP)
	pushl %edx

赋值

	movl $0x10,%edx
	mov %dx,%ds
	mov %dx,%es
	mov %dx,%fs

调用之前存在EAX中的C语言函数

	call *%eax

加8位开始准备出栈

	addl $8,%esp

把之前保存的全部出栈

    pop %fs
    pop %es
    pop %ds
    popl %ebp
    popl %esi
    popl %edi
    popl %edx
    popl %ecx
    popl %ebx
    popl %eax

四、内核中断代码实现过程分析

1.trap_init

​ set_trap_gate 设置的权限较高,只能由用户程序调用

​ set_system_gate 设置的权限较低,能由用户和系统所 有的进程调用

2.system_call

​ 所有的系统调用C函数放到了一个统一的sys_call_table

//定义系统调用的sys_call_table
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid };

​ 系统调用的操作码

系统的进程管理

一、系统进程的运转方式

系统时间:(jiffies 系统滴答) CPU内部有一个RTC作为系统定时器,会在上电的时候调用mktime函数算出从1970年1月1日0时开始到当前开机时间点所过的秒数。(代码在mktime.c)

给mktime函数传来的时间结构体(struct tm)的赋值是初始化时从RTC(CMOS)中读出的参数,转化为时间存入全局变量startup_time中,并为JIFFIES所用。

JIFFIES 是一个系统的时钟滴答,一个系统滴答是10ms,它是系统的脉搏,是定时器。

10ms一个系统滴答----->每隔10ms会引发一个定时器中断(中断服务函数中,首先进行JIFFIES自加 ,再timer_interrupt)

	call do_timer

调用do_timer函数

	if (cpl)//CPL变量是内核中用来指示被中断程序的特定变量	0表示被中断的是内核进程,1表示被中断的是用户进程
		current->utime++;//用户程序运行时间+1
	else
		current->stime++;//内核程序运行时间+1
	if (next_timer) {// next_timer 是连接jiffies变量的所有定时器的事件链表
	// 可以这样想象,jiffies是一个时间轴,然后这个时间轴上每个绳结上绑了一个事件,运行到该绳结就触发对应的事件
		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)();
		}
	}

current->counter ----->进程的时间片

	if ((--current->counter)>0) return;
	current->counter=0;//counter进程的时间片为0,task_struct[]是进程的向量表 
	//counter在哪里用? 进程的调度就是task_struct[]中检索,找时间片最大的进程对象来运行 直到时间片为0退出 之后再进行新一轮调用
	//counter在哪里被设置? 当task_struct[]所有进程的counter都为0,就进行新一轮的时间片分配
	if (!cpl) return;
	schedule();//这个就是进行时间片分配

task_struct 一个进程 task_struct[] 进程向量表

优先级分配:

	(*p)->counter = ((*p)->counter >> 1) +						(*p)->priority;

优先级时间片轮转调度算法

二、如何创建一个新的进程

进程的结构
堆栈
TSS 进程的状态描述符
LDT 局部描述符(数据段、代码段)

1.进程初始化:

在内核初始化的过程中,会创建0号进程,0号进程是所有进程的父进程。

在0号进程中:

  1. 打开标准输入 输出 错误控制句柄
  2. 创建1号进程,如果创建成功,则在1号进程中打开 “/etc/rc” 文件,执行SHELL程序 “/bin/sh”
  3. 0号进程不可能结束,它会在没有其他进程调用的时候调用,只会执行 for(; pause();

2.进程的创建(fork)

大致流程:

  • 在task链表中找一个进程空位存放当前的进程
  • 创建一个task_struct
  • 设置task_struct

进程的创建就是对0号进程或者当前进程的复制,

结构体的复制 把task[0]对应的task_struct复制给新创建的进程的tas_struct
对于栈堆的拷贝 进程创建时复制原有的堆栈

进程的创建是系统调用:

    .align 2
    _sys_fork://fork的系统调用
        call _find_empty_process//调用这个函数
        testl %eax,%eax
        js 1f
        push %gs
        pushl %esi
        pushl %edi
        pushl %ebp
        pushl %eax
        call _copy_process//
        addl $20,%esp
    1:	ret
  • 给当前要创建的进程分配一个进程号。find_empty_process
  • 创建一个子进程的task_struct结构体:
struct task_struct *p;
//其实就是malloc分配内存
p = (struct task_struct *) get_free_page();//在内存分配一个空白页,让指针指向它
  • 将当前的子进程放入到整体进程链表中
	task[nr] = p;
  • 设置创建的task_struct结构体
	//后面全是对这个结构体进行赋值相当于初始化赋值
	p->state = TASK_UNINTERRUPTIBLE;
	p->pid = last_pid;
	p->father = current->pid;
	p->counter = p->priority;
	p->signal = 0;
	p->alarm = 0;
	p->leader = 0;		/* process leadership doesn't inherit */
	p->utime = p->stime = 0;
	p->cutime = p->cstime = 0;
	p->start_time = jiffies;//当前的时间
	p->tss.back_link = 0;
	p->tss.esp0 = PAGE_SIZE + (long) p;
	p->tss.ss0 = 0x10;
	p->tss.eip = eip;
	p->tss.eflags = eflags;
	p->tss.eax = 0;//把寄存器的参数添加进来
	p->tss.ecx = ecx;
	p->tss.edx = edx;
	p->tss.ebx = ebx;
	p->tss.esp = esp;
	p->tss.ebp = ebp;
	p->tss.esi = esi;
	p->tss.edi = edi;
	p->tss.es = es & 0xffff;
	p->tss.cs = cs & 0xffff;
	p->tss.ss = ss & 0xffff;
	p->tss.ds = ds & 0xffff;
	p->tss.fs = fs & 0xffff;
	p->tss.gs = gs & 0xffff;
	p->tss.ldt = _LDT(nr);
	p->tss.trace_bitmap = 0x80000000;
    if (last_task_used_math == current)//如果当前进程使用了协处理器,就设置当前创建的进程的协处理器
    __asm__("clts ; fnsave %0"::"m" (p->tss.i387));
	if (copy_mem(nr,p)) {//老进程向新进程代码段和数据段进行拷贝
		task[nr] = NULL;//如果失败了
		free_page((long) p);//就释放当前页
		return -EAGAIN;
	}
	for (i=0; i<NR_OPEN;i++)//
		if (f=p->filp[i])//父进程打开过文件
			f->f_count++;//就会打开文件的计数+1,说明会继承这个属性
	if (current->pwd)//跟上面一样
		current->pwd->i_count++;
	if (current->root)
		current->root->i_count++;
	if (current->executable)
		current->executable->i_count++;
	set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
	set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
	p->state = TASK_RUNNING;//把状态设定为可运行状态	/* do this last, just in case */
	return last_pid;//返回新创建进程的id号

三、进程调度

进程被创建到了链表中,如何进行进一步的调用和调度呢?

​ void schedule(void); 进程调度函数

​ switch_to(next); 进程切换函数

进程状态 宏定义运行状态 说明
运行状态 #define TASK_RUNNING 0 可以被运行,只有在这个状态才能进行进程切换
可中断睡眠状态 #define TASK_INTERRUPTIBLE 1 可以被信号中断,变成running
不可中断睡眠状态 #define TASK_UNINTERRUPTIBLE 2 只能被wakeup唤醒,变成running
暂停状态 #define TASK_ZOMBIE 3 收到SIGSTOP SIGTSTP SIGTTIN这几个信号就会暂停
僵死状态 #define TASK_STOPPED 4 进程停止运行,但是父进程还未将其清空

1.进程调度函数代码分析

// 时间片分配和进程根据优先度调度
void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;

/* check alarm, wake up any interruptible tasks that have got a signal */

	for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
		if (*p) {//alarm是用来设置警告,比如jiffies有1000个可能其中一些需要警告那么就用alarm来实现
			if ((*p)->alarm && (*p)->alarm < jiffies) {
					(*p)->signal |= (1<<(SIGALRM-1));
					(*p)->alarm = 0;
				}
				//~(_BLOCKABLE & (*p)->blocked  
				//&&(*p)->state==TASK_INTERRUPTIBLE
				//用来排除非阻塞信号
				//如果该进程为可中断睡眠状态 则如果该进程有非屏蔽信号出现就将该进程的状态设置为running
			if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
			(*p)->state==TASK_INTERRUPTIBLE)
				(*p)->state=TASK_RUNNING;
		}

/* this is the scheduler proper: */
	// 以下思路,循环task列表 根据counter大小决定进程切换
	while (1) {
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
		while (--i) {
			if (!*--p)
				continue;//进程为空就继续循环
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)//找出c最大的task
				c = (*p)->counter, next = i;
		}
		if (c) break;//如果c找到了,就终结循环,说明找到了
		//如果c=0,说明所有进程的时间片都用完了,进行时间片的重新分配
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)//这里很关键,在低版本内核中,是进行优先级时间片轮转分配,这里搞清楚了优先级和时间片的关系
			//counter = counter/2 + priority
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
	//切换到下一个进程 这个功能使用宏定义完成的
	switch_to(next);
}

2.进程切换函数代码分析

// 进程切换是用汇编宏定义实现的
//1. 将需要切换的进程赋值给当前进程的指针
//2. 将进程的上下文(TSS和当前堆栈中的信息)切换
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \
	"je 1f\n\t" \
	"movw %%dx,%1\n\t" \
	"xchgl %%ecx,_current\n\t" \
	"ljmp %0\n\t" \
	"cmpl %%ecx,_last_task_used_math\n\t" \
	"jne 1f\n\t" \
	"clts\n" \
	"1:" \
	::"m" (*&__tmp.a),"m" (*&__tmp.b), \
	"d" (_TSS(n)),"c" ((long) task[n])); \
}

3.进程休眠函数代码分析

// 当某个进程想访问CPU资源,但是CPU资源被占用访问不到,就会休眠
void sleep_on(struct task_struct **p)
{
	struct task_struct *tmp;

	if (!p)//如果传进来的是空的 就返回
		return;
	if (current == &(init_task.task))//当前进程是0号 
		panic("task[0] trying to sleep");//就打印并且返回
	tmp = *p;
	*p = current;//这两步相当于用头插法给task_struct链表添加了一个新node
	// 其实核心就是把当前在请求CPU资源的进程的task_struct的state置为TASK_UNINTERRUPTIBLE(只能由wakeup唤醒的不可中断睡眠状态)
	current->state = TASK_UNINTERRUPTIBLE;
	schedule();
	if (tmp)
		tmp->state=0;
}

四、进程的退出/销毁(exit)

syscall和do开头的基本都是中断服务调用函数

1.exit是销毁函数-----一个系统调用-----do_exit-----子进程自身的操作

  • 首先该函数会释放进程的代码段和数据段占用的内存
	//释放内存页
	free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));
	free_page_tables(get_base(current->ldt[2]),get_limit(0x17));
  • 关闭进程打开的所有文件,对当前的目录和 i 节点进行同步(文件操作)
	for (i=0 ; i<NR_OPEN ; i++)//每个进程能打开的最大文件数NR_OPEN=20
		if (current->filp[i])
			sys_close(i);//关闭文件
	iput(current->pwd);
	current->pwd=NULL;
	iput(current->root);
	current->root=NULL;
	iput(current->executable);
	current->executable=NULL;
  • 如果当前要销毁的进程有子进程,那么就让1号进程作为新的父进程(init进程)
    //current->pid就是当前需要关闭的进程
    for (i=0 ; i<NR_TASKS ; i++)
        if (task[i] && task[i]->father == current->pid) {//如果当前进程是某个进程的父进程
            task[i]->father = 1;//就让1号进程作为新的父进程
            if (task[i]->state == TASK_ZOMBIE)//如果是僵死状态
                /* assumption task[1] is always init */
                (void) send_sig(SIGCHLD, task[1], 1);//给父进程发送SIGCHLD
        }
  • 如果当前进程是一个会话头进程,则会终止会话中的所有进程
    if (current->leader && current->tty >= 0)
        tty_table[current->tty].pgrp = 0;//清空终端		tty是控制台
    if (last_task_used_math == current)
        last_task_used_math = NULL;//清空协处理器
    if (current->leader)	//如果当前进程是会话头进程,则关闭会话的所有进程
        kill_session();//清空session
  • 改变当前进程的运行状态,变成TASK_ZOMBIE僵死状态,并且向其父进程发送信号SIGCHLD
    current->state = TASK_ZOMBIE;//设为僵死状态
    current->exit_code = code;
    tell_father(current->father);
  • 重新调度进程,正常运行
	schedule();

2.sys_waitpid-----父进程的操作

  • 父进程在运行子进程的时候会运行wait waitpid这两个函数(父进程等待某个子进程终止)。当父进程收到SIGCHLD信号时,父进程会终止僵死状态的子进程。
	int sys_waitpid(pid_t pid,unsigned long * stat_addr, int options);
  • 首先父进程会把子进程的运行时间累加到自己的进程变量中
    current->cutime += (*p)->utime;
    current->cstime += (*p)->stime;
  • 把对应的子进程的进程描述结构体进行释放,置空任务数组的槽位
	release(*p);

3.中间用到的一些函数

//完成清空了任务描述表task[]中对应task,释放对应内存页(代码段、数据段)
void release(struct task_struct * p);

//给指定的p进程发送对应的sig信号
static inline int send_sig(long sig,struct task_struct * p,int priv);

//终止当前进程的会话,给其发送SIGHUP
static void kill_session(void)
{
	struct task_struct **p = NR_TASKS + task;
	
	while (--p > &FIRST_TASK) {//从最后一个开始扫描(不包括0进程,因为0进程不能被关闭,它是所有进程的父进程)
		if (*p && (*p)->session == current->session)
			(*p)->signal |= 1<<(SIGHUP-1);
	}
}

// 系统调用 向任何进程 发送任何信号(类比shell中的kill命令也是发送信号的意思)
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) {//pid>0给对应进程发送信号
		if (*p && (*p)->pid == pid) 
			if (err=send_sig(sig,*p,0))
				retval = err;
	} else if (pid == -1) while (--p > &FIRST_TASK)//pid=-1给任何进程发送
		if (err = send_sig(sig,*p,0))
			retval = err;
	else while (--p > &FIRST_TASK)//pid<-1 给进程组发送信息
		if (*p && (*p)->pgrp == -pid)
			if (err = send_sig(sig,*p,0))
				retval = err;
	return retval;
}
/*PID	(
    		PID>0	给对应的PID发送SIG
    		PID=0	给当前进程的进程组发送SIG
    		PID=-1	给任何进程发送SIG
    		PID<-1	给进程组号为-PID的进程组发送SIG
    	)*/

//子进程告诉父进程自己要死了
static void tell_father(int pid);

五、进程间通信

操作系统的引导和启动程序

一、Linux操作系统的引导

本章探求三个问题:

  • Linux是如何从硬盘中读出的
  • Linux在启动的时候是如何拿到硬件参数的
  • Linux在初时运行中都做了什么

trap.c trap_init()

mktime.c time_init()

sched.c sched_init()

1.BIOS/Bootloader

由PC机的 BIOS ( 0XFFFF0 是 BIOS 存储的总线地址)把 bootsect 从某个固定的地址拿到内存中某个固定地址(每个版本Linux系统是不同的,如0.11是 0X90000 ),并进行一系列的硬件初始化和参数设置。

2.bootsect.s

bootsect.s 是 磁盘引导块程序,是 在磁盘的第一个扇区中的程序( 0磁道 0磁头 1扇区 )

作用:

  • 首先将后续的 setup.s 代码从磁盘中加载到内存中紧接着 bootsect.s 的地方
  • 在显示屏上显示 loading system 再将 system (操作系统) 模块加载到0x10000的地方
  • 最后跳转到 setup.s 中运行

3.setup.s

  • 解析BIOS/BOOTLOADER传递来的参数
  • 设置系统内核运行的LDT(局部描述符) IDT(中断描述符寄存器) GDT(全局描述符)
  • 设置中断控制芯片,进入保护模式运行 (svc32 保护模式 设置寄存器中的值)
  • 跳转到system模块的最前面的代码运行 (head.s)

4.head.s

  • 加载内核运行时的各数据段寄存器,重新设置中断描述符表
  • 开启内核正常运行时的协处理器等资源
  • 设置内存管理的分页机制
  • 跳转到main.c开始运行

5.main.c

 	//设置操作系统的根文件
	ROOT_DEV = ORIG_ROOT_DEV;
	//设置操作系统驱动参数
 	drive_info = DRIVE_INFO;
	//解析setup.s代码后获取系统内存参数
	//设置系统的内存大小	系统本身内存(1MB)+扩展内存大小(参数*KB)
	memory_end = (1<<20) + (EXT_MEM_K<<10);
	//取整4k的内存大小
	memory_end &= 0xfffff000;
	if (memory_end > 16*1024*1024)//控制操作系统的最大内存为16M
		memory_end = 16*1024*1024;
	if (memory_end > 12*1024*1024) 
		buffer_memory_end = 4*1024*1024;//设置高速缓冲区的大小,跟块设备有关,跟设备交互的时候,充当缓冲区,写入到块设备中的数据先放在缓冲区里,只有执行sync时才真正写入;这也是为什么要区分块设备驱动和字符设备驱动;块设备写入需要缓冲区,字符设备不需要是直接写入的
	else if (memory_end > 6*1024*1024)
		buffer_memory_end = 2*1024*1024;
	else
		buffer_memory_end = 1*1024*1024;
	main_memory_start = buffer_memory_end;
#ifdef RAMDISK
	main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif

二、Linux操作系统的启动

系统初始化的起点:磁盘引导程序,需要将内核移入内存进行运行,并初始化多种模块和硬件

系统初始化的终点:运行第一个应用程序——系统的根文件系统

1.初始化init

	if (!fork()) {	//创建0号进程 fork函数就是用来创建进程的函数	/* we count on this going ok */
		//0号进程是所有进程的父进程
		init();
void init(void)
{
	int pid,i;
	//设置有关硬件的驱动信息
	setup((void *) &drive_info);
	//打开标准输入控制台 句柄为0
	(void) open("/dev/tty0",O_RDWR,0);//打开标准输入控制台
	(void) dup(0);//打开标准输出控制台 这里是复制句柄的意思
	(void) dup(0);//打开标准错误控制台
	printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
		NR_BUFFERS*BLOCK_SIZE);
	printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
	
	//创建了1号进程,如果0号父进程创建进程成功则fork函数返回0,如果在子进程中fork则返回父进程PID
	//如果fork返回值为0,则其在新进程中执行;如果返回值不为0,为子进程的进程号,则在父进程中执行
	if (!(pid=fork())) {//这里创建1号进程
		close(0);//关闭了0号进程的标准输入输出

		//打开系统配置文件
		if (open("/etc/rc",O_RDONLY,0))//如果1号进程创建成功打开/etc/rc这里面保存的大部分是系统配置文件 开机的时候要什么提示信息全部写在这个里面
			_exit(1);
		execve("/bin/sh",argv_rc,envp_rc);//挂接文件系统,运行shell程序
		_exit(2);//shell程序一般不会关闭,所以这个exit不会执行
	}
	//在0号进程中等待子进程退出
	if (pid>0)//如果这个不是0号进程
		while (pid != wait(&i))//就等待父进程退出
			/* nothing */;
	while (1) {
		if ((pid=fork())<0) {//如果创建失败
			printf("Fork failed in init\r\n");
			continue;//进入下次循环重新创建子进程
		}
		//如果创建成功,在新创建的子进程中执行
		if (!pid) {//这个分支里面是进行进程的再一次创建
			close(0);close(1);close(2);//关闭之前的所有控制台
			setsid();//重新设置id
			(void) open("/dev/tty0",O_RDWR,0);
			(void) dup(0);
			(void) dup(0);//重新打开新的控制台
			//执行shell脚本
			_exit(execve("/bin/sh",argv,envp));//这里不是上面的argv_rc和envp_rc了是因为怕上面那种创建失败,换了一种环境变量来创建,过程和上面是一样的其实
		}
		//如果还在父进程中,则进行等待子进程退出,并重新开始循环
		//子进程执行shell,父进程等待回收
		while (1)
			if (pid == wait(&i))
				break;
		printf("\n\rchild %d died with code %04x\n\r",pid,i);
		sync();
	}
	_exit(0);	/* NOTE! _exit, not exit() */
}

2.操作系统如何移植

操作系统移植的过程分为2步:

  • 进行操作系统初始化的适配,能够让main在你的板子上跑起来
  • 进行驱动的移植

BOOTLOADER的启动内核代码

创建theKernel函数指针

void (*theKernel)(int zero, int arch, uint params);

把指针移到ih_ep上去,Linux的启动入口

theKernel = (void	(*) (int, int, uint))ntohl(hdr->ih_ep);

执行Linux并传入参数

theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
//bd->bi_arch_number 称为 process id——CPU的架构号
//bd->bi_boot_params 称为 参数地址

theKernel跳转到head.S文件__lookup_processor_type

比对当前板子的CPU是否支持Linux,如果不支持就不启动直接退出,如果支持则继续运行

	mrc	p15, 0, r9, c0, c0		@ get processor id
	bl	__lookup_processor_type		@ r5=procinfo r9=cpuid
	movs	r10, r5				@ invalid processor (r5=0)?
 THUMB( it	eq )		@ force fixup-able long branch encoding
	beq	__error_p			@ yes, error 'p'

设置物理内存的页面

	adr	r3, 2f
	ldmia	r3, {r4, r8}
	sub	r4, r3, r4			@ (PHYS_OFFSET - PAGE_OFFSET)
	add	r8, r8, r4			@ PHYS_OFFSET

跳转到__vet_atags,验证参数是否完整(tag_list)

	bl	__vet_atags

创建虚拟内存映射表

	bl	__create_page_tables

把函数__mmap_switched的地址装载进链接寄存器

	//创建完虚拟内存映射表后,要将旧的地址转化为虚拟地址,代码重定义,也就是__mmap_switched
	ldr	r13, =__mmap_switched		@ address to jump to after
	
	__mmap_switched:
	adr	r3, __mmap_switched_data

	ldmia	r3!, {r4, r5, r6, r7}
	cmp	r4, r5				@ Copy data segment if needed
1:	cmpne	r5, r6
	ldrne	fp, [r4], #4
	strne	fp, [r5], #4
	bne	1b

	mov	fp, #0				@ Clear BSS (and zero fp)
1:	cmp	r6, r7
	strcc	fp, [r6],#4
	bcc	1b

 ARM(	ldmia	r3, {r4, r5, r6, r7, sp})
 THUMB(	ldmia	r3, {r4, r5, r6, r7}	)
 THUMB(	ldr	sp, [r3, #16]		)
	str	r9, [r4]			@ Save processor ID
	str	r1, [r5]			@ Save machine type
	str	r2, [r6]			@ Save atags pointer
	bic	r4, r0, #CR_A			@ Clear 'A' bit
	stmia	r7, {r0, r4}			@ Save control register values
	b	start_kernel		//跳转到start_kernel
ENDPROC(__mmap_switched)

进程初始化C函数的调用,跳转到start_kernel,这就是我们整个内核的启动函数了(在init文件夹的Main.c中)

	asmlinkage void __init start_kernel(void);

这个函数有非常多的代码,进行了一系列的初始化。这里就不一一列举。

这里调用了一个重要的函数 setup_arch(&command_line);

setup_arch里面调用了另一个setup_processor()函数,而setup_processor()中

	struct proc_info_list *list; //创建了一个CPU指令集描述结构体	
	list = lookup_processor_type(read_cpuid_id());//从指定的内存中获取到该描述结构体
	cpu_name = list->cpu_name;//将获取到结构体的CPU名字赋值给一个全局变量

我们再回溯到setup_arch函数中setup_processor()的下一步,调用了一个 setup_machine_fdt 函数

	//找到一个移植Linux时写的最适合的machine_desc结构体,并返回
	mdesc = setup_machine_fdt(__atags_pointer);

setup_machine_fdt 函数又调用了一个宏定义for_each_machine_desc(mdesc)

	for_each_machine_desc(mdesc) {
		score = of_flat_dt_match(dt_root, mdesc->dt_compat);
		if (score > 0 && score < mdesc_score) {
			mdesc_best = mdesc;
			mdesc_score = score;
		}

#define for_each_machine_desc(p)			\
	for (p = __arch_info_begin; p < __arch_info_end; p++)
//这里我们可以分析出它实在遍历一个代码段

3.内核如何进行多平台的适配,在内核中如何认识这些板子?

这个问题的答案:结构体 machine_desc

链接脚本:vmlinux.lds.S

	.init.arch.info : {
		__arch_info_begin = .;
		*(.arch.info.init)	//代码段
		__arch_info_end = .;
	}

ARCH.H 中的宏定义

#define MACHINE_START(_type,_name)			\
static const struct machine_desc __mach_desc_##_type	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= MACH_TYPE_##_type,		\
	.name		= _name,

#define MACHINE_END				\
};

多种板子的BSP文件中出现的宏定义调用,以下是一块三星板子的例子

MACHINE_START(S3C2440, "SMDK2440")
	/* Maintainer: Ben Dooks  */
	.atag_offset	= 0x100,

	.init_irq	= s3c24xx_init_irq,
	.map_io		= smdk2440_map_io,
	.init_machine	= smdk2440_machine_init,
	.timer		= &s3c24xx_timer,
	.restart	= s3c244x_restart,
MACHINE_END

结合上面三段代码展开:

#define MACHINE_START(S3C2440,"SMDK2440")			\
static const struct machine_desc __mach_desc_S3C2440	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= MACH_TYPE_S3C2440,		\
	.name		= "SMDK2440",
	/* Maintainer: Ben Dooks  */
	.atag_offset	= 0x100,

	.init_irq	= s3c24xx_init_irq,
	.map_io		= smdk2440_map_io,
	.init_machine	= smdk2440_machine_init,
	.timer		= &s3c24xx_timer,
	.restart	= s3c244x_restart,
};

总结:

  • machine_desc结构体,用于Linux做设备板子的识别结构体,这些结构体被限定在了内存的某一篇区域。
  • 并且通过UBOOT传过来的参数进行该结构体的配置(通过检索taglist的方式来设置)
  • 并且在移植Linux的时候,也要对结构体的变量进行赋值
  • 并且在之后的启动或其它函数中,对该结构体的变量进行调用
//想去搜索processor的id,调用__lookup_processor_type
mrc	p15, 0, r9, c0, c0		@ get processor id
bl	__lookup_processor_type		@ r5=procinfo r9=cpuid
//__lookup_processor_type函数把
//__lookup_processor_type_data这样一块地址存进r3
//并进行一系列判断,如果相同,则运行,不相同,则报错
__lookup_processor_type:
	adr	r3, __lookup_processor_type_data
	//搜索得到__lookup_processor_type_data的定义,是
	//从__proc_info_begin到__proc_info_end的一块地址
	.type	__lookup_processor_type_data, %object
__lookup_processor_type_data:
	.long	.
	.long	__proc_info_begin
	.long	__proc_info_end
	.size	__lookup_processor_type_data, . - __lookup_processor_type_data
//我们从链接脚本vmlinux.lds.S发现其中存的是.proc.info.init这块区域
VMLINUX_SYMBOL(__proc_info_begin) = .;				\
*(.proc.info.init)						\
VMLINUX_SYMBOL(__proc_info_end) = .;
//再继续搜索.proc.info.init得到了一堆proc-开头的arm内核的指令集
//这些东西是Linux写好的,也就是Linux支持的型号

//再回到开头,我们得到了设备的id。
mrc	p15, 0, r9, c0, c0		@ get processor id
//通过层层深挖,我们发现它要和Linux支持的
//板子型号进行遍历检索对比,如果有相同的,则运行,不相同,则报错
bl	__lookup_processor_type		@ r5=procinfo r9=cpuid

__lookup_processor_type:
	adr	r3, __lookup_processor_type_data
	ldmia	r3, {r4 - r6}
	sub	r3, r3, r4			@ get offset between virt&phys
	add	r5, r5, r3			@ convert virt addresses to
	add	r6, r6, r3			@ physical address space
1:	ldmia	r5, {r3, r4}			@ value, mask
	and	r4, r4, r9			@ mask wanted bits
	teq	r3, r4
	beq	2f
	add	r5, r5, #PROC_INFO_SZ		@ sizeof(proc_info_list)
	cmp	r5, r6
	blo	1b
	mov	r5, #0				@ unknown processor
2:	mov	pc, lr
ENDPROC(__lookup_processor_type)

4.系统运行的第一个应用程序 根文件系统的挂接

static noinline int init_post(void)
{
	/* need to finish all async __init code before freeing the memory */
	async_synchronize_full();
	free_initmem();
	mark_rodata_ro();
	system_state = SYSTEM_RUNNING;
	numa_default_policy();


	current->signal->flags |= SIGNAL_UNKILLABLE;

	if (ramdisk_execute_command) {
		run_init_process(ramdisk_execute_command);
		printk(KERN_WARNING "Failed to execute %s\n",
				ramdisk_execute_command);
	}

	/*
	 * We try each of these until one succeeds.
	 *
	 * The Bourne shell can be used instead of init if we are
	 * trying to recover a really broken machine.
	 */
	if (execute_command) {//如果execute_command不为空 就运行这个命令
		run_init_process(execute_command);
		printk(KERN_WARNING "Failed to execute %s.  Attempting "
					"defaults...\n", execute_command);
	}//如果execute_command为空则执行下面四个
	run_init_process("/sbin/init");
	run_init_process("/etc/init");
	run_init_process("/bin/init");
	run_init_process("/bin/sh");//这几个必须由文件系统提供
	//这个panic表达的就是如果文件系统没找到则linux启动失败
	panic("No init found.  Try passing init= option to kernel. "
	      "See Linux Documentation/init.txt for guidance.");
}
static noinline int init_post(void)
{
	/* need to finish all async __init code before freeing the memory */
	async_synchronize_full();
	free_initmem();
	mark_rodata_ro();
	system_state = SYSTEM_RUNNING;
	numa_default_policy();


	current->signal->flags |= SIGNAL_UNKILLABLE;

	if (ramdisk_execute_command) {
		run_init_process(ramdisk_execute_command);
		printk(KERN_WARNING "Failed to execute %s\n",
				ramdisk_execute_command);
	}

	/*
	 * We try each of these until one succeeds.
	 *
	 * The Bourne shell can be used instead of init if we are
	 * trying to recover a really broken machine.
	 */
	if (execute_command) {//如果execute_command不为空 就运行这个命令
		run_init_process(execute_command);
		printk(KERN_WARNING "Failed to execute %s.  Attempting "
					"defaults...\n", execute_command);
	}//如果execute_command为空则执行下面四个
	run_init_process("/sbin/init");
	run_init_process("/etc/init");
	run_init_process("/bin/init");
	run_init_process("/bin/sh");//这几个必须由文件系统提供
	//这个panic表达的就是如果文件系统没找到则linux启动失败
	panic("No init found.  Try passing init= option to kernel. "
	      "See Linux Documentation/init.txt for guidance.");
}

你可能感兴趣的:(linux,学习,驱动开发)