您的位置:首页 > 运维架构 > Linux

linux0.12之内核代码signal.c说明

2015-07-08 15:35 579 查看
首先理论理解一下

信号是一种“软件中断”处理机制。信号机制提供了处理异步事件的方法。

信号也叫软中断。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。

内核代码中使用无符号长整型(32bit)编码信号,被定义在include/signal.h中

typedef unsigned int sigset_t;   /* 32 bits */
#define _NSIG             32
#define NSIG     _NSIG
#define SIGHUP   1
#define SIGINT   2
#define SIGQUIT  3
#define SIGILL   4
#define SIGTRAP  5
#define SIGABRT  6
#define SIGIOT   6
#define SIGUNUSED    7
#define SIGFPE   8
#define SIGKILL  9
#define SIGUSR1  10
#define SIGSEGV  11
#define SIGUSR2  12
#define SIGPIPE  13
#define SIGALRM  14
#define SIGTERM  15
#define SIGSTKFLT   16
#define SIGCHLD  17
#define SIGCONT  18
#define SIGSTOP  19
#define SIGTSTP  20
#define SIGTTIN  21
#define SIGTTOU  22


do_signal函数是由内核系统 调用(int 0x80)中断处理程序中对信号的预处理程序

如下有个信号处理程序的调用方式



我们知道从用户态到内核态,cpu会压入上下文,如下



如果正常返回,是继续执行cs:Eip地址的代码,而do_signal的作用就是修改代码执行



do_signal根据消息类型,将函数指针指向相应的信息处理函数



当系统调用或者中断返回的时候会调用do_signal函数。这个函数的主要作用是将信号处理句柄插入到用户程序堆栈中,并在本系统调用结束返回手立刻进入执行信号句柄程序,然后继续执行用户的程序。

看几个典型的函数

1、do_signal函数

int do_signal(long signr,long eax,long ebx, long ecx, long edx, long orig_eax,
long fs, long es, long ds,
long eip, long cs, long eflags,
unsigned long * esp, long ss)


传递了很多寄存器参数

此函数是在system_call.s(系统调用)中调用的

call _do_signal

第一条代码

struct sigaction * sa = current->sigaction + signr - 1;

首先sigaction是current结构体中一个元素

struct task_struct {
/* these are hardcoded - don't touch */
long state;    /* -1 unrunnable, 0 runnable, >0 stopped */
long counter;
long priority;
long signal;
struct sigaction sigaction[32];
.......................
}


current->sigaction是数组的第一个地址,所以

左值等效于

current->sigaction [signr - 1]

而这个数组又是一个结构体类型的

struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};


下面看if语句

if ((orig_eax != -1) && ((eax == -ERESTARTSYS) || (eax == -ERESTARTNOINTR)))

看看orig_eax,因为本do_signal函数是从system_call.s中调用的,

回到系统调用函数,看看是怎么压栈的

long orig_eax,long fs, long es, long ds,这是参数传递的顺序,压栈顺序相反

第一个:从system_call入口

_system_call:
push %ds
push %es
push %fs
pushl %eax  # save the orig_eax


第二个:从时间中断入口

_timer_interrupt:
push %ds    # save ds,es and put kernel data space
push %es    # into them. %fs is used by _system_call
push %fs
pushl $-1  # fill in -1 for orig_eax


第三个:从检测设备入口

_device_not_available:
push %ds
push %es
push %fs
pushl $-1  # fill in -1 for orig_eax


可以看出如果不是系统调用而是其他中断执行过程中断调用到本函数,riog_eax为-1.

sa_handler = (unsigned long) sa->sa_handler;
if (sa_handler==1)
return(1);   /* Ignore, see if there are more signals... */
if (!sa_handler)


其中sahandler是信号处理句柄

在signal.h中

#define SIG_DFL  ((void (*)(int))0) /* default signal handling */
#define SIG_IGN  ((void (*)(int))1) /* ignore signal */
#define SIG_ERR  ((void (*)(int))-1)    /* error return from signal */


这种宏第一定义用0、1表示函数指针类型,当为0时默认处理信号

