linux内核的异常链表--只逮捕,不行刑

linux内核的行为方式和现实社会非常的相似,内核就好像一个管理机构,管理着所有的用户进程,所以当进程犯了错误的时候,内核绝对不能不管,而是当场缉拿,要么惩罚,要么判处死刑。这其中巧妙的地方就是这个从缉拿到判刑的过程,采用了类似三权分立的思想,各司其职。
如果一个进程在用户空间访问了一个不属于它的地址空间的一个地址,那么会被内核发现,其实就是发生了缺页中断,然后内核直接判了这个进程的死刑,发送越界信号,事实上就是杀死了这个进程(内核够残酷吧...)。曾几何时,每当用户需要和内核交换数据的时候,内核首先要检查用户传来数据的合法性,就是检查该数据是否属于该进程的地址空间,这个操作浪费了大量的时间,如果是人类社会,人们不会认为这浪费了时间,因为人类的行为效率总是十分低下,如果砍掉一些部 门,那么会带来大量的失业,会增加社会的不稳定因素,因此人类社会的很多机构都是冗余机构,和操作系统一样,人类社会最重要的也是稳定,不同的是,人类有自由意志,一些规则在某些人身上往往不好使,在操作系统中稀有现象,比如越界,在人类社会就会成为常见现象,比如偷盗,(实际上也可以理解为用户空间 越界)以及用假身份证去办银行卡(可以理解为往管理机构传递数据时的越界),如果砍掉相关的检查机构,后果将不堪设想。还是计算机最省心,让它干什么它就干什么。
为了节省开销,内核将数据的合法性检测推迟到不能再推为止,何时是不能再推的时候呢?当然是访问该数据的时候了,访问该数据的时候,如果它不是合法数据,当然就不可能有页表映射,因此会产生缺页中断,因此将合法性检测统统归到缺页处理当中就可以了,我们来看看代码的相关内容:

asmlinkage void do_page_fault(struct pt_regs *regs, unsigned long error_code)

{

...

bad_area_nosemaphore:

if (error_code & 4) { //如果是在用户空间越界,那么直接判死刑,类似于,偷盗

...

force_sig_info(SIGSEGV, &info, tsk);

return;

}

no_context: //如果是内核空间发生错误,情况就复杂了,不能直接判死刑了

if (fixup_exception(regs))

return;

...

}

至 于为何在内核空间越界不能直接杀死的原因,其实很简单。我们想象一下用户空间越界为何可以发送越界信号,那是因为缺页处理执行完后就会返回用户空间,而返回用户空间的途中就会执行越界信号处理,从而杀死进程,而信号处理在且只在返回用户空间的时候执行,如果内核在执行发生越界代码之后并不是返回用户空间, 那么信号就不会被处理,本来该被枪毙的进程如此缓期是内核和别的进程不能忍受的,而且这很容易陷入死循环,比如在内核空间执行以下代码的时候发生越界:
movsb
那 么如果在缺页处理中仅仅发送了一个信号,而缺页处理又不是返回用户空间,那么信号处理就不会被执行,当缺页处理返回的时候,movsb重新执行,又一次缺 页,反复不止,从而死循环。以上说明内核越界为何不能发送信号,那么为何不直接杀死,即调用do_exit呢,实际上这是可行的,继续读 do_page_fault的后面的部分你就会发现在任何handle都处理不了的时候,内核确实调用了do_exit让进程立即退出,但是谁也不敢保证在越界之前该进程进行了什么操作,比如是否获得了一把自旋锁,如果是的话,这里直接do_exit的话,那把锁就没得释放了,所有等待那把锁的都将陷入死锁状态,因此内核在这里还是不要多管闲事的好,这就是内核异常链表的作用了,关于异常链表的具体定义和编程技巧我就不说了,我不喜欢抄书和复制粘贴除了代 码和电影以外的东西,这里只谈谈思想。
内核异常链表提供异常代码自己处理异常的机会,然后返回状态供下一级代码处理,内核本身只在异常代码自己处理不了的情况下才加以干涉。比如在__put_user_asm中:
#define __put_user_asm(x, addr, err, itype, rtype, ltype, errret) /
__asm__ __volatile__( /
"1: mov"itype" %"rtype"1,%2/n" /
"2:/n" /
".section .fixup,/"ax/"/n" /
"3: movl %3,%0/n" /
" jmp 2b/n" /
".previous/n" /
".section __ex_table,/"a/"/n" /
" .align 4/n" /
" .long 1b,3b/n" /
".previous" /
: "=r"(err) /
: ltype (x), "m"(__m(addr)), "i"(errret), ""(err))
__put_user_asm 宏中的.section __ex_table便是异常处理表,1b是可能的异常点,3b是恢复点,如果1b执行的时候发生了异常,那么跳到3b执行,我们看看3b做了什么,杀死 进程了吗?如果3b仅仅就是杀死进程,那还不如在do_page_fault中杀死呢,实际上3b中没有杀死进程,仅仅设置了一个返回码为 -EFAULT,这个有什么用呢,这个实际上就是将出错进程给逮捕了,但是具体行刑却交给了另外的机构,交给谁呢?当然交给了调用它的代码了。上述的宏由 put_user调用,我们随便看一个调用put_user的地方:
asmlinkage ssize_t sys_sendfile(int out_fd, int in_fd, off_t __user *offset, size_t count)
{
loff_t pos;
off_t off;
ssize_t ret;
if (offset) {
if (unlikely(get_user(off, offset)))
return -EFAULT;
pos = off;
ret = do_sendfile(out_fd, in_fd, &pos, count, MAX_NON_LFS);
if (unlikely(put_user(pos, offset)))
return -EFAULT;
return ret;
}
return do_sendfile(out_fd, in_fd, NULL, count, 0);
}
当 put_user返回-EFAULT时,系统调用直接返回,就不再继续了,实际上就反回了用户空间,至于接下来怎么办就不是内核的事情了,大多数情况下,进程会被判死刑,至于是否真的判死刑就看法院的策略了,内核的异常链表知道的唯一一件事就是它犯错了,逮捕它,给他挂上一个罪名,然后内核异常链表的任务 就完成了,因为它并不知道更多的信息,因此它也不能随意处置异常进程,万一是总统的儿子呢?呵呵,万一是init进程或者内核本身呢?上述 sys_sendfile不太典型,典型的就是如果一个进程在调用put_user之前获得了一把锁,那么当出错返回前,它必须释放掉这把锁。
linux是高度模块化的,它不是杂糅的一推,一个模块就处理一件事,它不管也不问下面该怎么办,现实社会也该好好学学了

你可能感兴趣的:(linux)