目录

1.信号快速认识

1-1 ⽣活⻆度的信号

1-2 技术应⽤⻆度的信号

1-2-1 ⼀个样例

1-2-2 ⼀个系统函数

1-3 信号概念

1-3-1 查看信号

1-3-2 信号处理

2. 产⽣信号

2-1 通过终端按键产⽣信号

2-1-1 基本操作

2-1-2 理解OS如何得知键盘有数据

2-1-3 初步理解信号起源

2-2 调⽤系统命令向进程发信号

2-3 使⽤函数产⽣信号

2-3-1 kill

2-3-2 raise

2-3-3 abort

2-4 由软件条件产⽣信号

2-4-1 基本alarm验证-体会IO效率问题

2-4-2 设置重复闹钟

2-4-3 如何理解软件条件

2-4-4 如何简单快速理解系统闹钟

2-5 硬件异常产⽣信号

2-5-1 模拟除0

2-5-2 模拟野指针

2-5-3 ⼦进程退出core dump

2-5-4 Core Dump

2-6 总结思考⼀下

3. 保存信号

3-1 信号其他相关常⻅概念

3-2 在内核中的表⽰

3-3 sigset_t

3-4 信号集操作函数

3-4-1 sigprocmask

3-4-2 sigpending

4. 捕捉信号

4-1 信号捕捉的流程

4-2 sigaction

4-3 穿插话题 - 操作系统是怎么运行的

4-3-1 硬件中断

4-3-2 时钟中断

4-3-3 死循环

4-3-4 软中断

4-3-5 缺⻚中断?内存碎⽚处理?除零野指针错误?

4-4 如何理解内核态和用户态

5. 可重⼊函数

6. volatile

7. SIGCHLD信号 


1.信号快速认识

1-1 生活角度的信号

        -你在⽹上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
        -当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的⾏为并不是⼀定要⽴即执⾏,可以理解成“在合适的时候去取”。
        -在收到通知,再到你拿到快递期间,是有⼀个时间窗⼝的,在这段时间,你并没有拿到快递,但是你知道有⼀个快递已经来了。本质上是你“记住了有⼀个快递要去取”
        -当你时间合适,顺利拿到快递之后,就要开始处理快递了。⽽处理快递⼀般⽅式有三种:1. 执⾏默 认动作(幸福的打开快递,使⽤商品)2. 执⾏⾃定义动作(快递是零⻝,你要送给你你的⼥朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开⼀把游戏)
        -快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
📌 基本结论:
        你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
        信号产⽣之后,你知道怎么处理吗?知道。如果信号没有产⽣,你知道怎么处理信号吗?知道。所以,信号的处理⽅法,在信号产⽣之前,已经准备好了。
        处理信号,⽴即处理吗?我可能正在做优先级更⾼的事情,不会⽴即处理?什么时候?合适的时候。
        信号到来 | 信号保存 | 信号处理
        怎么进⾏信号处理啊?a.默认 b.忽略 c.⾃定义, 后续都叫做信号捕捉。

1-2 技术应用角度的信号

信号 != 信号量,两个无关

信号是一种给进程发送的,用来进行异步通知的机制

异步:两件事同时发生,互不干扰

1-2-1 ⼀个样例

# include <iostream>
# include <unistd.h>
int main ()
{
        while ( true )
        {
                std :: cout << "I am a process, I am waiting signal!" << std :: endl ;
                sleep( 1 );
        }
}
运行结果:
结论:
        ⽤⼾输⼊命令,在Shell下启动⼀个前台进程
        ⽤⼾按下 Ctrl+C ,这个键盘输⼊产⽣⼀个硬件中断,被OS获取,解释成信号,发送给⽬标前台进程
        前台进程因为收到信号,进⽽引起进程退出

1-2-2 ⼀个系统函数

参数说明:
        signum:信号编号 [ 后⾯解释,只需要知道是数字即可 ]
        handler:函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行 handler方
⽽其实, Ctrl+C 的本质是向前台进程发送 SIGINT 2 号信号,我们证明⼀下,这⾥需要引⼊⼀个系统调⽤函数。
 开始测试
