OS57.【Linux】理解信号: 信号的产生(2) 键盘输入和系统调用
本文探讨了Linux系统中信号的产生机制。首先解释了Ctrl+C如何通过硬件中断转化为信号:键盘输入触发中断,操作系统解释为信号发送给前台进程。信号产生是异步的,进程随时可能被中断。文章详细介绍了三种产生信号的系统调用:kill(向指定进程发送信号)、raise(向自身发送信号)和abort(发送SIGABRT信号并终止进程)。通过代码示例展示了这些调用的使用方法和效果,包括自定义信号处理函数的实
目录
1.知识回顾
之前在OS57.【Linux】理解信号: 信号的产生(1)文章讲过:Ctrl+C可以杀死前台进程,下面讲讲Ctrl+C是如何变成信号的
2.Ctrl+C是如何变成信号的
硬件中断
操作系统为了保证安全,进程是无法直接读取键盘数据的,显然键盘的键位被按下,一定是操作系统先知道,由于Linux的"一切皆文件"的思想,键盘也是文件,那么操作系统肯定会先读取键盘缓冲区文件
那操作系统怎么知道键盘上有数据要读取呢?
答: CPU上的针脚和外设是间接相连的,键盘有数据时会向CPU发送硬件中断,有CPU告诉操作系统将键盘数据拷贝到键盘文件缓冲区
计算机中的外设有很多,显然每个外设都可以向CPU发送中断,那如何确保是键盘发送的中断呢?
答: 每个中断都有唯一的编号,这个编号为中断号,CPU依此来区分不同的中断
外设向CPU发送中断,CPU根据中断解释出中断号,保护模式下, CPU根据中断号到中断描述符表找出对应中断例程的地址,然后CPU跳转到那里执行,结束后CPU通过iret返回
这个中断例程可以是操作系统将键盘外设上的数据拷贝到键盘文件缓冲区
补充: 如果多个硬件同时发送中断,操作系统会串行处理
也可以看看《Linux内核设计与实现 第3版 》中译版的 第一章 Linux内核的简介 1.1.UNIX的历史 对此的描述:
内核还要负责管理系统的硬件设备。现有的几乎所有的体系结构,包括全部 Linux 支持的体系结构,都提供了中断机制。当硬件设备和系统通信的时候,它首先要发出一个异步的中断信号去打断处理器的执行,继而打断内核的执行。中断通常对应着一个中断号,内核通过这个中断号查找相应的中断服务程序,并调用这个程序响应和处理中断。举个例子,当你敲击键盘的时候,键盘控制器发送一个中断信号告知系统,键盘缓冲区有数据到来。内核注意到这个中断对应的中断号,调用相应的中断服务程序。该服务程序处理键盘数据然后通知键盘控制器可以继续输入数据了。为了保证同步,内核可以停用中止——既可以停止所有的中断也可以有选择地停止某个中断号对应的中断。许多操作系统的中断服务程序,包括 Linux 的,都不在进程上下文中执行。它们在一个与所有进程都无关的、专门的中断上下文运行。之所以存在这样一个专门的执行环境,就是为了保证中断服务程序能够在第一时间响应和处理中断请求,然后快速地退出。
信号
上方讲的是硬件中断,之前在OS57.【Linux】理解信号: 信号的产生(1)文章讲的信号是用软件的方式模拟硬件中断
变成信号的整个过程
用户按下Ctrl+C,这个键盘输入产生一个硬件中断,被操作系统获取,解释成信号,发送给目标前台进程, 前台进程因为收到信号,进而引起进程退出
回显的含义

正常情况下,从键盘输入的数据会显示到显示器上,这是因为操作系统将键盘缓冲区的数据拷贝到显示器的缓冲区

注: 图中的"键盘文件"和"显示器文件"是根据Linux的"一切皆文件"的思想,将硬件的内容抽象为文件

得出不回显的含义: 键盘缓冲区的数据不拷贝到显示器的缓冲区
3.信号产生是异步的
前台进程在运行过程中用户随时可能按下Ctrl+C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步
(Asynchronous)的
1.例子理解: 操作系统不知道用户什么时候会按键盘,这是异步的
2.对立面理解: 同步的反义词是异步
信号是进程之间事件异步通知的一种方式,属于软中断,软中断是仿照硬件中断实现的软件逻辑
4.信号的产生: 使用系统调用
新建以下文件:
test_signal
├── hello_world.cpp
├── makefile
└── test_signal.cpp
hello_world.cpp写入:
makefile:
kill

