Linux系统——进程信号之保存和捕捉信号

💁‍♂️个人主页:进击的荆棘

👇作者其它专栏:

《数据结构与算法》《算法》《C++起始之路》《Linux》


目录

1.信号前导

2.产生信号

3.保存信号

4.捕捉信号

5.可重入函数

6.volatile

7.SIGCHLD信号

8.附录


3.保存信号

3.1信号相关概念

●实际执行信号的处理动作称为信号递达(Delivery)

●信号从产生到递达之间的状态,称为信号未决(Pending)

●进程可以选择阻塞(Block)某个信号

●被阻塞的信号产生时将保持在未决状态,直到进程接触对此信号的阻塞,才会执行递达的动作

●注,阻塞和忽略是不同的,只有信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

3.2在内核中的表示

信号在内核中的表示示意图

●每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作

●SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞

●SIGQUIT信号从未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler

若在进程解除对某些信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只记一次,而实时信号在递达之前产生多次可以依次放在一个队列里

cpp 复制代码
// 内核结构 2.6.18
struct task_struct {
    ...
    /* signal handlers */
    struct sighand_struct *sighand;
    sigset_t blocked
    struct sigpending pending;
    ...
};

struct sighand_struct {
    atomic_t count;
    struct k_sigaction action[_NSIG]; // #define _NSIG 64
    spinlock_t siglock;
};

struct __new_sigaction {
    __sighandler_t sa_handler;
    unsigned long sa_flags;
    void (*sa_restorer)(void); /* Not used by Linux/SPARC */
    __new_sigset_t sa_mask;
};

struct k_sigaction {
    struct __new_sigaction sa;
    void __user *ka_restorer;
};

/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);

struct sigpending {
    struct list_head list;
    sigset_t signal;
};

3.3 sigset_t

从上图来看,每个信号只有一个bit未决信号,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的"屏蔽"应理解为阻塞而不是忽略(类似于权限那里的umask)

3.4信号集操作函数

sigset_t类型对于每种信号用一个bit表示"有效"或"无效"状态,至于这个类型内部是如何存储这些bit则依赖于系统实现,从使用者的角度不必关心,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量无意义

cpp 复制代码
#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置1,表示该信号集的有效信息包括系统支持的所有信号

●注:在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以再调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

这四个函数都是成功返回0,出错返回-1。sigismember是一个bool函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1

3.4.1 sigprocmask

调用sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1

若oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。若set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。若oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前信号屏蔽字位mask,下标说明how参数的可选值

若调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达

3.4.2 sigpending

cpp 复制代码
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>

void PrintPending(sigset_t &pending)
{
    std::cout << "curr process[" << getpid() << "]pending: ";
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << "\n";
}

void handler(int signo)
{
    std::cout << signo << " 号信号被递达!!!" << std::endl;
    std::cout << "-------------------------------" << std::endl;
    sigset_t pending;
    sigpending(&pending);
    PrintPending(pending);
    std::cout << "-------------------------------" << std::endl;
}

int main()
{
    // 0. 捕捉2号信号
    signal(2, handler); // ⾃定义捕捉
    //signal(2, SIG_IGN); // 忽略⼀个信号
    //signal(2, SIG_DFL); // 信号的默认处理动作
    // 1. 屏蔽2号信号
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set, SIGINT); // 我们有没有修改当前进⾏的内核block表呢?没有
    // 1.1 设置进⼊进程的Block表中
    sigprocmask(SIG_BLOCK, &block_set, &old_set); // 真正的修改当前进⾏的内核block表,完成了对2号信号的屏蔽!
    int cnt = 15;
    while (true)
    {
        // 2. 获取当前进程的pending信号集
        sigset_t pending;
        sigpending(&pending);
        // 3. 打印pending信号集
        PrintPending(pending);
        cnt--;
        // 4. 解除对2号信号的屏蔽
        if (cnt == 0)
        {
            std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
            sigprocmask(SIG_SETMASK, &old_set, &block_set);
        }
        sleep(1);
    }
}

$ ./run
curr process[448336]pending: 0000000000000000000000000000000
curr process[448336]pending: 0000000000000000000000000000000
^Ccurr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010
curr process[448336]pending: 0000000000000000000000000000010

程序运行时,每秒钟把各信号的未决状态打印一遍由于我们阻塞了SIGINT信号,按Ctrl+c会时SIGINT信号处于未决状态,按Ctrl+\仍可以终止程序,因为SIGQUIT信号没有阻塞