if (!sa_handler) {
switch (signr) {
case SIGCONT:
case SIGCHLD:
return(1);  /* Ignore, ... */
case SIGSTOP:
case SIGTSTP:
case SIGTTIN:
case SIGTTOU:
current->state = TASK_STOPPED;
current->exit_code = signr;
if (!(current->p_pptr->sigaction[SIGCHLD-1].sa_flags &
SA_NOCLDSTOP))
current->p_pptr->signal |= (1<<(SIGCHLD-1));
return(1);  /* Reschedule another event */
case SIGQUIT:
case SIGILL:
case SIGTRAP:
case SIGIOT:
case SIGFPE:
case SIGSEGV:


然后根据信号类型完成不同处理,包括修改进程的运行状态,再具体,还不是很了解。

下面看看invoking a handler

*(&eip) = sa_handler;

longs = (sa->sa_flags & SA_NOMASK)?7:8;

*(&esp) -= longs;

将sa_handler地址放入eip,并将esp向下扩展7(8)个长字,

verify_area(esp,longs*4);

其中verify_area函数,是内存验证函数

oid verify_area(void * addr,int size)
{
unsigned long start;
start = (unsigned long) addr;
size += start & 0xfff;
start &= 0xfffff000;
start += get_base(current->ldt[2]);
while (size>0) {
size -= 4096;
write_verify(start);
start += 4096;
}
}


这个函数是写操作前检查,(这里会涉及到copy_on_write的概念),

start = (unsigned long) addr;

获得内存地址

start & 0xfff; 4K偏移量(页面中的偏移量),

size += start & 0xfff;

这样就得到以页面开始的大小

示意图如下



start &= 0xfffff000;获得页对齐的地址

start += get_base(current->ldt[2]);

谷歌讲这是把start加上进程数据段在线性地址空间中的起始位置,变成系统整个线性空间中的地址位置。我不是太理解。

while (size>0) {

size -= 4096;

write_verify(start);

start += 4096;

}

函数write_verify是写页面验证,

void write_verify(unsigned long address)

{

unsigned long page;

if (!( (page = ((unsigned long ) ((address>>20) & 0xffc)) )&1))

return;

page &= 0xfffff000;

page += ((address>>10) & 0xffc);

if ((3 & (unsigned long ) page) == 1) /* non-writeable, present */

un_wp_page((unsigned long *) page);

return;

}

参数addres是线性地址,

if (!( (page = ((unsigned long ) ((address>>20) & 0xffc)) )&1))

return;

page得到的是页目录表项的地址中的数据

然后检查P位,是否置一,为0说明没有使用,对于没有使用的页面而言,没有共享和写时复用的概念。

page &= 0xfffff000;

获得页表地址基地址;

((address>>10) & 0xffc)从线性地址中获得页表项偏移地址

page += ((address>>10) & 0xffc);

获得页表项地址

if ((3 & (unsigned long ) page) == 1)

检测页表项内容,11b模式,即检测位1(R/W=1)、位0(P),如果不可写,并且存在,就要进行写时复制,

void un_wp_page(unsigned long * table_entry)

{

unsigned long old_page,new_page;

old_page = 0xfffff000 & *table_entry;

if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {

*table_entry |= 2;

invalidate();

return;

}

if (!(new_page=get_free_page()))

oom();

if (old_page >= LOW_MEM)

mem_map[MAP_NR(old_page)]–;

copy_page(old_page,new_page);

*table_entry = new_page | 7;

invalidate();

}

输入参数是页表项指针,为物理地址。

old_page = 0xfffff000 & *table_entry;

获取页帧地址,即内存页地址

if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {

*table_entry |= 2;

invalidate();

return;

}

内存页空间不再内核空间,并且只有一个进程共享,则修改页表项参数,变为可写

#define invalidate() \
__asm__("movl %%eax,%%cr3"::"a" (0))


cr3是页目录表基地址,就是刷新页目录高速缓冲

if (!(new_page=get_free_page()))

oom();

if (old_page >= LOW_MEM)

mem_map[MAP_NR(old_page)]–;

copy_page(old_page,new_page);

*table_entry = new_page | 7;

invalidate();

否则申请一页内存;并将数据复制到新的页内存中。

这里面还有好多,没能理解。。。。只是梳理了一点内容
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: