版权声明:本文为CSDN博主「VeronicaZhu」的原创文章,遵循 CC 4.0 BY-SA 版权协议
原文链接:https://blog.csdn.net/veronicazhu/article/details/18741425
一、Minix3的启动:
l 硬件读入引导程序,引导程序装入boot,Boot在内存中装入引导映像(包含内核、pm、fs等)
l 内核初始化(/Kernel/main.c):
1、初始化进程表和特权进程表:
状态设成空闲、区别任务还是其他进程设置p_nr值、将priv[i]映射到ppriv_addr[i]等
2、初始化引导映像中的程序,将进程名、进程指针等都复制到proc表中,并设置它们的特权(是否允许陷阱、是否能够运行kernel call等等);如果是内核任务或者根系统进程则可以立即被调度,其他进程则不能被调度;
3、初始化栈、建立内存映射、初始化寄存器值
4、初始化服务器的栈指针,将proc_ptr 指向当前进程,入队,设为可调度状态
5、系统任务system获得CPU,system初始化后进入阻塞状态
6、时钟任务获得CPU,它初始化后企图接受时钟中断程序发来的消息并阻塞
7、其他驱动程序和服务器进程获得CPU并初始化
8、CPU最后交给第一个普通用户进程INIT
l Init执行/etc/rc脚本,启动其他不在引导映像中的驱动器和服务器(服务等),Rc脚本检查上次是否正常关机
l Init根据/etc/ttytab文件为终端创建子进程,执行/usr/bin/getty等待用户登录,成功登陆后执行shell,等待用户命令
二、消息机制的实现:(/Kernel/proc.c)
l 构造notify(宏定义BuildNotifyMessage):
1、将消息的类型改变为notify
2、调用get_uptime函数读取时钟节拍让通知中包含时间戳
3、判断消息的发起者,如果是HARDWARE,则置位目标进程的中断位图,如果是SYSTEM,则置位目标进程的信号位图
l 消息传递(do_ipc)
1、跟踪进程
2、同步消息传递则调用do_sync_ipc
异步消息传递调用mini_senda
l 同步的消息传递(do_sync_ipc)
1、检查各种错误:只有系统调用号码小于32的才允许调用;只有是接收信息才能将目的进程设为任意;指定的源进程和目的进程是否有效;发起进程是否允许向目的进程发送消息;该进程是否有特权发出调用
2、根据消息的类型调用函数
SENDREC:设置标志位关中断,继续调用SEND和RECEIVE
SEND:调用mini_send,判断是否完成发送
RECEIVE:清空IPC的状态位,调用mini_receive
NOTIFY:调用mini_notify
SENDNB:调用mini_send,未准备好时不堵塞
其他:出错
l 具体实现函数:
1、Mini_send:
检查目的进程是否因为等待本条消息而堵塞(等待的对象是本发送进程或是ANY),复制消息,接收进程通过复位RECEIVING位取消阻塞,将接收进程入队使它可以运行;如果接收进程不是在等待本条消息,检查死锁,将发送进程阻塞并入队到接收进程的等待发送队列的末尾
2、Mini_receive:
检查等待队列中是否有进程在等待发送消息,如果有则寻找目的进程并接收消息,将已完成接收消息的目的进程出队;否则如果队列为空则检查位图判断是否有挂起的通知,如果发现挂起的是本进程在等待的通知,则标记为不再挂起,并复制接收通知,返回OK;如果没有找到,则检查MF_ASYNMSG位看是否有挂起的senda,如果有则尝试异步接收;没有找到适合的消息,则再次检查死锁,将本进程堵塞
3、Mini_notify:
与mini_send类似,如果通知的接收者堵塞且在等待接收信息,那么构造消息发送,并使接收者离开等待队列;如果接收者没有等待消息,则置位挂起信号的位图并返回
4、Mini_senda:
清空表,重设表头地址和表中参数的数目;检查要发送的数量,将入口不为空且内核还没有处理的位读入;检查各种出错可能;检查接收者是否在等待本条消息,若是则复制发送;若不是,则告知接收者。如果接收者没有立即接收消息,则记录表头地址和参数数目
三、 进程调度
l 头文件定义在(/servers/sched/schedproc.h)
结构体包括进程的端点,标志位,最高允许特权级,当前特权级和时间片长度
l 初始化和接收消息(/servers/sched/main.c)
1、本地启动并初始化
2、等待下一条消息,如果是通知的话判断消息发送者是不是时钟,是的话检查时间戳是否已经过期,不做答复;如果不是时钟,则答复给发送者“功能没有实现”
3、向sched服务器发送消息请求操作
l 具体调度(/servers/sched/schedule.c)
1、do_noquantum:
时间片用完后,如果本进程的优先级高于普通进程的最低优先级,则将进程降级,由内
核将该进程挂到队伍末尾
2、do_stop_scheduling:
接收来自PM和RS的所有消息,设置标志位表示schedule的插槽停止使用
3、do_start_scheduling:
接收来自PM和RS的所有消息,端口有效时根据消息设置该进程的schedproc的端点等内容,检查schedproc的类型,如果是刚生成的,则设置优先级为可允许的最高优先级,分配完整时间片,如果是继承父母的,则设置优先级和时间片长度都与父母进程的一致。设置标志位表明schedproc的插槽正在使用。接管调度的过程,利用内核调用回复消息填充进程当前的优先级和时间片。利用系统调用分配时间片。标记自己为新的调度。
4、do_nice:
保留请求进程的旧信息(包括旧的优先级、旧的最高优先级),将新的优先级设为最高优先级,由内核将其挂回队伍中,如果挂回过程中发生错误,则恢复旧的值
l 定时升级(balance_queues)
设置定时,每100ticks将时间片运转完的进程的优先级提高一级
四、时钟服务
l 启动定时器中断处理(bsp_timer_int_handler):
1、先获得现时的tick数(lost_ticks为clock_task未执行的时候外部记录的tick数,要将其加到当前时间中),更新时间
2、调用中断处理函数
3、若下次timeout时间已到,证明计时器过时,要通知clock task
l 获得节拍数(get_uptime):
返回自系统启动后经过的ticks数
l 新设计时器(set_timer):
1、将新的timer结构体插入clock_timers队列,注意这个队列是按照溢出时间由小到大排列的
2、用clock_timers->tmr_exp_time设定新的下次timeout的时间
l 删除计时器(reset_timer):
1、将不再需要的计时器从有效和过期的timer队列中删除,更新下次timeout的时间
2、若timer队列为空,则设定下次timeout时间为NEVER
l 更新负载(load_update):
1、将准备好的进程入队,计算平均负载
2、更新结构的时间
l 中断处理函数(ap_timer_int_handler):
1、若进程可以计费则更新它的系统时间和剩余节拍数
2、递减虚拟计时器,如果适用,递减当前进程的虚拟和真正计时器
3、如果计时器过期,则发出信号
4、更新平均负载
五、VFS
l 文件系统的总循环(/servers/vfs/main.c)
1、get_work接收信息设置全局变量who为调用者的进程表项号,把call_nr设置为即将执行的系统调用的编号。
2、判断如果消息请求是唤醒之前堵塞在管道或终端的进程,返回该进程的端点并唤醒该进程
3、根据不同的请求,比如重新打开、关闭等执行应答
4、处理特殊信息,如果请求是通知,且来自时钟,检查定时器是否已经过期,如果有必要则更新定时器队列
5、如果通知的发送者是DS,从DS获得事件和用户,并检查由VFS驱动的事件
6、如果通知的发送者是其他设备,则认为一个设备驱动程序已经就绪,调用dev_status处理
7、指向调用者的进程表入口,检查该进程是否被挂起,这种情况下返回的状态码被忽略,提示出错
8、如果消息发送者是PM,则调用service_pm根据消息的类型处理最后发送给PM处理应答消息
9、如果进程被堵塞,则不用发送应答消息
l 文件系统的初始化(/servers/vfs/main.c/sef_cb_init_fresh)
1、清空进程的端点区域
2、循环接收信息获得PM的pm_init初始化函数发出的消息,消息包括进程号、插槽数量和PID,设置用户的真实、有效PID等参数,直到收到进程号字段的值为“NONE”的消息则结束循环,发送同步消息给PM
3、调用build_map初始化dmap表,dmap表项对于每个设备提供了主设备号和响应的设备驱动程序之间的映射,当一个设备被打开、关闭、读或写的时候,dmap会提供响应操作的处理函数的名字
4、将所有设备的驱动程序加入到引导映像中,并初始化根设备和载入超级块
5、遍历进程表的文件系统部分,这样从引导映像中装入的每一个进程都可以识别根目录,并使用根目录作为工作目录
l 创建和打开文件(/servers/vfs/open.c)
1、do_creat函数提取文件名和文件长度,再调用common_open创建新文件并设置标志位
do_open函数检测该文件是否需要被创建,则读取创建的模式,再调用common_open打开
2、在common_open中,检查是否有空闲的文件描述符和空闲的filp表项,再检查标志位,如果需要创建新文件,则运行new_node创建新的inode并返回指向inode的指针,如果目录项已经存在,则同样返回指针但设置错误位。如果不需要创建,则扫描路径名,对文件描述符进行赋值、申请filp项。
3、如果文件不是新建的,则检查保护位,如果该文件是一个普通文件且已经设置了O_TRUNC位,再次调用forbidden确认文件可写,如果可写则将文件长度变为0,重新初始化vnode
4、对于其他文件类型(如目录、设备文件等)进行测试,目录只能读但不能被写,特殊进程的设备文件则返回打开设备的文件描述符,对于设备用dmap结构的函数打开,对于命名管道则调用pipe_open。
5、检查返回值,如果出现错误,则释放inode,并收回文件描述符和filp表项,若打开成功,则返回文件描述符
l 读文件(/servers/vfs/read.c)
1、 Do_read函数调用read_write函数并设置READING标志位
2、 Read_write函数中,读取vnode,长度和模式;判断是否是从管道中读写,是的话调用rw_pipe;判断是否是字符设备文件或块设备文件;字符设备文件中读取数据不需要经过块高速缓存
3、 对于一般文件,向文件系统发出请求,文件系统将逻辑文件位置转换为物理块号,在高速缓存中搜索块,并将信息返回到用户空间
4、 改变文件的位置
l 写文件(/servers/vfs/write.c)
1、同样调用read_write函数,但要检查O_APPEND位,看是否被设置了数据文件的搜索路径
2、写入后更新文件大小和修改时间
l 管道(/servers/vfs/pipe.c)
1、 创建管道(do_pipe)
申请一个空的vnode和两个未使用的文件描述符,向PFS发消息申请创建一个有名管道的inode,填充vnode的消息和filp项的消息
2、 映射vnode(map_vnode)
创建一个从现inode到PFS的临时映射,读写操作都是发消息由PFS实际完成
3、 检查管道(pipe_check)
如果管道为空但写者存在,则挂起读者,如果管道为空而没有写者,则返回空字节,如果写者存在但没有读者,则这是一个断裂管道报告错误;还要唤醒由于没有数据或者数据过多而被挂起的进程
4、 挂起管道(suspend)
将调用参数保存在进程表中,增加被挂起的管道数目
5、 试图唤醒被挂起的管道(release)
检查被挂起的管道,看文件指针是否仍然可用于文件操作,如果仍然可用,则调用revive唤醒管道,并减少被挂起的管道数目
六、常用系统调用
l Fork(lib/libc/posix/_fork.c):
1、执行系统调用 _syscall(PM_PROC_NR, FORK, &m),向进程管理器(PM)发送一个消息创建一个新进程,以 sendrec 方式发送。消息传递到 PM 后,PM 执行了 do_fork() 函数(servers/pm/forkexit.c)。
2、do_fork() 函数检查进程表是否还有空间,查看是否有 mproc 子进程的 slot,如果没有返回错误。
3、执行 vm_fork(),向 VM 发出消息。转到Servers/vm/fork.c中,VM拷贝一份父进程的 vmproc 信息到子进程的 vmproc 中,分配页表和相应的虚拟地址空间。Minix 默认使用的是独立的 I 和D 空间,设置子进程mp_seg[]表项中的数据段和栈,指向新的内存区,由于此处的数据段和正文段并未分开而数据区中的内容完全是从父进程中拷贝过来的,所以mp_seg[T].mem_phys中指的新内存中的正文段中的内容和父进程中正文段中的内容是一致的。
4、复制内存内容,设置对应的 vm_flags 和 priv 结构体中的 flags,告知 kernel 成功fork(调用 sys_fork),陷入 Kernel。sys_fork 对应了 kernel/system/do_fork(),首先复制 proc 结构,并使之暂时不能运行,然后去除特权级别,返回。
5、返回后, PM发送异步消息,通知 VFS 进行相应的 fork 操作(servers/vfs/misc.c pm_fork())。如子进程将继承父进程的文件描述符等等。
6、复制 fproc 结构,复制 Root directory 和 Working Directory,返回后,发送一个 SIGSTOP 给子进程。
7、最终返回子进程的 pid。fork 结束。
l Opendir (lib/libc/posix/_opendir.c)
1、 利用stat函数通过文件名获得文件信息,并保存在st结构体中,如果执行不成功则返回
2、 利用S_ISDIR判断该文件是否目录文件,如果不是则报错为ENOTDIR:参数name非真正的目录
3、利用open函数并以只读方式或不可阻挡的方式打开文件,打开成功则返回文件描述符d,(由open 返回的文件描述符一定是该进程尚未使用的最小描述符)若打开错误则返回
4、 用fstat函数通过文件描述符d获得文件状态,判断各种错误
EACCESS 权限不足
EMFILE 已达到进程可同时打开的文件数上限。
ENFILE 已达到系统可同时打开的文件数上限。
ENOTDIR 参数name非真正的目录
ENOENT 参数name 指定的目录不存在,或是参数name为一空字符串。
ENOMEM 核心内存不足。
5、该函数成功运行,将返回一组目录流(一组目录字符串)
l Execl(lib/libc/posix/_execl.c)
1、利用函数va_start(ap, arg):使参数列表指针ap指向函数参数列表中的第一个可选参数,说明:arg是位于第一个可选参数之前的固定参数,(或者说,最后一个固定参数;…之前的一个参数),函数参数列表中参数在内存中的顺序与函数声明时的顺序是一致的。
2、从第二个参数开始,当参数不为空时,利用va_arg(arg_ptr, type):返回参数列表中指针arg_ptr所指的参数,返回类型为type,并使指针arg_ptr指向参数列表中下一个参数,i为参数总个数
3、va_end(ap):清空参数列表,并置参数指针ap无效。
4、利用alloca函数分配i个字符型指针大小的内存空间,但注意alloca是在栈(stack)上申请空间,用完马上就释放,若分配失败则返回错误代码ENOMEM
5、恢复ap,重复第二步
6、改变参数列表数组的第一个参数为__UNCONST(arg)
7、将ap参数列表中的可选参数复制到现有的参数数组中
8、清空参数列表,并置参数指针ap无效
9、利用execve()用来执行参数path字符串所代表的文件路径,第二个参数是利用第7步获得的参数数组指针来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数*_penviron则为传递给执行文件的新环境变量数组。
l Exit(lib/libc/poxic/__exit.c)
1、定义函数指针suicide和message结构体m
2、将进程结束状态赋值给m
3、系统调用exit,向pm发送信息
(/Servers/vm/exit.c) PM接收消息,清除计时器,对父进程计费,发送 SIGHUP 给进程租,告知等待它的进程,同时通知 VFS;VFS 接收到消息后,关闭它打开的文件描述符,清除 fproc 位回复 PM;PM 接收到 VFS 的回复通知 Kernel 完成 exit;
Kernel去除中断处理器,释放地址空间,重置timer,清除 proc 和 priv 结构,答复 PM 已完成; PM 接收到Kernel 的答复,通知 VM,令 VM 清除掉为它非配的内存,以及清除它的 vmproc 结构; VM 答复,PM清除 mproc 结构
4、如果进程结束失败,则进程自杀,防止死锁
5、如果进程自杀失败,则先挂起
————————————————