4.捕捉信号

4.1信号捕捉的流程

若信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号

由于信号处理函数的代码是在用户空间的,处理过程比较复杂

●用户程序注册了SIGQUIT信号的处理函数sighandler

●当前正在执行main函数,这时发生中断或异常切换到内核态

●在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达

●内核决定用户态后不是恢复main函数的上下文继续执行,而是执行sighanlder函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程

●sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态

●若没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行

4.2 sigaction

cpp 复制代码
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction
*oact);

●sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回-1。signo是指定信号的编号。若act指针为空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体

●将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int函数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统调用

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,若这种信号再次产生,那么它会被阻塞到当前处理结束为止。若在调用信号处理函数时,处理当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项,这里都把它设为0,sa_sigaction是实时信号的处理函数

4.3操作系统是如何运行的

4.3.1硬件中断

●中断向量表就是操作系统的一部分,启动就加载到内存中了

●通过外部硬件中断,操作系统就不需要对外设进行任何周期性的检测或轮询

●由外部设备触发的,中断系统运行流程,叫做硬件中断

cpp 复制代码
//Linux内核0.11源码
void trap_init(void)
{
    int i;
    set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。
    set_trap_gate(1,&debug);
    set_trap_gate(2,&nmi);
    set_system_gate(3,&int3); /* int3-5 can be called from all */
    set_system_gate(4,&overflow);
    set_system_gate(5,&bounds);
    set_trap_gate(6,&invalid_op);
    set_trap_gate(7,&device_not_available);
    set_trap_gate(8,&double_fault);
    set_trap_gate(9,&coprocessor_segment_overrun);
    set_trap_gate(10,&invalid_TSS);
    set_trap_gate(11,&segment_not_present);    
    set_trap_gate(12,&stack_segment);
    set_trap_gate(13,&general_protection);
    set_trap_gate(14,&page_fault);
    set_trap_gate(15,&reserved);
    set_trap_gate(16,&coprocessor_error);
// 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱
⻔。
    for (i=17;i<48;i++)
        set_trap_gate(i,&reserved);
    set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。
    outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。
    outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。
    set_trap_gate(39,&parallel_interrupt);// 设置并⾏⼝的陷阱⻔。
}

void
rs_init (void)
{
    set_intr_gate (0x24, rs1_interrupt); // 设置串⾏⼝1 的中断⻔向量(硬件IRQ4信号)。
    set_intr_gate (0x23, rs2_interrupt); // 设置串⾏⼝2 的中断⻔向量(硬件IRQ3信号)。
    init (tty_table[1].read_q.data); // 初始化串⾏⼝1(.data 是端⼝号)。
    init (tty_table[2].read_q.data); // 初始化串⾏⼝2。
    outb (inb_p (0x21) & 0xE7, 0x21); // 允许主8259A 芯⽚的IRQ3,IRQ4 中断信号请求。
}

4.3.2时钟中断

问题:

●进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行?

●外部设备可以触发硬件中断,但是这个是需要用户或设备自己触发,有无自己可以定期触发的设备

此时,操作系统就可以在硬件的推动下,自动调度

cpp 复制代码
// Linux 内核0.11
// main.c
sched_init(); // 调度程序初始化(加载了任务0 的tr, ldtr) (kernel/sched.c)

// 调度程序的初始化⼦程序。
void sched_init(void)
{
    ...
    set_intr_gate(0x20, &timer_interrupt);
    // 修改中断控制器屏蔽码,允许时钟中断。
    outb(inb_p(0x21) & ~0x01, 0x21);
    // 设置系统调⽤中断⻔。
    set_system_gate(0x80, &system_call);
    ...
}

// system_call.s
_timer_interrupt:
    ...
;// do_timer(CPL)执⾏任务切换、计时等⼯作,在kernel/shched.c,305 ⾏实现。
    call _do_timer ;// 'do_timer(long CPL)' does everything from

// 调度⼊⼝
void do_timer(long cpl)
{
    ...
    schedule();
}

void schedule(void)
{
    ...
    switch_to(next); // 切换到任务号为next 的任务,并运⾏之。
}

4.3.3死循环

若是这样,那操作系统不就什么也不用做了吗?是的,操作系统就是什么也不用做,需要什么功能,就像中断向量表中添加方法即可。操作系统的本质:就是一个死循环

