文章目录
- 前言
- 一、认识进程信号
- 1.1 同步异步机制的定义
- 1.2 基本结论
- 二、信号的产生
- 2.1 Linux系统下的信号列表
- 2.2 信号的处理
- 2.2.1 更改默认处理
- 2.2.2前台和后台进程
- 2.3 键盘产生信号
- 2.4 kill系统命令产生信号
- 2.5 系统调用产生信号
- 2.6 硬件异常产生信号(重要)
- 2.6.1 除零错误
- 2.6.2 野指针问题
- 2.7 软件条件产生信号 && 操作系统运行原理模拟
- 2.7.1 理解闹钟
- 2.8 Core or Trem【重要】
- 三、信号保存
- 3.1 信号其他相关常见概念
- 3.2 信号在内核中的表示
- 3.3信号集sigset_t
- 3.3.1 信号集操作函数
- 3.3.2 信号集函数demo
- 四、捕捉信号
- 4.1 信号捕捉流程
- 4.2 sigaction
- 五 、操作系统的运行【重要】
- 5.1硬件中断
- 5.2 软中断
- 5.2.1 系统调用
- 5.3 用户态和内核态
- 5.3.1 切换条件
- 六、可重入函数
- 总结
前言
在操作系统中,进程信号(Signal) 是一种用于通知进程发生特定事件的通信机制,常用于进程间通信(IPC)、异常处理或系统控制。
我们将根据以下顺序阐述信号。
一、认识进程信号
在我们生活中有关信号的类比便是红绿灯了,它亮红灯时通知人们要等待,绿灯表示可以通行,它维持了马路中交通秩序。而在计算机的世界中,进程信号的作用便是通知进程发生了特定事件(如用户输入、硬件异常、其他进程请求等)
1.1 同步异步机制的定义
- 同步:任务按顺序执行,前一个任务完成后,才能执行下一个任务。执行流程是阻塞的(Blocking)。
- 类比:在餐厅点单,服务员必须等厨师完成当前菜品后,才能处理下一个订单。
- 异步:任务触发后不等待完成,继续执行后续代码,任务完成后通过回调、事件或Promise通知结果。执行流程是非阻塞的(Non-blocking)。
- 类比:餐厅服务员将订单交给厨师后,继续接待其他顾客,此时服务员可以不用等待餐品的完成而去继续做应该做的事情,厨师完成后通知服务员取餐。
进程信号的使用是异步的。 |
1.2 基本结论
- 操作系统中规定了进程如何识别信号,所以信号没产生的时候,进程就知道如何处理信号。
- 由于异步机制,信号的处理通常不是立即处理(所以要求进程要记录下信号),而是等待合适的时机处理信号。
- 产生信号的信号源非常多
在task_struct中会有一个信号 位图记录每个信号,收到信号本质上还是操作系统自己来修改内核数据
struct task_struct
{unsigned int signal_bitmap;
二、信号的产生
我们平常用的ctrl+c就是一种由我们人敲键盘输入的信号。
2.1 Linux系统下的信号列表
kill -l
这些信号在内核源代码中都是宏定义,1-31普通信号(可以不立即处理,crtl+c就是2号信号),34-64称作实时信号(产生需要立即处理)不做考虑,其中相当一部分的信号就是让进程终止 |
2.2 信号的处理
收到信号,处理信号,会有三种方式
- 系统默认处理动作
- 自定义信号处理动作
- 忽略处理
2.2.1 更改默认处理
一下例子均以2号信号为例做演示,首先我们来认识一下系统调用下的更改系统默认处理信号的函数
我们需要对signal函数传入函数指针,这个函数指针指向我们自定义的函数,这个函数将解释信号的作用。
例:
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void handlerSig(int sig)
{cout<<"获得了一个信号"<<sig<<endl;
}
int main()
{signal(SIGINT,handlerSig);int cnt=0;while(true){cout<<"hello world"<<cnt++<<endl;sleep(1);}return 0;
}
当我们按ctrl+c(2号信号)的时候,不会终止这个进程了,而是改为执行我们写的handlerSig函数,想要终止我们可以按ctrl+\(三号信号)
以下命令可以查看信号的详细信息
man 7 signal
action是信号的处理动作,core,term是终止但会有差别,ign是忽略,还有stop停止
2.2.2前台和后台进程
前台进程:./xxx
后台进程:./xxx &
前台进程只能有一个,可以和输入(键盘等)进行交互,而后台进程可以有多个
命令补充:
jobs | 查看所有后台任务 |
---|---|
fg 任务号 | 将特定的进程提到前台 |
ctrl+z | 进程切换到后台 |
bg 任务号 | 将后台任务恢复到前台 |
2.3 键盘产生信号
在终端(Terminal)中,以下组合键会触发信号:
键盘组合 | 信号名称 | 信号编号 | 默认行为 |
---|---|---|---|
Ctrl + C | SIGINT | 2 | 中断进程(Interrupt) |
Ctrl + \ | SIGQUIT | 3 | 终止进程并生成核心转储 |
Ctrl + Z | SIGTSTP | 20 | 暂停进程(可恢复) |
Ctrl + D | EOF | - | 发送“文件结束”(非信号) |
2.4 kill系统命令产生信号
通过kill命令,向指定进程发送指定的信号,它隶属于系统命令
kill _signal(信号) _pid(进程id)
2.5 系统调用产生信号
在c语言系统下提供了kill,absort,raise等信号相关函数
kil>>>
KILL(2) Linux Programmer's Manual KILL(2)NAMEkill - send signal to a processSYNOPSIS#include <sys/types.h>#include <signal.h>int kill(pid_t pid, int sig);//用于给指定进程发送指定信号
以下是模拟实现kill的demo
#include<iostream>
#include<string>
#include<unistd.h>
#include<signal.h>
using namespace std;int main(int argc,char* argv[])
{if(argc!=3){cout<<"./mykill target signal"<<endl;return 1;}int sig=stoi(argv[1]);pid_t pid=stoi(argv[2]);int n=kill(pid,sig);cout<<"运行成功"<<endl;if(n==0){cout<<"send"<<sig<<"to"<<pid<<"success"<<endl;}return 0;
}
abort>>>
ABORT(3) Linux Programmer's Manual
NAME abort - cause abnormal process termination
SYNOPSIS#include<stdlib>void abort(void);//向当前进程发送6号信号,异常终止程序
void handler(int sig)
{std::cout << "get a sig: " << sig << std::endl;
}int main()
{signal(6, handler);while (true){sleep(1);std::cout << "hello bit, pid: " << getpid() << std::endl;abort();}
}
我们发现abort可以被捕捉,但还是会终止进程(例外情况)。9号信号是不允许被捕捉的,也是用来异常终止的。为了维护系统的安全性和稳定性,某些命令可能不允许或限制用户自定义信号捕捉。 |
raise>>>向当前进程发送信号
2.6 硬件异常产生信号(重要)
操作系统是软硬件的管理者
当我们的程序发生野指针错误,和除零这类典型的错误时,在硬件上运行后发现错误便会由操作系统向进程发送信号,达到终止进程的目的。收到这些信号,进程必须退出吗?不是,可以用signal等系统调用函数捕捉以上的异常信号,但是我们推荐终止进程,为什么呢?
2.6.1 除零错误
关于进程中的计算问题,一般都是交由cpu完成的,在计算的过程中,难免会出现错误的计算,比如说除0,那么cpu又是如何知道的呢?
这就要提到cpu中的寄存器了,cpu中是有很多的寄存器的,其中有一个寄存器:EFLAGS寄存器(状态寄存器)。该寄存器中有很多状态标志:这些标志表示了算术和逻辑操作的结果,如溢出(OF)、符号(SF)、零(ZF)、进位(CF)、辅助进位(AF)和奇偶校(PF)。例如当前寄存器存储的是0 0 0 0 0,若第二个标志位代表是否溢出,那么检测到进行计算的硬件溢出的时候,此时寄存器便是0 0 0 1 0
我们要知道cup内部是只有一套寄存器的,寄存器中的数据是属于每一个进程的,是需要对进程上下文进行保存和恢复的。
如果进程因为除0操作而被操作系统标记为异常状态,但没有被终止,那么它可能会被挂起,等待操作系统的进一步处理。当操作系统决定重新调度这个进程时,会进行上下文切换,即将当前进程的上下文保存到其PCB(进程控制块)中,并加载异常进程的上下文到CPU寄存器中。
上下文切换是一个相对耗时的过程,包括保存和恢复寄存器、堆栈等信息。当切换回这个进程的时候,溢出标志位的错误信息同样会被恢复,会频繁的导致除0异常而触发上下文切换,会大大增加系统的开销。
为什么推荐呢?因为终止进程能释放进程上下文的数据,包括溢出标志数据或其他的异常数据。
2.6.2 野指针问题
这个问题就与页表,MMU及CR2,CR3寄存器有关联了。
MMU和页表是操作系统实现虚拟内存管理和内存保护的关键机制,它们通过虚拟地址到物理地址的转换来确保程序的正确运行和内存安全。CR2和CR3寄存器在内存管理和错误处理中扮演着重要角色。CR3寄存器用于切换不同进程的页表,而CR2寄存器则用于存储引起页错误的虚拟地址,帮助操作系统定位和处理错误。
CR2寄存器用于存储引起页错误的线性地址(即虚拟地址)。当MMU无法找到一个虚拟地址对应的物理地址时(例如,解引用空指针或野指针),会触发一个页错误(page fault)。此时,CPU会将引起页错误的虚拟地址保存到CR2寄存器中,并产生一个异常,此时就会向进程发送11号信号。
2.7 软件条件产生信号 && 操作系统运行原理模拟
alarm(闹钟)函数:
#include <unistd.h>unsigned int alarm(unsigned int seconds);
- 参数:
seconds:定时时间(秒),如果为 0,则取消之前的定时器。
- 返回值:
返回之前设置的剩余时间(秒),如果之前没有定时器,则返回 0。
struct task_struct{pid_t id;int count = 10; // 时间片,本质就是一个计数器!void(*code)();};std::vector<task_struct> task_list;// //funcvoid Sched(){std::cout << "我是进程调度" << std::endl;//t.count--;// if(t.count <= 0)// 切换其他进程}void MemManger(){std::cout << "我是周期性的内存管理,正在检查有没有内存问题" << std::endl;}void Fflush(){std::cout << "我是刷新程序,我在定期刷新内存数据,到磁盘" << std::endl;
}
// /using func_t = std::function<void()>;std::vector<func_t> funcs;
int timestamp = 0;// 每隔一秒,完成一些任务void handlerSig(int sig){timestamp++; //10000std::cout << "##############################" << std::endl;for(auto f : funcs)f();std::cout << "##############################" << std::endl;int n = alarm(1);//由于闹钟是一次性的,设置重复闹钟,每隔一秒收到一个闹钟信号}int main(){funcs.push_back(Sched);funcs.push_back(MemManger);funcs.push_back(Fflush);signal(SIGALRM, handlerSig);alarm(1); while(true) // 这就是操作系统!{pause();//wait for signal}return 0;}
2.7.1 理解闹钟
系统闹钟,其实本质是OS必须⾃⾝具有定时功能,并能让⽤⼾设置这种定时功能,才可能实现闹钟这样的技术。
当操作系统中多处要用到alarm的时候,OS就会采用时间轮(可以用最小堆进行理解),进行判断,要先向谁发送SIGALRM信号
2.8 Core or Trem【重要】
core 和 term 是两种不同的信号默认行为,它们决定了信号未被捕获(没有自定义处理函数)时进程的响应方式。
core(Core Dump,核心转储)
term(Terminate,终止)
core:会在当前路径下形成一个文件,进程异常退出的时候,进程在内存中的核心数据拷贝到磁盘形成一个文件,然后进程退出。- - -支持debug{云服务器上core dump被禁掉}
term:进程退出
使用core主要是为了定位程序在哪里出错的,用于debug调试 |
ulimit -a 用于查看core信息,若为0则不能形成
ulimit -c xxxx 设置core文件大小
core-file core 在debug调试过程中直接定位core文件中出错行
进程退出码:
三、信号保存
3.1 信号其他相关常见概念
- 实际执⾏信号的处理动作(自定义,默认,忽略)称为信号递达(Delivery)
- 信号从产⽣到递达之间的状态,称为信号未决(Pending)。信号在位图中还没来得及处理便是信号未决
- 进程可以选择阻塞【屏蔽】(Block)某个信号。
- 被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作。
3.2 信号在内核中的表示
- 每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针表⽰处理动作。信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
在上图的例⼦中,SIGHUP信号未阻塞也未产⽣过,当它 递达时直接执⾏默认处理动作。 - SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是⽤⼾⾃定义函数sighandler
当进程收到信号的时候pending 信号集中对应的信号 0->1 ,当我们准备递达的时候首先要清空pengding信号集中对应的信号位图 1->0,想要知道一个信号是否可以被递达:pen ding & (~block)
3.3信号集sigset_t
#ifndef ____sigset_t_defined
#define ____sigset_t_defined#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;#endif
每个信号只有⼀个bit的未决标志,⾮0即1,不记录该信号产⽣了多少次,阻塞标志也是这样表⽰的。
因此,未决和阻塞标志可以⽤相同的数据类型_sigset_t来存储,这个类型可以表⽰每个信号的“有效”或“⽆效”状态,
在阻塞信号集中“有效”和“⽆效”的含义是该信号是否被阻塞,⽽在未决信号集中“有效”和“⽆效”的含义是该信号是否处于未决状态。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal_Mask)
3.3.1 信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
- 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
- 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//返回值:若成功则为0,若出错则为-1
sigprocmask(操作block表的函数)
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
- how:指定对信号屏蔽集的操作方式,有以下几种方式:
- SIG_BLOCK:将set所指向的信号集中包含的信号添加到当前的信号屏蔽集中,即信号屏蔽集和set信号集进行逻辑或操作。
- SIG_UNBLOCK:将set所指向的信号集中包含的信号从当前的信号屏蔽集中删除,即信号屏蔽集和set信号集的补集进行逻辑与操作。
- SIG_SETMASK:将set的值设定为新的进程信号屏蔽集,即set直接对信号屏蔽集进行了赋值操作。
- set:指向一个sigset_t类型的指针,表示需要修改的信号集合。如果只想读取当前的屏蔽值而不进行修改,可以将其置为NULL。
- oldset:指向一个sigset_t类型的指针,用于存储修改前的内核阻塞信号集。如果不关心旧的信号屏蔽集,可以传递NULL。
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
#include <signal.h>
int sigpending(sigset_t *set);
sigpending(检查pending信号集,获取当前进程pending位图)
- 参数:set 是一个指向 sigset_t 类型的指针,用于存储当前进程的未决信号集合。
- 返回值:函数调用成功时返回 0,失败时返回 -1,并设置 errno 以指示错误原因。
3.3.2 信号集函数demo
#include <iostream>
#include <signal.h>
#include<unistd.h>
void PrintPending(__sigset_t &pending)
{printf("我是一个进程(%d),pending:",getpid());for (int signo = 1; signo <= 31; signo++){if (sigismember(&pending, signo)){printf("1");}else{printf("0");}}printf("\n");
}
int main()
{// 屏蔽2号信号__sigset_t block, oblock;sigemptyset(&block);sigemptyset(&oblock);sigaddset(&block, SIGINT); // 当前仅仅是在用户态给block这个结构体中的信号屏蔽字赋值int n = sigprocmask(SIG_SETMASK, &block, &oblock);(void)n;int cnt=0;while (true){sigset_t pending;int m = sigpending(&pending);PrintPending(pending);if(cnt==10){sigprocmask(SIG_SETMASK, &oblock, nullptr);}cnt++;sleep(1);}return 0;
}
在递达前对应未决信号集置零,常规信号在递达前产生多次计为一次 |
四、捕捉信号
4.1 信号捕捉流程
如果信号的处理动作是⽤⼾⾃定义函数,在信号递达时就调⽤这个函数,这称为捕捉信号。
由于信号处理函数的代码是在⽤⼾空间的,处理过程⽐较复杂,举例如下:
- ⽤⼾程序注册了 SIGQUIT 信号的处理函数 sighandler 。
- 当前正在执⾏ main 函数,这时发⽣中断或异常切换到内核态。
- 在中断处理完毕后要返回⽤⼾态的 main 函数之前检查到有信号 SIGQUIT 递达。
- 内核决定返回⽤⼾态后不是恢复 main 函数的上下⽂继续执⾏,⽽是执⾏ sighandler 函数, sighandler 和 main 函数使⽤不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个独⽴的控制流程。
- sighandler 函数返回后⾃动执⾏特殊的系统调⽤ sigreturn 再次进⼊内核态。
- 如果没有新的信号要递达,这次再返回⽤⼾态就是恢复 main 函数的上下⽂继续执⾏了。
以上案例适用于捕捉信号
4.2 sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);struct sigaction { void (*sa_handler)(int); // 指向信号处理函数的指针,接收信号编号作为参数 void (*sa_sigaction)(int, siginfo_t *, void *); // 另一个信号处理函数指针,支持更丰富的信号信息 sigset_t sa_mask; // 设置在处理该信号时暂时屏蔽的信号集 int sa_flags; // 指定信号处理的其他相关操作 void (*sa_restorer)(void); // 已废弃,不用关心
};
- signum:指定要设置或获取处理程序的信号编号。可以指定SIGKILL和SIGSTOP以外的所有信号。
- act:指向sigaction结构体的指针,用于指定新的信号处理方式。如果此参数非空,则根据此参数修改信号的处理动作。
- oldact:如果非空,则通过此参数传出该信号原来的处理动作。(如果你想恢复以前的方式,此参数就是保存之前的操作方式)
当某个信号的处理函数被调⽤时,内核⾃动将当前信号加⼊进程的信号屏蔽字,当信号处理函数返回时⾃动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产⽣,那么它会被阻塞到当前处理结束为⽌。如果在调⽤信号处理函数时,除了当前信号被⾃动屏蔽之外,还希望⾃动屏蔽另外⼀些信号,则⽤sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时⾃动恢复原来的信号屏蔽字。
五 、操作系统的运行【重要】
操作系统就是躺在中断处理例程上的代码块。中断是有优先级的。
在硬件中会有时钟源(当代集成在cpu中),以特定频率向cpu发送中断,至此操作系统在硬件时钟中断的驱动下进行进程调度,操作系统是基于中断进行工作的软件,在思想上,信号机制就是中断的衍生 |
进程调度切换根据时间片伪代码:
struct task_struct
{int cnt=10;
}
//时间片current->cnt--;if(current->cnt==0)
{schedule();//进程调度
}
5.1硬件中断
以键盘举例:
当我们在键盘上输入命令或数据时,键盘上的电路会检测到按键的按下或释放,并生成相应的电信号。这些电信号随后被转化为中断信号,通过硬件连线(如总线)传递到CPU的中断控制器。中断控制器根据信号的优先级和当前CPU的状态,决定是否向CPU发送中断请求。
CPU通过中断处理机制来响应来自硬件设备的中断请求。在保护模式下,CPU会维护一个中断描述符表(IDT),该表包含了所有可能的中断向量及其对应的中断服务例程的地址。当中断发生时,CPU会根据中断向量在IDT中找到对应的中断服务例程的地址,并跳转到该地址执行中断处理程序。
1,2,3,4过程可以理解为硬件上通知cpu准备好了来传递数据,6,7过程则是软件方面来判断数据的读写并执行相关操作。
在操作系统上会有一个中断向量表,它是操作系统内部的一个函数指针,可以理解为它拿着中断号来执行对应的读写相关操作。
至此OS不再关注外部设备是否准备好,而是根据是否有对应的数据来执行函数方法。
5.2 软中断
由CPU内部实现的中断被称为软中断,为了让操作系统⽀持进⾏系统调⽤,CPU也设计了对应的汇编指令(int 0x80或者_syscall),可以让CPU内 部触发中断逻辑。
缺⻚中断?内存碎⽚处理?除零野指针错误?这些在cpu中寄存器上由软件导致硬件出现的问题,全部都会被转换成为CPU内部的软中断,然后⾛中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有的是⽤来处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。
5.2.1 系统调用
操作系统不会提供任何系统调用接口,而是提供系统调用号 |
我们平时用的大部分系统调用函数都是Linux的gnu_C标准库给我们封装的,而不是真正意义上OS所提供的操作函数。
当我们使用系统调用的时候,其中系统调用会通过宏定义等方式转换成系统调用号,然后通过eax寄存器(32位系统)来进行系统调用号的传递,接着执行syscall
//执行syscall后通过汇编指令找到系统调用函数指针表中的函数
syscall()
{call [_sys_call_table+eax*4];
]
系统调⽤的过程,其实就是先int 0x80、syscall陷⼊内核,本质就是触发软中断,CPU就会⾃动执⾏系统调⽤的处理⽅法,⽽这个⽅法会根据系统调⽤号,⾃动查表,执⾏对应的⽅法
5.3 用户态和内核态
-
内核态:也叫内核空间,是内核进程/线程所在的区域。主要负责运行系统、硬件交互。
-
用户态:也叫用户空间,是用户进程/线程所在的区域。主要用于执行用户程序。
虚拟地址空间大小为4G(32位系统),其中内核空间【3,4G】,而用户空间则是【0,3G】,所有进程的内核空间公用内核页表进行虚拟到物理地址空间的映射,而用户区则是有各自的页表进行映射。区分内核态和用户态的主要原因主要是一种保护机制。 |
- 内核态:运行的代码不受任何限制,CPU可以执行任何指令。
- 用户态:运行的代码需要受到CPU的很多检查,不能直接访问内核数据和程序,也就是说不可以像内核态线程一样访问任何有效地址。
- 操作系统在执行用户程序时,主要工作在用户态,只有在其执行没有权限完成的任务时才会切换到内核态。
5.3.1 切换条件
内核态和用户态的改变依赖于寄存器CS中的数据,不做过多赘述
1、系统调用(主动)
由于用户态无法完成某些任务,用户态会请求切换到内核态,内核态通过为用户专门开放的中断完成切换。
2、异常(被动)
在执行用户程序时出现某些不可知的异常,会从用户程序切换到内核中处理该异常的程序,也就是切换到了内核态。
3、外围设备中断(被动)
外围设备发出中断信号,当中断发生后,当前运行的进程暂停运行,并由操作系统内核对中断进程处理,如果中断之前CPU执行的是用户态程序,就相当于从用户态向内核态的切换。
六、可重入函数
- main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断等中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
- 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
如果一个函数符合以下条件之一则是不可重入的:(大部分函数是不可被重入的,可重入或者不可重入,描述的是函数的特点,并不是优缺点)
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
总结
本文主要介绍了信号以及使用,并了解了操作系统运行的逻辑,全文一万四千字符,其中由于博主知识面缺陷,仍有不足之处,但也是百尺竿头更进一步,共勉。