# include <iostream>
# include <unistd.h>
# include <signal.h>
void handler ( int signumber)
{
        std::cout << " 我是 : " << getpid () << ", 我获得了⼀个信号 : " << signumber << std::endl;
}
int main ()
{
        std::cout << " 我是进程 : " << getpid () << std::endl;
        signal (SIGINT /*2*/ , handler);
        while ( true )
        {
                std::cout << "I am a process, I am waiting signal!" << std::endl;
                sleep ( 1 );
        }
}

📌 注意
        -要注意的是,signal函数仅仅是设置了特定信号的捕捉⾏为处理⽅式,并不是直接调⽤处理动作。如果后续特定信号没有产⽣,设置的捕捉函数永远也不会被调⽤!!
        -Ctrl-C 产⽣的信号只能发给前台进程。⼀个命令后⾯加个&可以放到后台运⾏,这样
        -Shell不必等待进程结束就可以接受新的命令,启动新的进程。
        -Shell可以同时运⾏⼀个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C
这种控制键产⽣的信号。
        -前台进程在运⾏过程中⽤⼾随时可能按下 Ctrl-C ⽽产⽣⼀个信号,也就是说该进程的⽤
⼾空间代码执⾏到任何地⽅都有可能收到 SIGINT 信号⽽终⽌,所以信号相对于进程的控
制流程来说是异步(Asynchronous)的。
        -关于进程间关系,我们在⽹络部分会专⻔来讲,现在就了解即可。
        -可以渗透 & 和 nohup

1-3 信号概念

信号是进程之间事件异步通知的⼀种⽅式,属于软中断。

1-3-1 查看信号

编号34以上的是实时信号,我们只讨论编号34以下的信号,不讨论实时信号。这些信号各⾃在什么条件下产⽣,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

1-3-2 信号处理

( sigaction 函数稍后详细介绍),可选的处理动作有以下三种:
1.忽略此信号
# include <iostream>
# include <unistd.h>
# include <signal.h>
void handler ( int signumber)
{
        std::cout << " 我是 : " << getpid () << ", 我获得了⼀个信号 : " << signumber << std::endl;
}
int main ()
{
        std::cout << " 我是进程 : " << getpid () << std::endl;
        signal (SIGINT /*2*/ , SIG_IGN); // 设置忽略信号的宏
        while ( true )
        {
                std::cout << "I am a process, I am waiting signal!" << std::endl;
                sleep ( 1 );
        }
}
Ctrl+c毫无反应
2.执行此信号的默认处理动作
# include <iostream>
# include <unistd.h>
# include <signal.h>
void handler ( int signumber)
{
        std::cout << " 我是 : " << getpid () << ", 我获得了⼀个信号 : " << signumber << std::endl;
}
int main ()
{
        std::cout << " 我是进程 : " << getpid () << std::endl;
        signal (SIGINT /*2*/ , SIG_DFL);
        while ( true )
        {
                std::cout << "I am a process, I am waiting signal!" << std::endl;

                sleep ( 1 );
        }
}
输入ctrl+c进程就退出,就是默认动作

