- 论坛徽章:
- 0
|
一个非常好的文档,这份文档是作者高桥浩和在分析Linux内核2.4.0-test10时所著,最新版本为初稿4(draft4),成稿时间2000.12.9.由于描述的内核版本比较老,部分内容并不适合现在,但其根本并没有改变。这个文档虽然是分析内核,但主要说的linux内核的运行机制及用伪C代码描述的系统级别函数的实现方法,我觉得对理解UNIX中C函数的具体机理及其所代表的具体含义有很大的帮助作用,这个文档虽然不能帮助我们达到“知其所以然”的境界,但从另一个角度看不但能让我们在短时间内对Linux内核的具体实现有个大概的了解,而且对在Linux下编程的我们来说其意义更在于了解函数与函数间是如何相互作用。更安全,更准确,更高效地去使用它们。
我翻译这个文档出于学习目的,由于文档本身并没有版权声明,因此这个翻译版本也没有得到作者的任何授权,在此声明。另外,我也是边翻译边学习,由于自己知识极端不足,我无法保证(虽然尝试)翻译的正确性,加上由于我毫无任何文字功底可言,所使用的文法也可能造成你阅读困难,在此先道歉,最后还有一点就是,由于我从接触计算机知识时就不具备阅读中文书籍的条件,因此我对专业术语的中文对应词汇的知识仅来自于论坛,如果所用词汇不符和大众习惯,请忍耐,如果影响了文档的正确性,请在论坛留言,谢谢。
--------------------------------------------------------------------------------------------------
文档按顺序由“执行管理”,“文件系统”,“内存管理”,“网络”(相当精彩),“系统引导”,“多处理器管理”六个部分组成。我会先完成“执行管理”部分,之后是“内存管理”,之后是“网络”或“文件系统”...
目录
- 第一部分:
- 执行管理
- [color=blue]。进程管理
- 。进程模型
- 。构成进程的资源
- 。进程的状态迁移
- 。进程的一生
- 。fork
- 。exec
- 。exit
- 。进程的调度
- 。调度器
- 。进程的切换
- 。进程的同期
- 。抢占处理
- 。信号灯
- 。其他的与调度有关的函数的说明
- 。进程的父子关系
- 。进程ID
- 。函数说明
- 。信号
- 。函数说明
- 。信号的忽视及屏蔽
- 。SIGCHLD信号
- 。延缓信号
- 。与信号有关的数据结构及其他函数
- 。线程[/color]
- 。延迟处理
- 。软中断处理函数
- 。函数说明
- 。BH处理函数
- 。函数说明
- 。任务队列
- 。函数说明
- 。计时器
- 。时钟控制
- 。计时器列表
- 。函数说明
- 。其他与计时器相关的功能
- 。内核内的时限功能
- 。settimer系统调用
- 。中断控制
- 。中断控制函数
- 。中断控制函数的注册
- 。中断控制函数的起动
- 。问题点
- 。禁止中断
- 。CPU级别的中断控制
- 。补充
- 。中断控制器级别的中断控制
- 。内核服务的入口
- 。系统调用的入口
- 。中断入口
- 。页访问失败
- 。一般异常
- 。文件系统
- 。
- 。
- 。
复制代码
进程模型
- linux中的所有的进程都由task_struct构造体管理。在生成进程的时候将会分配一个task_struct构造体,之后将通过这个构造体对进程进行管理。
- 与传统的UNIX不同,linux并不存在user构造体,进程的管理情报全部存放于task_struct构造体中。task_struct构造体存在于平坦地址空间内,任何时候Linux内核都可以参照所有进程的所有管理情报。内核堆栈也同样位于平坦地址空间内。
- (传统的UNIX将不被别的进程参照的数据及内核堆栈放在各个进程独立的U构造体内。)
复制代码
构成进程的资源
- 以下为今后说明时使用的task_struct构造体的主要成员。
- 主要成员:
- struct task_struct {
- struct files_struct* files; //文件描述符
- struct signal_struct* sig; //信号控制signal handler
- struct mm_struct* mm; //内存管理模块
- long stat //进程状态
- struct list_head runlist; //用于联结RUN队列
- long priority; //基本优先权
- long counter; //变动优先权
- char comm[]; //命令名
- struct thread_struct tss; //上下文保存领域
- };
- 当使用fork生成新的进程的时候,将复制所有的资源。但是内存领域使用Copy-On-Write,只到进行写操作时才进行复制处理。对文件则只负责复制文件描述符以共用file结构体。依赖于这种策略,由shell起动的命令可以什么都不考虑而使用标准输入输出及利用管道和重定向。
复制代码
进程的状态迁移
- 进程的状态为以下中的一种。CPU上可执行的进程状态为TASK_RUNNING。CPU将分配给多个TASK_RUNNING状态进程中优先权最高的。
- TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE都是为等待某个条件成立而中断的状态(发送硬盘I/O要求而等待I/O完成的状态,等待TTY终端的输入的状态等)。等待状态分为两种是为了控制在等待期间接受到信号时是否解除等待状态。以TASK_INTERRUPTIBLE状态经入等待的情况下,可被强制唤醒。(等待硬盘I/O时,为了操作完成为止延迟操作信号,使用TASK_UNINTERRUPTIBLE,而等待无法保证唤醒时间的TTY终端I/O的时候,使用TASK_INTERRUPTIBLE)。
- TASK_STOPPED状态表示接受到延缓信号(SIGSTOP,SINGTTIN等)而进入中断状态。这种状态的进程将不会成为进程切换的对象。在接受到恢复信号(SIGCONT)后返回TASK_RUNNING状态并成为切换对象。
- TASK_ZOMBIE状态为exit后到消亡为止之间的进程状态。进程在结束后到父进程执行wait之间,成为TASK_ZOMBIE状态并持续存在于系统中。
- 最终,linux没有实现进程全体的swap,曾在头文件中定义的TASK_SWAPPING状态在版本2.4中被删除了。
-
-
- 状态 说明
- TASK_RUNNING 执行可能状态
- TASK_INTERRUPTIBLE 等待状态。可接受信号
- TASK_UNINTERRUPTIBLE 等待状态。不能接受信号
- TASK_ZOMBIE 僵尸状态。exit后的状态
- TASK_STOPPED 延缓状态
- Linux中定义的状态是直线形的,不存在类似既是等待又是延缓的状态。
- Linux中,对等待状态中的进程发送SIGSTOP等信号的话将记录对其有延缓请求,当进程转为TASK_RUNNING时,根据记录中的请求将其转为TASK_STOPPED。
复制代码
进程的一生
fork
-
- 之后将讲述的线程也完全一样使用do_fork函数生成。只是在于呼叫这个函数时传递的参数不同。
- do_fork(flag,进程上下文)
- 分配一个空的task_struct(alloc_task_struct函数)
- 分配一个进程ID(get_pid函数)
- 初始化task_struct构造体的各个成员
- 复制文件描述符表(copy_files函数)
- 当前目录,umask等的复制(copy_fs函数)
- 复制信号情报(copy_sighand函数)
- 父进程上下文的复制(copy_thread函数)
- 使用Copy-On-Write复制虚拟内存空间(copy_mm函数)
- 将生成的子进程联结入RUNQ(wake_up_process函数)
复制代码
exec
- 进程可使用exec系统调用执行新的命令。exec系统调用将一次性释放全部虚拟内存空间,之后生成新的空间并将新的命令影射入内。
- do_execve(文件路径,参数。环境)
- 打开文件(open_namei函数)
- 计算exec后的UID/GID,读入文件头(prepare_binprm函数)
- 读入命令名,环境变量,起动参数(copy_strings函数)
- 呼叫各种不同二进制文件的操作函数(search_binary_handler函数)
- ELF格式的话,经由search_binary_handler函数呼叫load_elf_binary函数。如果是动态联结,同时影射动态联结器(ld*.so)
- load_elf_binary(linux_binprm* bprm,pt_regs* regs)
- 分析ELF文件头
- 读入程序的头部分(kernel_read函数)
- if(存在解释器头部){
- 读入解释器名(ld*.so)(kernel_read函数) |(zalem note:可用
- 打开解释器文件(open_exec函数) | objdump -s -j .interp xxx
- 读入解释器文件的头部(kernel_read函数) |命令查看,
- } |linux下是/lib/ld-linux.so.x)
- 释放空间,清楚信号,关闭指定了close-on-exec标识的文件(flush_old_exec函数)
- 生成堆栈空间,塞入环境变量/参数部分(setup_arg_pages函数)
- for(可引导的所有的程序头){
- 将文件影射入内存空间(elf_map,do_mmap 函数)
- }
- if(为动态联结){
- 影射动态联结器(load_elf_interp函数)
- }
- 释放文件(sys_close函数)
- 确定执行中的UID,GID(compute_creds函数)
- 生成bss领域(set_brk函数)
- bss领域清零(padzero函数)
- 设定从exec返回时的IP,SP(start_thread函数)(动态联结时的IP指向解释器的入口)
复制代码
exit
- 进程的结束由do_exit函数进行。除显式呼叫exit系统函数外,接受到信号而终止的情况下也被调用。do_exit函数释放除task_struct构造体外的所有资源。do_exit使用exit_notify函数向父进程发送SIGCHLD信号。接受到SIGCHLD函数的父进程找出成为ZOMBIE状态的子进程,释放其task_struct.
- do_exit(终止号)
- {
- 停止此进程的计时器(del_timer_sync函数)
- 释放IPC信号灯(sem_exit函数)
- 释放虚拟空间(__exit_mm函数)
- 关闭文件及释放管理空间(__exit_files函数)
- 释放当前目录,umask情报(__exit_fs函数)
- 抛弃信号及管理领域的释放(__exit_sighand函数)
- 将进程状态设为TASK_ZOMBIE
- 通知父进程(exit_notify函数)
- 放弃CPU(schedule函数)
- }
- 父进程使用下面的release函数释放成为了ZOMBIE的task_struct构造体。
- release(ZOMBIE的子进程)
- 释放PID(unhash_process函数)
- 释放task_struct(free_task_struct函数)
复制代码
进程调度
调度器
- 调度器对进程和线程同等对待。执行可能状态的进程(线程)连接入下图的RUN队列。调度器在连入RUN队列中选出优先度最高的进程并将CPU(执行权)交给它。叫做current的指针指向现在执行中的进程。
- 如果没有任何可执行的进程,调度器将执行权交给叫做idle的什么也不作的进程。
- schedule()
- 执行任务队列 – tq_scheduler(run_task_queue函数) <后述>;
- 呼叫BH handler(do_softirq函数) <后述>;
- 拒绝抢占请求 (zalem note: cli)
- if(调度器呼叫出的进程状态并非TASK_RUNNING){
- 将进程从RUN队列中清除(del_from_runqueue函数)
- }
- while(对接入RUN队列的所有进程){
- 搜索优先度最高的进程(goodness函数)
- }
- while(对系统中所有的进程){
- 重新计算优先度
- }
- 切换进程上下文(switch_to函数)
- 补充:
- Linux的调度器十分的简单。不但RUN队列只有一个而且每次都进行线性查找,因此在巨大系统中可能有些不高效。
复制代码
进程切换
- 将现在运行中的进程的上下文(CPU状态-即寄存器情报)保存,将下一个要运行的进程的上下文读入CPU的工作,称为进程切换。运行再开的时候,将先前保存于内存中的上下文传入CPU的话。就可以再次从中断处开始运行。
- Linux中,switch_to担负此项工作。上下文的储存领域则使用进程的内核堆栈及struct_task中的领域(tss部分 – 关于tss的名称请参照Inter CPU的手册 zalem note:Task State Segment)
复制代码
进程的同期
- 运行中的进程经入等待状态的话,将自身连入为每个等待对象(zalem note:指硬盘I/O操作,串口输入输出等,称为等待对象)准备的wait队列的头部并放弃CPU。为此(自己将自己从RUN队列中脱离,并呼叫调度器)准备了sleep_on函数,interruptible_sleep_on函数。这两个函数的区别在于成为WAIT状态时进程能否被信号唤醒。
- sleep_on(WAIT队列头)(或者是interruptible_sleep_on(WAIT队列头))
- 在堆栈上准备wait_queue
- 将当前进程的状态转为TASK_UNINTERRUPTIBLE
- (interruptible_sleep_on的话是TASK_INTERRUPTIBLE)
- 在wait_queue中注册当前进程
- 将wait_queue连入WAIT队列头部(add_wait_queue函数)
- 呼叫调度器以放弃CPU(schedule函数)
- 将wait_queue脱离WAIT队列(remove_wait_queue函数)
- 这个进程将会被事件的发生所唤醒。(wake_up函数,wake_up_interruptible函数等),进程被唤醒时将被连入RUN队列,但此时仍存在于wait队列中。当这个进程再度获得执行权时,首先要作的就是将自己从wait队列中清除。
- 这种方式的好处在于,由于中断控制函数(handler)的延长逾期而导致被唤醒的时候,并不用进行wait队列的操作,并可简化互斥处理。
- __wake_up(WAIT队列头,mode)
- while(对在WAIT队列中等待的所有的进程){
- 如果进程属性同mode指定的一致(TASK_INTERRUPTIBLE等)
- 则唤醒(__wake_up_process函数)。
- 如果指定为只唤醒一个(TASK_EXCLUSIVE),则break;
- }
- __wake_up_process(进程)
- 将进程状态改为TASK_RUNNING
- if(这个进程还没有连入RUN队列)
- 连入RUN队列(add_to_runqueue函数)
- 要求重新调度(reschedule_idle函数)
- }
- 到版本2.2为止,wake_up函数将WAIT队列中所有成为对象的进程都设为RUN状态,出于性能改善的目的,从版本2.4开始可以只将WAIT队列中位于头部的进程设为RUN状态。这用于后面将提到的信号灯及负荷较集中的地方。如果进程属性中加入TASK_EXCLUSIVE,则仅唤醒先头的进程。
- __wake_up()函数被外包以方便使用及与版本2.2的兼容
- 。wake_up(WAIT队列头)
- 。唤醒TASK_UNINTERRUPTIBLE,TASK_INTERRUPTIBLE两个属性的进程。但仅唤醒位于先头的一个(TASK_EXCLUSIVE)。(唤醒了具有TASK_EXCLUSIVE属性的进程后,将不会唤醒其后的进程)
- 。wake_up_all(WAIT队列头)
- 。唤醒所有TASK_UNINTERRUPTIBLE,TASK_INTERRUPTIBLE的等待进程(忽略TASK_EXCLUSIVE属性)
- 。wake_up_interruptible(WAIT队列头)
- 。唤醒TASK_INTERRUPTIBLE属性的进程。但仅唤醒位于先头的一个(TASK_EXCLUSIVE)。(唤醒了具有TASK_EXCLUSIVE属性的进程后,将不会唤醒其后的进程)
- 。wake_up_interruptible_all(WAIT队列头)
- 。 唤醒所有TASK_INTERRUPTIBLE的等待进程
- 补充说明1:
- 虽然一般情况下是调用sleep_on函数,或interruptible_sleep_on函数经入等待,但在linux内核中很多地方是将sleep_on函数展开,自己操作wait队列,自己改变进程状态及呼叫调度器。
- 补充说明2:
- 传统的UNIX在sleep中由信号强制唤醒的话,默认状态为直接长跳转(zalem note:longjmp()|siglongjmp())到系统调用的出口并返回EINTR,而Linux下只是普通的唤醒,不进行长跳转。
复制代码
抢占处理
- 当进程被wake_up_process等函数改变为执行可能的时候,被连接入RUN队列。但是如果只是仅仅连入RUN队列的话,这个无论这个进程的优先度多高都不会获得CPU。
- 当这个进程的优先权大于当前运行中的进程的话,必须对调度器显式提出获取CPU的要求(抢占请求)(reschedule_idle函数)(如果优先度相差很小的话则例外)。抢占请求是靠将当前运行的进程的task_strcut的need_resched成员作记号来实现的。(在支持多处理器前,好像是系统的全局变量。虽然在多处理器化后改为进程单位了,但其实应该以CPU为单位)。
- 接受到抢占请求的调度器在Linux内核执行到某个段落之后进行调度(schedule函数)。再调度的执行在下几个点。
- 。系统调用完成时
- 。中断处理完成时
- 。idle时
- 另外,这意味着linux的内核内运行时不可被抢占(这也意味着当Linux用于实时系统时无法获得好的反应性。即使是实时性的进程也无法在内核执行时获得CPU)(zalem note:2.4!=2.6)。这利于简化Linux内核内的资源互斥(Mutual Exclusion)操作。
复制代码
信号灯
- 为获得/等待资源,Linux提供了信号灯操作函数。在需要等待的类型的资源内定义semaphore型成员,对这个成员进行down函数,up函数操作来完成资源获得和资源解放工作。用down函数获得资源失败的话,到(别的进程用)up函数解放资源为止,处于WAIT状态。
- 到版本2.2为止,up并不进行复杂处理而是唤起所有的进程,被唤醒的进程依靠先到先得的规则取得(down)信号灯,从版本2.4开始,则改为只唤起位于先头的一个进程。
- 下面显示down函数和up函数的大概的流程(事实上为了对应SMP包含了很多复杂的汇编代码)。
- 作为Linux的功能,系统还提供了进程能被信号唤醒的函数(down_interrutptible)和如果信号灯在请求时能获得则获得的(down_trylock)函数。
- down(信号灯)
- if(信号灯的值还有剩余)
- 信号灯的值减一,return OK;
- 在堆栈上准备wait_queue
- 当前进程状态改为TASK_UNINTERRUPTIBLE|TASK_EXCLUSIVE
- 在wait_queue中注册当前进程
- 将信号灯连接入wait_queue(add_wait_queue_exclusive函数)
- while(1){
- if(信号灯的值为0){
- 呼叫调度器放弃CPU(schedule函数)
- 进程状态改为TASK_UNINTERRUPTIBLE|TASK_EXCLUSIVE
- }else{
- 信号灯的值减一
- 将进程状态改为TASK_RUNNING
- 将信号灯从wait_queue分离(remove_wait_queue函数)
- return
- }
- }
- up(信号灯)
- 信号灯的值加一
- 将等待这个信号灯的进程中处于先头的进程唤醒(wake_up)
- 补充说明
- XXX:在性能,效率方面都很好,但进程优先度方面还有问题。本来,唤醒的应该是等待这个信号灯的进程中优先度最高的那个。
复制代码
其他与进程调度相关的函数的说明
- 。reschedule_idle()
- 。指定的进程的优先度高于当前运行进程的话,向调度器提交抢占请求
- 。goodness()
- 。取得指定进程的优先度
- 。add_to_runqueue()
- 。将进程放入RUN队列先头
- 。del_from_runqueue()
- 。将进程从RUN队列中清除
- 。move_last_runqueue(),move_first_runqueue()
- 。将进程移到RUN队列的最后或最前
- 。add_wait_queue(WAIT队列头)
- 。将进程连入WAIT队列的先头
- 。add_wait_queue_exclusive(WAIT队列头)
- 。将进程放入WAIT队列的最后
- 。remove_wait_queue()
- 。将进程从WAIT队列中清除
- 。wake_up_process_synchronous()
- 。基本等同于wake_up_process(),但不发出抢占请求
- 。wake_up_sync(),wake_up_interruptible_sync()
- 。和wake_up(),wake_up_interruptible()相同,但不发出抢占请求。用于管道的处理等处理完成之前不发送抢占请求从而对性能有利的地方。
复制代码
进程的父子关系
- 各个进程之间有父子关系。这是为了实现父进程等待(wait系统调用)子进程的终止。
- 使用fork系统调用生成子进程时,进程的task_struct间持有如下图所示的连接构造。如果父进程先终止的话,子进程们的父进程被换为init进程。唯有init进程没有父进程。
复制代码
进程ID
- 作为标识,各个进程都有进程ID。进程ID为进程生成时(fork)时由Linux内核分配的唯一值。另外,除进程ID外,进程还持有进程组ID,会话ID。
- 这些ID一般按下图中的方法使用。这个样子进行进程组操作的是Shell。先以Shell为头打开会话,在会话中为每个工作(job)(由管道连接的命令群)制作一个进程组。这部分的操作根据Shell种类的不同而不同。
- 另外,为了防止与其他进程相互干涉,各种守护进程一般拥有自己的会话。
- 进程组ID,会话ID并不是自由分配的,必须遵守以下规则:
- 。会话首领兼任进程组首领。(会话首领无法改变进程组)
- 。会话中持有多个进程组。进程可以生成新的进程组(将自己的PID作为组ID)。进程可移动到存在于会话中的任何一个进程组。
- 老的BSD系统中没有会话的概念从而有了一个很小的安全漏洞。在导入工作控制功能前,进程组就相当于现在UNIX的会话。
复制代码
函数说明
- 。sys_setpgid()
- 。变更进程的进程组ID。可指定的ID要么为这个进程的进程ID,或为存在于这个进程所属的会话内进程组ID。
- 。sys_getpgid(),sys_getpgrp()
- 。取得进程的进程组ID
- 。sys_setsid()
- 。进程打开新的会话。会话ID,进程组ID为调用函数的进程的进程ID。
- 。sys_getsid()
- 。取得进程的会话ID。
复制代码
信号
- 作为向进程发送非同期事件通知的方法,Linux提供了叫做信号(Signal)的手段。
- 当进程发生了异常时,Linux内核将其转换为信号通知进程。另外,为了通知内核内所发生的事件,Linux内核也会生成信号。(pipe,tty等经常用到)。还有,进程可使用kill系统调用显式的生成信号。
- 并不是说当向一个进程发送信号后(send_sig函数),立刻将对象进程清除或起动对象进程注册了的信号处理函数。发送信号的一方只是保证通知,(kill系统调用完成并不表示这个时候对应的进程已经终止。常常可以看到这种理解错误的代码。)处理信号的部分全部在对象进程的上下文中完成(do_signal函数)。
- 信号的生成既可能是由异常(存取内存0地址等),pipe/tty状态变更等由Linux内核自动生成的,也可由用户进程发行kill系统调用显式生成。对象进程处于等待状态的时候也可能被强制唤醒(wake_up_process)。
- 进程是否接收到信号将在系统调用出口/异常控制函数出口/中断控制函数出口处被检测(与软中断的检测(zalem note:int 0x80)基本相同)。如果收到了信号,则开始处理(do_signal)。信号处理函数(do_signal)中,根据条件的不同,终止进程或为执行用户的信号控制函数作准备。关于用户信号控制函数的执行的实现方式,请参照下图及函数说明。无论在什么情形下产生信号(下图中的signal A B),操作信号的过程都是一样的 。
复制代码
函数说明
- 下面将根据处理的流程来说明承担主要任务的函数群。
- 。sys_signal(sig,handler),do_sigaction(sig,action..)
- 。注册信号种类为sig的信号操作函数handler。注册处为进程固有的信号处理函数表(struct signal_struct),各个的进程的进程表(struct task_strcut)中对其进行连接。
- 。do_sigaction函数可以对信号控制函数作更细节性的指定。
- 。send_sig(sig,p)
- 。向p进程发送sig信号
- 。当信号控制函数指定为忽视(SIG_IGN)时,什么都不作。但,当信号为SIGCHLD时,即使指 定为忽视(SIG_IGN)也发送。
- 。如果信号没有被屏蔽(mask),设对象进程为信号保留状态(task_struct中的sigpendig 标识设为on)。当信号发送的对象进程处于等待状态并且能接收中断的话(TASK_INTERRUPTIBLE), 将被强制性唤醒(wake_up_process函数)。
- 。do_signal()
- 。处理被送往对象进程的信号。因信号种类的不同而不同。
- 。如果指定为默认(SIG_DEL)处理,一般的信号将终止进程(do_exit函数)。在处理有的信号时,在进行进程终止处理前先生成core文件(do_coredump函数)。
- 但如果信号为延缓类型则是将进程转为TASK_STOPPED状态。
- 。信号为SIGCHLD并且指定为忽视(SIG_IGN)的话,进行成为僵尸的子进程的释放处理(sys_wait函数)。
- 。如果对象进程注册了信号处理函数,则为进程调用信号处理函数作准备工作。(handle_signal函数)
- 。handle_signal()...(参照上图)
- 。在进程空间中准备一个执行信号控制函数用的堆栈并对其进行初始化(setup_frame)。虽然执行信号的专用领域也可用于构造此堆栈(如果登录了的话),但一般情况下在进程的用户堆栈中分配领域。
- 。复制内核堆栈中的此进程的各种上下文到信号处理函数的执行用堆栈中。(在执行指定为再执行(zalem note:sigaction->;SA_RESTART)的系统调用过程中再次收到信号的话,则根据系统调用命令(INT命令)的大小(size)修改(rewind)返回地址。
- 。制造内核堆栈中的进程上下文的各种情报。返回地址(eip (zalem note:PC))改为用户定义的信号处理函数,用户堆栈(esp)修改为前面分配的信号处理栈的值。 这样,当这个进程(从内核级)返回到用户模式时将从信号处理函数处开始执行。(zalem note:esp指向栈顶,因此当执行push %ebp;movl %esp %ebp;后,就算建立了handler自己的stack frame,用于存取参数,返回及display(语言的上级函数变量参照,gdb->;backtrace等使用)。)
- 。为了当用户定义的信号控制函数执行完毕时,自动呼叫sigreturn系统调用,修改此信号堆栈。在栈底处复制入呼叫sigreturn系统调用的指令(有些野蛮)并将地址装入信号堆栈的顶部。这样,当从用户定义的信号控制函数返回时将自动执行存放了sigreturn系统调用的指令。
- 。改变进程的信号屏蔽状态。一般情况下,在执行信号控制函数时将不接受同种信号。(根据注册信号控制函数时指定的不同而不同)(zalem note:sigaction->;SA_NODEFER)
- 。sys_sigreturn()...(参照上图)
- 。当用户的信号控制函数结束时将自动呼叫sigreturn系统调用。
- 。将保存于信号堆栈中的进程上下文恢复至内核堆栈的上下文中。这样当进程从Linux内核(sigreturn系统调用)返回时,可以从接受到信号前的状态开始执行。
- 。信号控制函数可以存取堆栈中的上下文领域(接收到信号时的寄存器群的拷贝)。
复制代码
信号的忽视及屏蔽(阻塞 block)
- 在需要忽视信号的场合,可以在signal系统调用中显式地指定。send_sig函数在收到信号后将检测是否指定为忽视,如果是则抛弃这个信号。
-
- 信号的屏蔽由sys_sigprocmask控制。被屏蔽的信号记录于task_struct的blocked成员中。send_sig函数会将信号传送给对象进程并记录于task_struct的signal成员中,但在对象进程屏蔽此信号的过程中将不会呼叫处理接收信号的do_signal函数。
复制代码
SIGCHLD信号
- 进程终止时SIGCHLD信号会被送往其父进程。对SIGCHLD的处理与其他信号不同。
- 接收到SIGCHLD的进程的接收信号处理函数(do_signal函数)作以下行为。
- 。默认(SIG_DFL)时,在发送端(send_sig函数)就将不作任何处理直接抛弃此信号。
- 。指定为忽视(SIG_IGN)时,释放成为僵尸状态的子进程的资源(sys_wait4函数)。由于处理全部在Linux内核内部完成,因此对用户进程来说是透明的。
- 。如果注册了控制函数,则和普通的信号同样对待。
- 如果没有等待(wait系统调用)子进程结束的必要的话,只要指定SIGCHLD为忽视(SIG_IGN),则系统中不会增加无意义的僵尸进程。
复制代码
延缓(暂停,suspend)信号
- 延缓信号用于暂时将某个进程设为中断状态(TASK_STOPPED)。这类信号有以下4种。tty驱动程序生成的SIGSTP,SIGTTIN,SIGTTOU3种和kill系统调用显式生成的SIGSTOP信号。
- SIGCONT用于再开处于中断状态(TASK_STOPPED)进程。
- 它们各自的行为如下:
- 。SIGSTP,SIGTTIN,SIGTTOU,SIGSTOP信号
- 。指定为忽视(SIG_IGN)的时候则什么都不作(send_sig函数)
- 。接收到信号的进程被信号接收处理函数(do_signal函数)设为TASK_STOPPED状态并中断执行。(调用schedule函数放弃CPU)
- 。SIGCONT信号
- 。由信号送信函数(send_sig函数)将TASK_STOPED进程唤起(使之成为TASK_RUNNING)
- 。接收到SIGCONT的进程从被信号接收处理函数(do_signal函数)中断的地方开始执行。如果注册了对应SIGSTP,SIGTTIN,SIGTTOU,SIGSTOP的信号控制函数的话,则起动之(do_signal函数)。vi在延缓后再次恢复时,画面的重绘即是利用了这个。
复制代码
与信号相关的数据结构及其他函数[color]
- struct task_struct{
- .
- .
- int sigpending; //保留中的需处理信号
- sigset_t signal; //接收到的信号
- sigset_t blocked; //被屏蔽(阻塞)的信号
- signal_struct *sig;//指向信号处理函数注册表的指针
- .
- .
- }
- 。sigaddset(),sigdelset()
- 。操作信号接收的标识(flag)的ON/OFF
- 。sigaddsetmask(),sigdelsetmask()
- 。操作信号屏蔽的标识的ON/OFF
- 。sigismember()
- 。检测对应于指定信号的标识位是否为ON
- 。kill_someting_info(),kill_proc(),kill_pg()
- 。向指定的进程,指定的进程组中的进程发送信号。
- 。也准备了向属于会话的所有进程组发送信号的kill_sl()。
- 。sys_sigprocmask()
- 。变更信号的屏蔽(mask)。(利用sigaddsetmask函数,sigdelsetmask函数)
- 补充: 最初,UNIX信号的机制,从kill系统调用的名字中也可知道是被设计为强制杀死一个进程这种程度的。
- 之后,信号机制不断被扩充而具有了各种各样的功能。但是,虽说站在用户角度看来很便利,其不联贯的机能扩张导致内核的信号控制的实现部分十分混乱。
- 最近扩张的功能为,当多个信号连续发生时,将按其产生顺序呼叫信号处理函数。
复制代码
线程
- 在linux中作为执行单位的进程和线程是以完全相同的方式实现的。(以扩张进程来实现线程)
- 线程与进程不同的只是,它们共用进程所持有的各种内核内的资源。下面显示了由fork生成新进程时的数据结构和由clone生成新的线程是的数据结构。
- 当fork生成新的进程的时候,将会同时进行所有资源的复制。但是由于将内存设定为Copy-On-Write,到下次对资源进行写操作时为止并不执行复制处理。对文件则仅复制文件描述符以共用file构造体。依赖于这种策略,由shell起动的命令可以什么都不考虑而使用标准输入输出及利用管道和重定向。
- 而另一方面,由clone生成线程是,如下图所示那样,完全不进行资源的复制。两个线程的上下文完全参照同样的资源。不过除此点之外则于进程完全相同。
- 同样,对调度器来说两者也完全没有区别。即使是共有同一个内存空间的线程(同一个进程中的线程),在多处理器环境下也可以同时执行于不同的处理器。
复制代码
---------------------待续---------------------- |
|