cpp 复制代码
void main(void) /* 这⾥确实是void,并没错。 */
{     /* 在startup 程序(head.s)中就是这样假设的。 */
    ...
/*
* 注意!! 对于任何其它的任务,'pause()'将意味着我们必须等待收到⼀个信号才会返
* 回就绪运⾏态,但任务0(task0)是唯⼀的意外情况(参⻅'schedule()'),因为任
* 务0 在任何空闲时间⾥都会被激活(当没有其它任务在运⾏时),
* 因此对于任务0'pause()'仅意味着我们返回来查看是否有其它任务可以运⾏,如果没
* 有的话我们就回到这⾥,⼀直循环执⾏'pause()'。
*/
    for (;;)
    pause();
} // end main

●这样,操作系统,就可以在硬件时钟的推动下,自动调度了

●所以,什么是时间片?CPU为什么有主频?为什么主频越快,CPU越快?主频可以作为OS调度执行速度的参考之一

4.3.4软中断

●上述硬件中断,需要硬件设备触发

●有无可能,因为软件原因,也能触发上面的逻辑?有!

●为让操作系统支持进行系统调用,CPU也设计了对应的汇编指令(int或syscall),可以让CPU内部触发中断逻辑

所以:

问题:

●用户层如何把系统调用号给操作系统? 寄存器(如EAX)

●操作系统如何把返回值给用户? 寄存器或用户传入的缓冲区地址

●系统调用的过程,其实就是先int 0x80、syscall陷入内核,本质就是触发软中断,CPU就会自动执行系统调用的处理方法,而这个方法会根据系统调用号,自动查表执行对应的方法

●系统调用号本质:数组下标