3.提供⼀个信号处理函数,要求内核在处理该信号时切换到⽤⼾态执⾏这个处理函数,这种⽅式称为⾃定义捕捉(Catch)⼀个信号。(9号不能被自定义捕捉

// 就是开始的样例
注意看源码:
# define SIG_DFL ((__sighandler_t) 0) /* Default action. */
# define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
/* Type of a signal handler. */
typedef void (* __sighandler_t ) ( int );
// 其实 SIG_DFL SIG_IGN 就是把 0,1 强转为函数指针类型

2. 产⽣信号

当前阶段:

2-1 通过终端按键产⽣信号

2-1-1 基本操作

-Ctrl+C (SIGINT) 已经验证过,这⾥不再重复
-Ctrl+\(SIGQUIT)可以发送终⽌信号并⽣成core dump⽂件,⽤于事后调试

# include <iostream>
# include <unistd.h>
# include <signal.h>
void handler ( int signumber)
{
        std::cout << " 我是 : " << getpid () << ", 我获得了⼀个信号 : " << signumber << std::endl;
}
int main ()
{
        std::cout << " 我是进程 : " << getpid () << std::endl;
        signal (SIGQUIT /*3*/ , handler);
        while ( true )
        {
                std::cout << "I am a process, I am waiting signal!" << std::endl;
                sleep ( 1 );
        }
}
注释调signal的代码后输入ctrl+\:
-Ctrl+Z(SIGTSTP)可以发送停⽌信号,将 当前前台进程挂起到后台 等。
# include <iostream>
# include <unistd.h>
# include <signal.h>
void handler ( int signumber)
{
        std::cout << " 我是 : " << getpid () << ", 我获得了⼀个信号 : " << signumber << std::endl;
}
int main ()
{
        std::cout << " 我是进程 : " << getpid () << std::endl;
        signal (SIGTSTP /*20*/ , handler);
        while ( true )
        {
                std::cout << "I am a process, I am waiting signal!" << std::endl;
                sleep ( 1 );
        }
}
注释掉signal代码后输入ctrl+z的运行结果: 进程被挂起
关于前后台问题:
键盘只有一个,一定是传给前台进程的,前台进程只有一个后台进程可有多个。
后台进程无法从标准输入获取内容
前台进程可以从键盘获取标准输入
都可以从标准输出打印

2-1-2 理解OS如何得知键盘有数据

2-1-3 初步理解信号起源

📌 注意:
信号其实是从纯软件⻆度,模拟硬件中断的⾏为
只不过硬件中断是发给CPU,⽽信号是发给进程
两者有相似性,但是层级不同,这点我们后⾯的感觉会更加明显

2-2 调⽤系统命令向进程发信号

# include <iostream>
# include <unistd.h>
# include <signal.h>
int main ()
{
        while ( true )
        {
                sleep ( 1 );
        }
}
首先在后台执行死循环程序,然后使用kil命令给它发生SIGSEGV信号。就可以杀死进程,也可以直接发送: kill -11 1219287

2-3 使⽤函数产⽣信号

2-3-1 kill

kill 命令是调⽤ kill 函数实现的。 kill 函数可以给⼀个指定的进程发送指定的信号。
NAME
        kill - send signal to a process
SYNOPSIS
        # include <sys/types.h>
        # include <signal.h>
        int kill ( pid_t pid, int sig);
RETURN VALUE
        On success (at least one signal was sent), zero is returned. On error, -1 is returned, and errno is set appropriately.

实现自己的kill命令:

#include <iostream>

#include <sys/types.h>

#include <unistd.h>

#include <signal.h>

using namespace std;

// mykill -signumber pid

int main(int argc,char *argv[])

{

    if(argc != 3)

    {

        cerr << "Usage:" << argv[0] << " -signumber pid " << endl;

        return 1;

    }

    int number = stoi(argv[1]+1);

    pid_t pid = stoi(argv[2]);

    int n = kill(pid,number);

    return n;

}

2-3-2 raise

raise 函数可以给当前进程发送指定的信号(⾃⼰给⾃⼰发信号)。
NAME
        raise - send a signal to the caller
SYNOPSIS
        # include <signal.h>
        int raise ( int sig);
RETURN VALUE
        raise () returns 0 on success, and nonzero for failure.

实现raise命令:

#include <iostream>

#include <sys/types.h>

#include <unistd.h>

#include <signal.h>

using namespace std;

void handler(int signumber)

{

    cout << "获取了一个信号:" << signumber << endl;

}

int main()

{

    signal(SIGINT,handler);

    while(true)

    {

        sleep(1);

        raise(2);

    }

}

也就是自己给自己每隔一秒发送一个2号信号,所以会一直打印handler函数

2-3-3 abort

abort 函数使当前进程接收到信号⽽异常终⽌。
NAME
        abort - cause abnormal process termination
SYNOPSIS
        # include <stdlib.h>
        void abort ( void );
RETURN VALUE
        The abort () function never returns.
        // 就像 exit 函数⼀样 ,abort 函数总是会成功的 , 所以没有返回值。

#include <iostream>

#include <sys/types.h>

#include <unistd.h>

#include <signal.h>

using namespace std;

void handler(int signumber)

{

    cout << "获取了一个信号:" << signumber << endl;

}

int main()

{

    signal(SIGABRT,handler);

    while(true)

    {

        sleep(1);

        abort();

    }

}

abort给自己发送的是固定6号信号,虽然捕捉了,但是还是退出

2-4 由软件条件产⽣信号

SIGPIPE 是⼀种由软件条件产⽣的信号,在“管道”中已经介绍过了。本节主要介绍 alarm 函数
SIGALRM 信号。
NAME
        alarm - set an alarm clock for delivery of a signal
SYNOPSIS
        # include <unistd.h>
        unsigned int alarm ( unsigned int seconds);
RETURN VALUE
        alarm () returns the number of seconds remaining until any previously  scheduled alarm was due to be delivered, or zero if there was no previ
ously scheduled alarm.

-调⽤ alarm 函数可以设定⼀个闹钟,也就是告诉内核在 seconds 秒之后给当前进程发
SIGALRM 信号,该信号的默认处理动作是终⽌当前进程。
-这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个⽐⽅,某⼈要⼩睡⼀觉,设定闹钟为30分钟之后响,20分钟后被⼈吵醒了,还想多睡⼀会⼉,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表⽰取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。

2-4-1 基本alarm验证-体会IO效率问题

// IO
# include <iostream>
# include <unistd.h>
# include <signal.h>
int main ()
{
        int count = 0 ;
        alarm ( 1 );
        while ( true )
        {
                std::cout << "count : " << count << std::endl;
                count++;
        }
        return 0 ;
}
一秒的运行结果:
// IO
# include <iostream>
# include <unistd.h>
# include <signal.h>
int count = 0 ;
void handler ( int signumber)
{
        std::cout << "count : " << count << std::endl;
        exit ( 0 );
}
int main ()
{
        signal (SIGALRM, handler);
        alarm ( 1 );
        while ( true )
        {
                count++;
        }
        return 0 ;
}
一秒的运行结果:
📌 结论:
        闹钟会响⼀次,默认终⽌进程
        有IO效率低

2-4-2 设置重复闹钟

#include <functional>

#include <iostream>

#include <signal.h>

#include <unistd.h>

#include <vector>

////////////func//////////

void Sched()

{

    std::cout << "我是进程调度" <<std::endl;

}

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++;//记录时间也记录次数,前提外部是固定时间间隔刺激OS

    std::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();

    }

    return 0;

}

