文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-12-01 12:22

关注

这个需求要做的事,跟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 (0 == 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内核在信号处理时要做的是,把进程从内核返回后要运行的代码地址,改成信号处理函数的地址。

修改过程如下:

系统内核的信号处理过程

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是不是也这么做的,我就不知道了。

但是,这么做是可行的。

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

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

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

3.回到开头的问题

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

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

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

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

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

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

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

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

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

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

PS:

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

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

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

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

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

siska信号处理,1

siska信号处理,2

来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