cpp 复制代码
// sys.h
// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
extern int sys_setup (); // 系统启动初始化设置函数。 (kernel/blk_drv/hd.c,71)
extern int sys_exit (); // 程序退出。 (kernel/exit.c, 137)
extern int sys_fork (); // 创建进程。 (kernel/system_call.s, 208)
extern int sys_read (); // 读⽂件。 (fs/read_write.c, 55)
extern int sys_write (); // 写⽂件。 (fs/read_write.c, 83)
extern int sys_open (); // 打开⽂件。 (fs/open.c, 138)
extern int sys_close (); // 关闭⽂件。 (fs/open.c, 192)
extern int sys_waitpid (); // 等待进程终⽌。 (kernel/exit.c, 142)
extern int sys_creat (); // 创建⽂件。 (fs/open.c, 187)
extern int sys_link (); // 创建⼀个⽂件的硬连接。 (fs/namei.c, 721)
extern int sys_unlink (); // 删除⼀个⽂件名(或删除⽂件)。 (fs/namei.c, 663)
extern int sys_execve (); // 执⾏程序。 (kernel/system_call.s, 200)
extern int sys_chdir (); // 更改当前⽬录。 (fs/open.c, 75)
extern int sys_time (); // 取当前时间。 (kernel/sys.c, 102)
extern int sys_mknod (); // 建⽴块/字符特殊⽂件。 (fs/namei.c, 412)
extern int sys_chmod (); // 修改⽂件属性。 (fs/open.c, 105)
extern int sys_chown (); // 修改⽂件宿主和所属组。 (fs/open.c, 121)
extern int sys_break (); // (-kernel/sys.c, 21)
extern int sys_stat (); // 使⽤路径名取⽂件的状态信息。 (fs/stat.c, 36)
extern int sys_lseek (); // 重新定位读/写⽂件偏移。 (fs/read_write.c, 25)
extern int sys_getpid (); // 取进程id。 (kernel/sched.c, 348)
extern int sys_mount (); // 安装⽂件系统。 (fs/super.c, 200)
extern int sys_umount (); // 卸载⽂件系统。 (fs/super.c, 167)
extern int sys_setuid (); // 设置进程⽤⼾id。 (kernel/sys.c, 143)
extern int sys_getuid (); // 取进程⽤⼾id。 (kernel/sched.c, 358)
extern int sys_stime (); // 设置系统时间⽇期。 (-kernel/sys.c, 148)
extern int sys_ptrace (); // 程序调试。 (-kernel/sys.c, 26)
extern int sys_alarm (); // 设置报警。 (kernel/sched.c, 338)
extern int sys_fstat (); // 使⽤⽂件句柄取⽂件的状态信息。(fs/stat.c, 47)
extern int sys_pause (); // 暂停进程运⾏。 (kernel/sched.c, 144)
extern int sys_utime (); // 改变⽂件的访问和修改时间。 (fs/open.c, 24)
extern int sys_stty (); // 修改终端⾏设置。 (-kernel/sys.c, 31)
extern int sys_gtty (); // 取终端⾏设置信息。 (-kernel/sys.c, 36)
extern int sys_access (); // 检查⽤⼾对⼀个⽂件的访问权限。(fs/open.c, 47)
extern int sys_nice (); // 设置进程执⾏优先权。 (kernel/sched.c, 378)
extern int sys_ftime (); // 取⽇期和时间。 (-kernel/sys.c,16)
extern int sys_sync (); // 同步⾼速缓冲与设备中数据。 (fs/buffer.c, 44)
extern int sys_kill (); // 终⽌⼀个进程。 (kernel/exit.c, 60)
extern int sys_rename (); // 更改⽂件名。 (-kernel/sys.c, 41)
extern int sys_mkdir (); // 创建⽬录。 (fs/namei.c, 463)
extern int sys_rmdir (); // 删除⽬录。 (fs/namei.c, 587)
extern int sys_dup (); // 复制⽂件句柄。 (fs/fcntl.c, 42)
extern int sys_pipe (); // 创建管道。 (fs/pipe.c, 71)
extern int sys_times (); // 取运⾏时间。 (kernel/sys.c, 156)
extern int sys_prof (); // 程序执⾏时间区域。 (-kernel/sys.c, 46)
extern int sys_brk (); // 修改数据段⻓度。 (kernel/sys.c, 168)
extern int sys_setgid (); // 设置进程组id。 (kernel/sys.c, 72)
extern int sys_getgid (); // 取进程组id。 (kernel/sched.c, 368)
extern int sys_signal (); // 信号处理。 (kernel/signal.c, 48)
extern int sys_geteuid (); // 取进程有效⽤⼾id。 (kenrl/sched.c, 363)
extern int sys_getegid (); // 取进程有效组id。 (kenrl/sched.c, 373)
extern int sys_acct (); // 进程记帐。 (-kernel/sys.c, 77)
extern int sys_phys (); // (-kernel/sys.c, 82)
extern int sys_lock (); // (-kernel/sys.c, 87)
extern int sys_ioctl (); // 设备控制。 (fs/ioctl.c, 30)
extern int sys_fcntl (); // ⽂件句柄操作。 (fs/fcntl.c, 47)
extern int sys_mpx (); // (-kernel/sys.c, 92)
extern int sys_setpgid (); // 设置进程组id。 (kernel/sys.c, 181)
extern int sys_ulimit (); // (-kernel/sys.c, 97)
extern int sys_uname (); // 显⽰系统信息。 (kernel/sys.c, 216)
extern int sys_umask (); // 取默认⽂件创建属性码。 (kernel/sys.c, 230)
extern int sys_chroot (); // 改变根系统。 (fs/open.c, 90)
extern int sys_ustat (); // 取⽂件系统信息。 (fs/open.c, 19)
extern int sys_dup2 (); // 复制⽂件句柄。 (fs/fcntl.c, 36)
extern int sys_getppid (); // 取⽗进程id。 (kernel/sched.c, 353)
extern int sys_getpgrp (); // 取进程组id,等于getpgid(0)。(kernel/sys.c, 201)
extern int sys_setsid (); // 在新会话中运⾏程序。 (kernel/sys.c, 206)
extern int sys_sigaction (); // 改变信号处理过程。 (kernel/signal.c, 63)
extern int sys_sgetmask (); // 取信号屏蔽码。 (kernel/signal.c, 15)
extern int sys_ssetmask (); // 设置信号屏蔽码。 (kernel/signal.c, 20)
extern int sys_setreuid (); // 设置真实与/或有效⽤⼾id。 (kernel/sys.c,118)
extern int sys_setregid (); // 设置真实与/或有效组id。 (kernel/sys.c, 51)

// 系统调⽤函数指针表。⽤于系统调⽤中断处理程序(int 0x80),作为跳转表。
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid
};

// 调度程序的初始化⼦程序。
void sched_init(void)
{
    ...
    // 设置系统调⽤中断⻔。
    set_system_gate(0x80, &system_call);
}