运行结果:每隔一秒进程执行一次func任务

📌 结论:
        闹钟设置⼀次,起效⼀次
        重复设置的⽅法

2-4-3 如何理解软件条件

在操作系统中,信号的软件条件指的是由软件内部状态或特定软件操作触发的信号产⽣机制。这些条件包括但不限于定时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产⽣的SIGPIPE信号)等。当这些软件条件满⾜时,操作系统会向相关进程发送相应的信号,以通知进程进⾏相应的处理。简⽽⾔之,软件条件是因操作系统内部或外部软件操作⽽触发的信号产⽣。

2-4-4 如何简单快速理解系统闹钟

系统闹钟,其实本质是OS必须⾃⾝具有定时功能,并能让⽤⼾设置这种定时功能,才可能实现闹钟这样的技术。
现代Linux是提供了定时功能的,定时器也要被管理:先描述,在组织。内核中的定时器数据结构是:
struct timer_list {
        struct list_head entry ;
        unsigned long expires;
        void (*function)( unsigned long );
        unsigned long data;
        struct tvec_t_base_s * base ;
};

我们不在这部分进⾏深究,为了理解它,我们可以看到:定时器超时时间expires和处理⽅法
function。
操作系统管理定时器,采⽤的是时间轮的做法,但是我们为了简单理解,可以把它在组织成为"堆结构"。
闹钟的理解:堆(最小堆)

2-5 硬件异常产⽣信号

硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执⾏了除以0的指令, CPU的运算单元会产⽣异常, 内核将这个异常解释为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址, MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送给进程。

2-5-1 模拟除0

# include <stdio.h>
# include <signal.h>
void handler ( int sig)
{
        printf ( "catch a sig : %d\n" , sig);
}
int main ()
{
         signal(SIGFPE, handler); // 8) SIGFPE
        sleep( 1 );
        int a = 10 ;
        a/= 0 ;
        while ( 1 );
        return 0 ;
}
分母不能为0

2-5-2 模拟野指针

# include <stdio.h>
# include <signal.h>
void handler ( int sig)
{
        printf ( "catch a sig : %d\n" , sig);
}
int main ()
{
         signal(SIGSEGV, handler);
        sleep( 1 );
        int *p = NULL ;
        *p = 100 ;
        while ( 1 );
        return 0 ;
}
由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层⾯上,是被当成信号处理的。
📌 注意:
通过上⾯的实验,我们可能发现:
发现⼀直有8号信号产⽣被我们捕获,这是为什么呢?上⾯我们只提到CPU运算异常后,如何处理后续的流程,实际上 OS 会检查应⽤程序的异常情况,其实在CPU中有⼀些控制和状态寄存器,主要⽤于控制处理器的操作,通常由操作系统代码使⽤。状态寄存器可以简单理解为⼀个位图,对应着⼀些状态标记位、溢出标记位。OS 会检测是否存在异常状态,有异常存在就会调⽤对应的异常处理⽅法。
除零异常后,我们并没有清理内存,关闭进程打开的⽂件,切换进程等操作,所以CPU中还
保留上下⽂数据以及寄存器内容,除零异常会⼀直存在,就有了我们看到的⼀直发出异常信
号的现象。访问⾮法内存其实也是如此,⼤家可以⾃⾏实验。

2-5-3 ⼦进程退出core dump

除0代码示例:

# include <iostream>
# include <string>
# include <unistd.h>
# include <stdlib.h>
# include <signal.h>
# include <sys/wait.h>
int main ()
{
        if (fork() == 0 )
        {
                sleep( 1 );
                int a = 10 ;
                a /= 0 ;
                exit ( 0 );
        }
        int status = 0 ;
        waitpid( -1 , &status, 0 );
        printf ( "exit signal: %d, core dump: %d\n" , status& 0x7F , (status>> 7 )& 1 );
        return 0 ;
}
运行结果:

man 7 signal:查看 Linux 信号的权威手册

ulimit -a:查看当前 Shell 的资源限制

2-5-4 Core Dump

        -SIGINT的默认处理动作是终⽌进程,SIGQUIT的默认处理动作是终⽌进程并且Core Dump,现在我们来验证⼀下。
        -⾸先解释什么是Core Dump。当⼀个进程要异常终⽌时,可以选择把进程的⽤⼾空间内存数据全部保存到磁盘上,⽂件名通常是core,这叫做Core Dump。
        -进程异常终⽌通常是因为有Bug,⽐如⾮法内存访问导致段错误,事后可以⽤调试器检查core⽂件以查清错误原因,这叫做 Post-mortem Debug (事后调试)。
        -⼀个进程允许 产⽣多⼤的 core ⽂件取决于进程的 Resource Limit (这个信息保存 在PCB 中)。默认是不允许产⽣ core ⽂件的, 因为 core ⽂件中可能包含⽤⼾密码等敏感信息,不安全。
        -在开发调试阶段可以⽤ ulimit 命令改变这个限制,允许产⽣ core ⽂件。 ⾸先⽤ ulimit 命令改变 Shell 进程的 Resource Limit ,如允许 core ⽂件最⼤为 1024K: $ ulimit -c 1024
写一个死循环程序:验证

int main()

{

    printf("getpid:%d\n",getpid());

    while(1);

    return 0;

}

然后ctrl+c或者ctrl+\:

ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制⽽来,所以也具 有和Shell进程相同的Resource Limit值,这样就可以产⽣Core Dump了。 使⽤core⽂件:

2-6 总结思考⼀下

上⾯所说的所有信号产⽣,最终都要有OS来进⾏执⾏,为什么? OS是进程的管理者
信号的处理是否是⽴即处理的? 在合适的时候
信号如果不是被⽴即处理,那么信号是否需要暂时被进程记录下来? 记录在哪⾥最合适呢?
⼀个进程在没有收到信号的时候,能否能知道,⾃⼰应该对合法信号作何处理呢?
如何理解OS向进程发送信号?能否描述⼀下完整的发送处理过程?

3. 保存信号

当前阶段:

3-1 信号其他相关常见概念

实际执⾏信号的 处理动作称为信号递达( Delivery)
信号从 产⽣到递达之间 的状态,称为 信号未决 (Pending)。
进程可以选择 阻塞 (Block )某个信号。
被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作。

3-2 在内核中的表示

        -每个信号都有两个标志位分别表⽰阻塞(block)和未决(pending),还有⼀个函数指针表⽰处理动作。信号产⽣时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例⼦中,SIGHUP信号未阻塞也未产⽣过,当它递达时执⾏默认处理动作。
        -SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
        -SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,它的处理动作是⽤⼾⾃定义函数sighandler。
        如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理?POSIX.1允许系统递送该信号⼀次或多次。Linux是这样实现的:常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。
