OS内核的信号机制:所有的异步都可以是同步的

资讯 2年前 (2022) 千度导航
9 0 0

给我提了一个关于操作系统的问题:怎么把定时器线程里的回调函数,(在定时器触发之后)挪到工作线程里运行?

这个需求要做的事,跟Linux内核的信号机制是一样的。

OS内核的信号机制,在1970年的Unix时代就有了,是一个上古话题

在unix里,可以使用kill -9 pid命令杀掉进程(pid为进程号),在Linux里也可以。

1,OS内核的信号,

有个专有的宏定义#define SIGKILL 9,然后信号9就成了一个特别牛的信号,大概除了0号idle进程1号init进程之外,其他进程都可以杀死。

0号进程和1号进程是不能杀死的,否则系统就崩溃了

int sys_kill(int sig, int pid)

{

if (sig < 0 || pid < 0)

return -EINVAL;

if (== pid || 1 == pid) {

if (SIGKILL == sig) return -1;

}

tasks[pid]->sigmap |= 1 << sig;

return 0;

}

OS内核里对应着kill命令的sys_kill()系统调用,大概是上面这样:

在进程的task结构体sigmap成员变量上,设置1个标志位,进程就可以收到信号了。

每个进程,在OS内核里都被一个task结构体表示,这个结构体的其中一个成员变量就是记录信号的:我们给他起名叫sigmap,Linux的不一定要叫这个名字,但肯定有这一项。

这个信号在什么时候处理呢?

等到收信号的进程下一次被调度运行的时候。

当前运行的进程,肯定是发信号的进程,否则它没法主动发起kill()系统调用。

发信号的进程做的事,只是把信号设置到接收进程的信号图上,这时信号实际上已经发到了:但是接收进程并不会马上因为SIGKILL信号而被杀死。

SIGKILL信号的杀进程,实际上进程是自杀的

当收到信号的进程再次被调度运行的时候,操作系统会让它先执行信号的处理函数,而SIGKILL的处理函数,就是exit()系统调用:进程退出

这个过程可以是异步的,等到接收进程下一次被调度时再处理,至于什么时候轮到它:等吧。

也可以让它马上同步处理,只需要在sys_kill()函数的末尾加一行代码就行:

shedule_task( tasks[pid] );

直接选择接收进程是下一个要调度的进程,并且马上调度它运行:接下来它就完事了。

不需要等OS内核统计时间片,确定调度的优先级了,既然用户想让它挂掉,OS当然要马上让它挂掉。

毕竟Linux系统也惹不起用户啊,用户是可以重装windows

接下来,说说shedule_task()之后的细节。

2,信号是怎么处理的,

每个信号都有一个处理函数,叫信号处理函数

信号处理函数,是在用户态的代码里运行的。

所以,程序员可以自己给部分信号编写处理函数,用signal()系统调用注册到OS内核,就可以(在收到信号时)运行这个自己编写的函数了。

如果信号处理函数是在内核状态运行的,那显然用户编写的函数是没法运行的,因为用户函数的内存地址用户空间(它在进程的代码段里)。

OS内核在信号处理时要做的是,把进程从内核返回后要运行的代码地址改成信号处理函数的地址。

修改过程如下:

OS内核的信号机制:所有的异步都可以是同步的

系统内核的信号处理过程

1)进程从内核返回时的状态,如上图。

内核栈上的寄存器排布顺序不一定是对的,这要查intel的手册,但是这些项肯定都有

在进程使用iret指令(中断返回)从内核返回的那一刻,内核栈上的这些数据都要弹出到对应的寄存器。

然后,进程就会运行EIP指向的用户代码,同时用户态的栈顶就是ESP

EIP和ESP指向的内容到底是什么,内核不需要管:这是由程序员写代码时确定的。

进程从内核返回之后的错误,错的是程序员,不是系统内核。

但要是返不回来,或者不能处理信号,错的就是系统内核了。

2)OS内核要做的是,修改内核栈上、保存的、用户态的、EIP和ESP(注意这3个定语):

A,让EIP指向信号处理函数

B,让ESP指向信号处理函数的参数

C,在信号处理函数的下方,放上“真正的”返回地址

D,在信号处理函数运行完之后丢掉(信号处理函数的)参数,弹出真正的返回地址:让程序恢复正常的状态,继续运行。

如上图中的绿字部分

如果一次要处理多个信号的话,就顺着用户栈继续叠加就行。

siska内核demo里的信号处理代码,如下的3张图:

因为信号处理函数有参数,而参数要压在用户态的栈上,所以信号处理函数运行完之后还要清理它。

所以,与一般的C函数不同,信号处理函数是被调函数清理堆栈的:即它是pascal调用,而不是C调用

C调用,都是主调函数清理堆栈的。

所以,信号处理函数的总入口是一段汇编代码,用来在C语言里完成这个pascal调用

这么看来,pascal这种老语言,也不是想象的那么差

这个信号处理方式,是我给出来的解决方案

至于Linux是不是也这么做的,我就不知道了。

但是,这么做是可行的

OS内核的信号机制:所有的异步都可以是同步的

siska信号处理,pascal调用的汇编

上图95行的call *(%eax),就是调用信号处理的函数指针

它前后的汇编代码,都是准备参数和清理堆栈。

3,回到开头的问题,

怎么让定时器线程在触发之后,让回调函数工作线程里运行?

回调函数一般有一个参数,表示回调上下文,但没有返回值

因为定时器的添加和处理在2个线程里,回调函数的返回值没有意义。

如果回调函数的处理出错了,就在上下文里设置错误码作为提示。

所以,它的函数声明是这样的:void callback(void* ctx);

要让它正常运行,必须把回调上下文的指针添加到工作线程的用户栈上,同时让工作线程的内核栈上保存的EIP指向回调函数

这个处理方式,与OS内核的信号处理方式是一样的。

信号处理函数的声明:void sighandler(int sig); 也是一个参数、无返回值

在定时器触发之后,定时器线程可以发起一个系统调用,把这些信息给到内核,然后内核修改工作线程的数据,让定时器的回调处理“像个信号”一样就可以了

这个系统调用如果Linux没有提供的话,就只能自己修改Linux内核代码,或者给Linus大牛提个需求了(他有可能看不过来你的邮件)。

PS:

工作线程定时器线程同一个进程里,所以它们的用户态内存代码段、数据段、堆都是共享的,只是内核栈用户栈不一样。

内核栈:在内核看来,每个线程也是一个可调度的进程,它必须有自己的内核栈页表

同一个进程的不同线程之间共享内存,靠的是页表的映射:把它们映射到同一个物理内存页上。

用户栈:不同的线程可以并发运行,它们的用户栈肯定是不同的,否则局部变量互相覆盖了:这肯定是不可能的。

siska里信号处理的代码,如下:

OS内核的信号机制:所有的异步都可以是同步的

siska信号处理,1

OS内核的信号机制:所有的异步都可以是同步的

siska信号处理,2

版权声明:千度导航 发表于 2022年11月5日 22:30。
转载请注明:OS内核的信号机制:所有的异步都可以是同步的 | 千度百科

相关文章