_system_call:
    cmp eax,nr_system_calls-1 ;// 调⽤号如果超出范围的话就在eax 中置-1 并退出。
    ja bad_sys_call
    push ds ;// 保存原段寄存器值。
    push es
    push fs
    push edx ;// ebx,ecx,edx 中放着系统调⽤相应的C 语⾔函数的调⽤参数。
    push ecx ;// push %ebx,%ecx,%edx as parameters
    push ebx ;// to the system call
    mov edx,10h ;// set up ds,es to kernel space
    mov ds,dx ;// ds,es 指向内核数据段(全局描述符表中数据段描述符)。
    mov es,dx
    mov edx,17h ;// fs points to local data space
    mov fs,dx ;// fs 指向局部数据段(局部描述符表中数据段描述符)。
;// 下⾯这句操作数的含义是:调⽤地址 = _sys_call_table + %eax * 4。参⻅列表后的说
明。
;// 对应的C 程序中的sys_call_table 在include/linux/sys.h 中,其中定义了⼀个包括72
个
;// 系统调⽤C 处理函数的地址数组表。
    call [_sys_call_table+eax*4]
    push eax ;// 把系统调⽤号⼊栈。
    mov eax,_current ;// 取当前任务(进程)数据结构地址??eax。
;// 下⾯97-100 ⾏查看当前任务的运⾏状态。如果不在就绪状态(state 不等于0)就去执⾏调度程
序。
;// 如果该任务在就绪状态但counter[??]值等于0,则也去执⾏调度程序。
    cmp dword ptr [state+eax],0 ;// state
    jne reschedule
    cmp dword ptr [counter+eax],0 ;// counter
    je reschedule
;// 以下这段代码执⾏从系统调⽤C 函数返回后,对信号量进⾏识别处理。
ret_from_sys_call:

●那为什么我们用的系统调用,从来没有见过int 0x80或syscall呢?都是直接调用上层的函数?

●那是因为Linux的gnu C标准库,给我们把几乎所有的系统调用全部封装了

●#define SYS_ify(syscall_name) _NR##syscall_name:是一个宏定义,用于将系统调用的名称转换为对应的系统调用号。如:SYS_ify(open)会被展开为__NR_open

●而系统调用号,不是glibc提供的,是内核提供的,内核提供系统调用入口展开函数man 2 syscall,或者直接提供汇编级别软中断命令int or syscall,并提供对应的头文件或开发入口,让上层语言的设计者使用系统调用号,完成系统调用过程

cpp 复制代码
源代码路径:linux-2.6.18\linux-2.6.18\include\asm-x86_64\unistd.h
/* at least 8 syscall per cacheline */
#define __NR_read 0
__SYSCALL(__NR_read, sys_read)
#define __NR_write 1
__SYSCALL(__NR_write, sys_write)
#define __NR_open 2
__SYSCALL(__NR_open, sys_open)
#define __NR_close 3
__SYSCALL(__NR_close, sys_close)
#define __NR_stat 4
__SYSCALL(__NR_stat, sys_newstat)
#define __NR_fstat 5
__SYSCALL(__NR_fstat, sys_newfstat)
#define __NR_lstat 6
__SYSCALL(__NR_lstat, sys_newlstat)
#define __NR_poll 7
__SYSCALL(__NR_poll, sys_poll)

#define __NR_lseek 8
__SYSCALL(__NR_lseek, sys_lseek)
#define __NR_mmap 9
__SYSCALL(__NR_mmap, sys_mmap)
#define __NR_mprotect 10
__SYSCALL(__NR_mprotect, sys_mprotect)
#define __NR_munmap 11
__SYSCALL(__NR_munmap, sys_munmap)
#define __NR_brk 12
__SYSCALL(__NR_brk, sys_brk)
#define __NR_rt_sigaction 13
__SYSCALL(__NR_rt_sigaction, sys_rt_sigaction)
#define __NR_rt_sigprocmask 14
__SYSCALL(__NR_rt_sigprocmask, sys_rt_sigprocmask)
#define __NR_rt_sigreturn 15
__SYSCALL(__NR_rt_sigreturn, stub_rt_sigreturn)

...

或部分版本在glibc中,库函数调用实现方式:

cpp 复制代码
# define INTERNAL_SYSCALL_NCS(name, err, nr, args...) \
({ \
    unsigned long int resultvar; \
    LOAD_ARGS_##nr (args) \
    LOAD_REGS_##nr \
    asm volatile ( \
    "syscall\n\t" \
    : "=a" (resultvar) \
    : "0" (name) ASM_ARGS_##nr : "memory", "cc", "r11", "cx"); \
    (long int) resultvar; })

4.3.5缺页中断,内存碎片处理,除零野指针错误

cpp 复制代码
void trap_init(void)
{
    int i;
    set_trap_gate(0,&divide_error);// 设置除操作出错的中断向量值。以下雷同。
    set_trap_gate(1,&debug);
    set_trap_gate(2,&nmi);
    set_system_gate(3,&int3); /* int3-5 can be called from all */
    set_system_gate(4,&overflow);
    set_system_gate(5,&bounds);
    set_trap_gate(6,&invalid_op);
    set_trap_gate(7,&device_not_available);
    set_trap_gate(8,&double_fault);
    set_trap_gate(9,&coprocessor_segment_overrun);
    set_trap_gate(10,&invalid_TSS);
    set_trap_gate(11,&segment_not_present);
    set_trap_gate(12,&stack_segment);
    set_trap_gate(13,&general_protection);
    set_trap_gate(14,&page_fault);
    set_trap_gate(15,&reserved);
    set_trap_gate(16,&coprocessor_error);
    // 下⾯将int17-48 的陷阱⻔先均设置为reserved,以后每个硬件初始化时会重新设置⾃⼰的陷阱
    ⻔。
    for (i=17;i<48;i++)
        set_trap_gate(i,&reserved);
    set_trap_gate(45,&irq13);// 设置协处理器的陷阱⻔。
    outb_p(inb_p(0x21)&0xfb,0x21);// 允许主8259A 芯⽚的IRQ2 中断请求。
    outb(inb_p(0xA1)&0xdf,0xA1);// 允许从8259A 芯⽚的IRQ13 中断请求。
    set_trap_gate(39,&parallel_interrupt);// 设置并⾏⼝的陷阱⻔。
}

●缺页中断,内存碎片处理,除零野指针错误,这些问题,全部都会被转化称CPU内部的软中断,然后走中断处理例程,完成所有处理。有的是进行申请内存,填充页表,进行映射的。有的是用来处理内存碎片的,有的是用来给目标进行发送信号,杀掉进程等

所以:

●操作系统就是趟在中断处理例程上的代码块

●CPU内部的软中断,比如int 0x80或者syscall,我们叫做陷阱

●CPU内部的软中断,比如除零/野指针等,我们叫做异常

4.4如何理解用户态和内核态

结论:

●操作系统无论怎么切换进程,都能找到同一个操作系统。即操作系统调用发法的执行,是在进程的地址空间中执行的

●用户态就是执行用户0,3GB时所处的状态

●内核态就是执行内核3,4GB时所处的状态

●区分就是按照CPU内的CPL决定,CPL(Current Privilege Level),即当前特权级别

●一般执行int 0x80或syscall软中断,CPL会在校验之后自动变更

相关推荐
开开心心就好1 小时前
免安装PDF盖章工具,透明度和位置可调
运维·服务器·前端·javascript·pdf·ocr·1024程序员节
whyutianict_vv3 小时前
云计算 HCIE 认证体系解析:HCIA/HCIP/HCIE 三级科目与知识边界
linux·运维·云计算
0+11110 小时前
Linux --从 poll 到 epoll多路转接
linux·服务器·1024程序员节
青瓦梦滋10 小时前
WebSocket协议
linux·服务器·网络·c++·websocket·网络协议·http
桌面运维家10 小时前
旧电脑跑不动课程软件,VDI云桌面怎样接着用?
运维·服务器·虚拟化·pve·vdi·云桌面
zhang1338308907510 小时前
传统机械三杯式风速传感器易磨损冻堵?CG-88金属超声波风速风向传感器给出免运维方案
运维·服务器·网络·人工智能·自动化
知已难寻11 小时前
第 2 篇 · 云的地基:服务器、存储、网络三大基础设施
运维·服务器·网络·云原生·云计算·负载均衡·1024程序员节
2601_9498477513 小时前
bcrypt.js 跨端三方库仓颉适配全流程
linux·运维·ubuntu
m0_6399069413 小时前
Linux 操作系统入门:目录结构、服务排查常用命令、Shell 脚本基础
linux·运维·后端