// 内核结构 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来存储, , 这个类型
可以表⽰每个信号的“有效”或“⽆效”状态, 在阻塞信号集中“有效”和“⽆效”的含义是该信号
是否被阻塞, ⽽在未决信号集中“有 效”和“⽆效”的含义是该信号是否处于未决状态。下⼀节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的 这⾥的“屏蔽”应该理解为阻塞⽽不是忽略。

3-4 信号集操作函数

sigset_t类型对于每种信号⽤⼀个bit表⽰“有效”或“⽆效”状态, ⾄于这个类型内部如何存储这些
bit则依赖于系统实现, 从使⽤者的⻆度是不必关⼼的, 使⽤者只能调⽤以下函数来操作sigset_ t变量,⽽不应该对它的内部数据做任何解释, ⽐如⽤printf直接打印sigset_t变量是没有意的。
代码块 # 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。

3-4-1 sigprocmask

调⽤函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。
代码块
# 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参数的可选值。
宏定义 含义 等效操作(假设当前屏蔽字为 mask
SIG_BLOCK set 中的信号添加到当前屏蔽字 `mask = mask set`
SIG_UNBLOCK set 中的信号从当前屏蔽字中移除 mask = mask & ~set
SIG_SETMASK 将当前屏蔽字设置set 指向的值 mask = set
如果调⽤sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀个信号递达。

3-4-2 sigpending

# include <signal.h>
int sigpending ( sigset_t * set );
读取当前进程的未决信号集 , 通过 set 参数传出。
调⽤成功则返回 0 , 出错则返回 -1
实验:

#include <iostream>

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

#include <sys/wait.h>

using namespace std;

void PrintPending(sigset_t &pending)

{

    cout << "curr process[" << getpid() << "]pending:";

    for(int i = 31;i > 0;i--)

    {

        if(sigismember(&pending,i))

            cout << 1;

        else

            cout << 0;

    }

    cout << endl;

}

void handler(int sig)

{

    cout << "信号被递达!!!" << endl;

    sigset_t pending;

    sigpending(&pending);

    PrintPending(pending);

}

int main()

{

    //1.捕捉2号信号

    signal(2,handler);

    //2.屏蔽2号信号

    sigset_t block_set,old_set;

    sigemptyset(&block_set);

    sigemptyset(&old_set);

    sigaddset(&block_set,SIGINT);//给block_set信号集添加要屏蔽的2号信号

    sigprocmask(SIG_BLOCK,&block_set,&old_set);//修改内核中当前进程的 block 表(信号屏蔽字)

    int cnt = 15;

    while(true)

    {

        //3.获取当前进程的信号集

        sigset_t pending;

        sigpending(&pending);

        //4.打印pending信号集

        PrintPending(pending);

        cnt--;

        //5.解除对2号信号的屏蔽

        if(cnt == 0)

        {

            cout << "解除对2号信号的屏蔽!" << endl;

            sigprocmask(SIG_BLOCK,&old_set,&block_set);

        }

        sleep(1);

    }

    return 0;

}

运行结果:进程ctrl+c之后就修改了pending表

4. 捕捉信号

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);
        -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字段包含⼀些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数。

4-3 穿插话题 - 操作系统是怎么运行的

4-3-1 硬件中断