作用: 向指定进程发送指定的信号
kill的参数:
pid: 进程号
sig: 信号编号
编写"kill -signo pid"命令
test_signal.cpp写入:
#include <unistd.h>
#include <iostream>
#include <signal.h>
int main(int argc,char* argv[])
{
if (argc!=3)
{
std::cout<<"用法: kill -signo pid"<<std::endl;
exit(1);
}
std::string signo_s=argv[1];
std::string pid_s=argv[2];
int signo=std::stoi(std::string(signo_s,1));
int pid=stoi(pid_s);
if (pdi<0||pid>64)
{
std::cout<<"非法的信号编号"<<std::endl;
exit(2);
}
if (kill(pid,signo))
{
perror("kill failed");
exit(3);
}
std::cout<<"已发送信号"<<std::endl;
return 0;
}
启动一个无限打印Hello World的进程,尝试杀死
运行结果:

raise

作用: 发送一个信号给调用者
sig是信号的编号
test_signal.cpp写入:
#include <unistd.h>
#include <iostream>
#include <signal.h>
int main(int argc,char* argv[])
{
int cnt=0;
for(;;)
{
std::cout<<"Hello World!"<<std::endl;
sleep(1);
cnt++;
if (cnt==5)
raise(9);
}
return 0;
}
运行结果:

结论: kill(getpid(),signo)和rasie(signo)作用等价
abort

作用: 给调用者发送6号信号SIGABRT
可以手动捕获abort()发送的6号信号,test_signal.cpp写入:
#include <unistd.h>
#include <iostream>
#include <signal.h>
void myhandler(int signo)
{
std::cout<<"已执行自定义动作,信号编号为"<<signo<<"号"<<std::endl;
}
int main(int argc,char* argv[])
{
signal(SIGABRT,myhandler);
int cnt=0;
for(;;)
{
std::cout<<"Hello World!"<<std::endl;
sleep(1);
cnt++;
if (cnt==5)
abort();
}
return 0;
}
运行结果:

发现捕获SIGABRT信号后,程序仍然终止了

abort函数发送SIGABRT信号,编号为6

结论: abort()除了能发送信号,内部实现有终止进程这个功能
5.系统调用号和组织系统调用的数据结构
Linux的每个系统调用函数都有对应的系统调用号,以x86为例,这个调用号可以在/arch/x86/entry/syscalls目录下找到:

可以看到32位的系统调用号存在syscall_32.tbl,64位的系统调用号存在syscall_64.tbl
需要强调的是, 同一个系统调用号在32位和64位下对应的系统调用函数不一样
这里截取一部分:
syscall_32.tbl:
0 i386 restart_syscall sys_restart_syscall
1 i386 exit sys_exit - noreturn
2 i386 fork sys_fork
3 i386 read sys_read
4 i386 write sys_write
5 i386 open sys_open compat_sys_open
6 i386 close sys_close
7 i386 waitpid sys_waitpid
8 i386 creat sys_creat
syscall_64.tbl:
0 common read sys_read
1 common write sys_write
2 common open sys_open
3 common close sys_close
4 common stat sys_newstat
5 common fstat sys_newfstat
6 common lstat sys_newlstat
7 common poll sys_poll
8 common lseek sys_lseek
9 common mmap sys_mmap
Linux内核中有大量的系统调用,为了管理这些系统调用,需要将这些系统调用放到对应的数据结构中
在arch/x86/entry/syscall_32.c和/arch/x86/entry/syscall_64.c中都定义了sys_call_table这个数据结构:
syscall_32.c的部分内容:
/*
* The sys_call_table[] is no longer used for system calls, but
* kernel/trace/trace_syscalls.c still wants to know the system
* call address.
*/
#ifdef CONFIG_X86_32
#define __SYSCALL(nr, sym) __ia32_##sym,
const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_32.h>
};
#undef __SYSCALL
#endif
syscall_64.c的部分内容:
/*
* The sys_call_table[] is no longer used for system calls, but
* kernel/trace/trace_syscalls.c still wants to know the system
* call address.
*/
#define __SYSCALL(nr, sym) __x64_##sym,
const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
#undef __SYSCALL
更多推荐



所有评论(0)