20169219linux 内核原理与分析第四周作业
系统调用
- 系统调用是用户空间访问内核的唯一手段;除异常和陷入外,它们是内核唯一的合法入口。
- 一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。
- 要访问系统调用,通常通过C库中定义的系统调用来进行。系统调用还会通过一个long类型的返回值来表示成功或者错误。负的返回值表示错误,返回0通常表明成功。
- Linux中每个系统调用都有一个系统调用号。通过这个独一无二大的号就可以关联系统调用。当用户空间的进程执行一个系统调用的时候,这个系统调用号就用来指明到底是要执行哪个系统调用;进程不会提及系统调用的名称。系统调用的列表存储在sys_call_table中。
- Linux系统调用比其他许多操作系统执行要快。Linux很短的上下文切换时间是一个重要原因,进出内核都被优化的简洁高效。另外一个原因是系统调用处理程序和每个系统调用本身也都非常简洁。
- 内核驻留在受保护的地址空间上,所以用户空间的程序不能直接调用内核空间中的函数。通常通过软中断,触发一个异常促使系统切换到内核执行系统调用处理程序。在x86上预定义的软中断中断号是128.
- 在x86上,系统调用号是通过eax寄存器传递给内核的。
- 内核在执行系统调用的时候处于进程上下文。current指针指向当前任务,即引发系统调用的那个进程。
在进程上下文中,内核可以休眠并且可以被抢占。能够休眠说明系统调用可以使用内核提供的绝大部分功能。在进程上下文中能够内抢占表明,像用户空间内的进程一样,当前的进程同样可以被其他进程抢占。因为新的进程可以使用相同的系统调用,所以必须小心,保证系统调用是可重入的。 - 建立一个新的系统调用有利有弊,需谨慎使用。
- 常用的系统调用函数有fork、wait、getpid等
进程管理
进程
- 进程就是处于执行期的程序。但不仅仅是代码,通常还包括其他资源,如打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程,还有用来存放全局变量的数据段等。
- 线程是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的对象不是进程而是线程。一个进程可以包含多个线程。但linux系统的线程实现并不特别区分进程和线程。
- 在linux系统中,通常通过调用fork()系统创建进程。该系统通过复制一个现有进程来创建一个全新的进程。调用fork()的进程为父进程,新产生的进程为子进程。通常新的进程都是为了立即执行新的、不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。最终程序通过exit()系统调用退出执行。
- exit这个系统调用是用来终止一个进程的。无论在程序中的什么位置,只要执行到exit系统调用,进程就会停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本进程的运行。
- 进程通过一个唯一的进程标识值或PID来标识每个进程。
- linux中可以通过ps命令查看所有进程的信息。
ps -aux
(a)显示其他用户启动的进程
(x)查看系统中属于自己的进程
(u)启动这个进程的用户和它启动的时间
进程状态
进程的五种状态及其转化关系如下图所示:
进程的创建
linux进程创建分两步:fork()和exec()
fork():通过拷贝当前进程创建一个子进程
exec():读取可执行文件并将其载入地址空间开始运行。
进程创建流程:
(1)调用调用dup_task_struct()为新进程分配内核栈,task_struct等,其中的内容与父进程完全相同。
(2)检查并确保新创建子进程后,当前用户所拥有的进程数目没有超出分配限制。
(3)清理新进程的信息(比如PID置0等),使之与父进程区别开。
(4)设置新进程状态为 TASK_UNINTERRUPTIBLE。
(5)更新task_struct的flags成员。
(6)调用alloc_pid()为新进程分配一个有效的PID。
(7)根据clone()的参数标志,拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
(8)做一些扫尾工作并返回指向子进程的指针。
- 在实际用fork()创建进程的过程中,对于子进程,fork()通常返回给它0。之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id 0总是由交换进程使用,所以一个子进程的进程id不可能为0 )。
进程终结
(1)设置task_struct中的标识成员设置为PF_EXITING
(2)调用del_timer_sync()删除内核定时器, 确保没有定时器在排队和运行
(3)do_exit()调用acct_update_integrals()输出记账信息。
(4)调用exit_mm()释放进程占用的mm_struct。
(5)调用sem__exit(),使进程离开等待IPC信号的队列。
(6) 调用exit_files()和exit_fs(),释放进程占用的文件描述符和文件系统资源。
(7)把task_struct的exit_code设置为进程的返回值。
(8)调用exit_notify()向父进程发送信号,并把自己的状态设为EXIT_ZOMBIE。
(9)切换到新进程继续执行。
孤儿进程与僵尸进程
在对应的父进程结束执行后,进程就会变成孤儿进程,但之后会立即由init进程“收养”为其子进程。
某一子进程终止执行后,若其父进程未提前调用wait,则内核会持续保留子进程的退出状态等信息,以使父进程可以wait获取。而因为在这种情况下,子进程虽已终止,但仍在消耗系统资源,所以其亦称僵尸进程。
网易云课堂学习
mykernel文件夹下有两个文件:mymain.c和myinterrupt.c
mymain.c
myinterrupt.c
运行解果如图
一个简单的时间片轮转多道程序内核代码,是一个精简内核。一个精简内核需要实现的基本功能有:创建进程、中断机制。
- 主函数mymain.c 可以创建一个进程,也可以创建多个进程。新创建的进程放在进程队尾。
- mypcb.c 每个进程的信息存储在进程控制块(PCB)中,进程控制块需要存储进程ID、进程控制块大小、进程状态、进程优先级。
- myinterrupt.c中断机制实行过程中,内核每经过固定的时间周期检查是否有中断发生,如果有中断发生,要保护现场;若有多个中断发生,需要判断中断优先级,在main.c中执行中断,然后恢复现场。一个中断执行完之后根据优先级执行下一个中断。
存在的问题
通过运行简单的程序,实践了简单的系统调用函数的功能,如wait(),fork(),getpid(),exit和_exit()的区别。
每个小程序中都有fork()给子程序的返回值是0,之前不理解为什么新创建的子程序pid为0,但是现在已经解决了这个问题。