-中断向量表就是操作系统的⼀部分,启动就加载到内存中了
-通过外部硬件中断,操作系统就不需要对外设进⾏任何周期性的检测或者轮询
-由外部设备触发的,中断系统运⾏流程,叫做硬件中断
//Linux 内核 0.11 源码
void trap_init ( void )
{
        int i;
        set_trap_gate( 0 ,÷_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 ,¶llel_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 时钟中断

问题
-进程可以在操作系统的指挥下,被调度,被执⾏,那么操作系统⾃⼰被谁指挥,被谁推动执⾏呢
-外部设备可以触发硬件中断,但是这个是需要⽤⼾或者设备⾃⼰触发,有没有⾃⼰可以定期触发的设备?
-这样,操作系统不就在硬件的推动下,⾃动调度了么!!!
// 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 死循环

如果是这样,操作系统不就可以躺平了吗?对,操作系统⾃⼰不做任何事情,需要什么功能,就向中断向量表⾥⾯添加⽅法即可.操作系统的本质:就是⼀个死循环!
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主动完成的中断

        -上述外部硬件中断,需要硬件设备触发。
        -有没有可能,因为软件原因,也触发上⾯的逻辑?有! 
        -为了让操作系统⽀持进⾏系统调⽤,CPU也设计了对应的汇编指令(int 或者 syscall),可以让CPU内部触发中断逻辑。
所以:
// 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, 10 h ; // set up ds,es to kernel space

        mov ds,dx ; // ds,es 指向内核数据段 ( 全局描述符表中数据段描述符 )
        mov es,dx
        mov edx, 17 h ; // 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 ,并提供对应的头⽂件或者开
发⼊⼝,让上层语⾔的设计者使⽤系统调⽤号,完成系统调⽤过程
源代码路径 linux-2.6.18\linux-2.6.18\include\asm-x86_64\unistd.
/* 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中,库函数调⽤实现⽅式:
# 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 缺⻚中断?内存碎⽚处理?除零野指针错误?

void trap_init ( void )
{
        int i;
        set_trap_gate( 0 ,÷_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 ,¶llel_interrupt); // 设置并⾏⼝的陷阱⻔。
}
缺⻚中断?内存碎⽚处理?除零野指针错误 ?这些问题,全部都会被转换成为CPU内部的软中断,然后⾛中断处理例程,完成所有处理。有的是进⾏申请内存,填充⻚表,进⾏映射的。有的是⽤来处理内存碎⽚的,有的是⽤来给⽬标进⾏发送信号,杀掉进程等等。
📌 所以:
        操作系统就是躺在中断处理例程上的代码块!
        CPU内部的软中断,⽐如int 0x80或者syscall,我们叫做 陷阱 ---主动中断
        CPU内部的软中断,⽐如除零/野指针等,我们叫做 异常。(所以,能理解“缺⻚异
常”为什么这么叫了吗?) ---被动中断

4-4 如何理解内核态和用户态

结论:
        -操作系统⽆论怎么切换进程,都能找到同⼀个操作系统!换句话说操作系统系统调⽤⽅法的执⾏,是在进程的地址空间中执⾏的!
        -关于特权级别,涉及到段,段描述符,段选择⼦,DPL,CPL,RPL等概念, ⽽现在芯⽚为了保证兼容性,已经⾮常复杂了,进⽽导致OS也必须得照顾它的复杂性,这块我们不做深究了。
        -⽤⼾态就是执⾏⽤⼾[0,3]GB时所处的状态
        -内核态就是执⾏内核[3,4]GB时所处的状态
        -区分就是按照CPU内的CPL决定,CPL的全称是Current Privilege Level,即当前特权级别。
        -⼀般执⾏ int 0x80 或者 syscall 软中断,CPL会在校验之后⾃动变更
        -这样会不会不安全??

5. 可重⼊函数

        -main函数调⽤insert函数向⼀个链表head中插⼊节点node1,插⼊操作分为两步,刚做完第⼀步的时候,因为硬件中断使进程切换到内核,再次回⽤⼾态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调⽤insert函数向同⼀个链表head中插⼊节点node2,插⼊操作的两步都做完之后从sighandler返回内核态,再次回到⽤⼾态就从main函数调⽤的insert函数中继续往下执⾏,先前做第⼀步之后被打断,现在继续做完第⼆步。结果是,main函数和sighandler先后 向链表中插⼊两个节点,⽽最后只有⼀个节点真正插⼊链表中了。
        -像上例这样,insert函数被不同的控制流程调⽤,有可能在第⼀次调⽤还没返回时就再次进⼊该函数,这称为重⼊,insert函数访问⼀个全局链表,有可能因为重⼊⽽造成错乱,像这样的函数称为 不可重⼊函数,反之,如果⼀个函数只访问⾃⼰的局部变量或参数,则称为可重⼊(Reentrant) 函数。想⼀下,为什么两个不同的控制流程调⽤同⼀个函数,访问它的同⼀个局部变量或参数就不会造成错乱?
如果⼀个函数符合以下条件之⼀则是不可重⼊的:
-调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的。
-调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构。

6. volatile

该关键字在C当中我们已经有所涉猎,今天我们站在信号的⻆度重新理解⼀下
# include <stdio.h>
# include <signal.h>
int flag = 0 ;
void handler ( int sig)
{
        printf ( "chage flag 0 to 1\n" );
        flag = 1 ;
}
int main ()
{
        signal( 2 , handler);
        while (!flag);
        printf ( "process quit normal\n" );
        return 0 ;
}
Makefile:
sig:sig.c
        gcc -o sig sig.c 
.PHONY:clean
clean:
        rm -f sig

标准情况下的运行结果键⼊ CTRL-C ,2号信号被捕捉,执⾏⾃定义动作,修改 flag1 , while 条件不满⾜, 退出循环,进程退出

# include <stdio.h>
# include <signal.h>
int flag = 0 ;
void handler ( int sig)
{
        printf ( "chage flag 0 to 1\n" );
        flag = 1 ;
}
int main ()
{
        signal( 2 , handler);
        while (!flag);
        printf ( "process quit normal\n" );
        return 0 ;
}
Makefile:
sig:sig.c
        gcc -o sig sig.c -02
.PHONY:clean
clean:
        rm -f sig

优化情况下优化情况下,键⼊ CTRL-C ,2号信号被捕捉,执⾏⾃定义动作,修改 flag1 ,但是 while 条件依旧满⾜,进程继续运⾏!但是很明显flag肯定已经被修改了,但是为何循环依旧执⾏?很明显,while 循环检查的 flag,并不是内存中最新的 flag,这就存在了数据⼆异性的问题。 while 测的 flag 其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile

# include <stdio.h>
# include <signal.h>
volatile int flag = 0 ;
void handler ( int sig)
{
        printf ( "chage flag 0 to 1\n" );
        flag = 1 ;
}
int main ()
{
        signal( 2 , handler);
        while (!flag);
        printf ( "process quit normal\n" );
        return 0 ;
}

Makefile:
sig:sig.c
        gcc -o sig sig.c -02
.PHONY:clean
clean:
        rm -f sig
volatile 作⽤ :保持内存的可⻅性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进⾏操作

7. SIGCHLD信号 

进程⼀章讲过⽤wait和waitpid函数清理僵⼫进程,⽗进程可以阻塞等待⼦进程结束,也可以⾮阻 塞地查 询是否有⼦进程结束等待清理(也就是轮询的⽅式)。采⽤第⼀种⽅式,⽗进程阻塞了就不 能处理⾃⼰的⼯作了;采⽤第⼆种⽅式,⽗进程在处理⾃⼰的⼯作的同时还要记得时不时地轮询⼀ 下,程序实现复杂。
其实,⼦进程在终⽌时会给⽗进程发SIGCHLD信号,该信号的默认处理动作是忽略,⽗进程可以⾃ 定义SIGCHLD信号的处理函数,这样⽗进程只需专⼼处理⾃⼰的⼯作,不必关⼼⼦进程了,⼦进程 终⽌时会通知⽗进程,⽗进程在信号处理函数中调⽤wait清理⼦进程即可。
请编写⼀个程序完成以下功能:⽗进程fork出⼦进程,⼦进程调⽤exit(2)终⽌,⽗进程⾃定 义SIGCHLD信号的处理函数, 在其中调⽤wait获得⼦进程的退出状态并打印。
事实上,由于UNIX 的历史原因,要想不产⽣僵⼫进程还有另外⼀种办法:⽗进程调 ⽤sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的⼦进程在终⽌时会⾃动清理掉,不 会产⽣僵⼫进程,也不会通知⽗进程。系统默认的忽略动作和⽤⼾⽤sigaction函数⾃定义的忽略 通常是没有区别的,但这是⼀个特例。此⽅法对于Linux可⽤,但不保证在其它UNIX系统上都可 ⽤。请编写程序验证这样做不会产⽣僵⼫进程。
内核层面的默认忽略和显示设置SIG_IGN忽略是两个不同的逻辑
测试代码:
# include <stdio.h>
# include <stdlib.h>
# include <signal.h>
void handler ( int sig)
{
        pid_t id;
        while ( (id = waitpid( -1 , NULL , WNOHANG)) > 0 )
        {
                printf ( "wait child success: %d\n" , id);
        }
        printf ( "child is quit! %d\n" , getpid());
}
int main ()
{
        signal(SIGCHLD, handler);
        pid_t cid;
        if ((cid = fork()) == 0 )
        {
                 //child
                printf ( "child : %d\n" , getpid());
                sleep( 3 );
                exit ( 1 );
        }
        while ( 1 )
        {
                printf ( "father proc is doing some thing!\n" );
                sleep( 1 );
        }
        return 0 ;
}
运行结果:
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