嵌入式Hacker (es-hacker)

Embedded bsp developer enjoys thinking and hacking opensource and develop boards(NanoPi, LicheePi, RPi...)

0%

Linux系统编程-信号机制入门2

早,继续记录我的学习心得。

机械的练习:
只是埋头干!我刚刚挥起球拍,努力去击球。我刚刚听到了那些数字,想办法去记住。我刚刚看到了那些数学题,正试着解答。

有准确目的的练习:
意味着要比机械的练习更有目的性,考虑更周全,从而让自己变得更专注。

以玩模拟赛车 acc 为例,怎么样才算是有准确目的的练习:

  • 比较明确的目标: 每天跑圈提升 0.5 秒。
  • 更明确的目标: 过某个弯提升多少速度。
  • 更更明确的目标:过某个弯时应在哪里刹车、应如何控制刹车/油门/方向盘,怎么走线等。

Linux系统编程-信号机制入门2

写作目的:

  • 举例说明 Linux 信号集 (Signal Set)、信号屏蔽字 (Signal Mask) 相关的知识点。

  • 分享一个《刻意练习》里提到的概念:心理表征 (Mental Representations)。

正文目录:

1
一、信号集 (Signal Set)
2
二、信号屏蔽字 (Signal Mask)
3
    2.1 基础概念
4
    2.2 实验 demo
5
三、待处理的信号 (Pending Signals)
6
四、相关参考
7
五、如何高效学习:心理表征 (Mental Representations)
8
六、欢迎关注我的公众号 (嵌入式Hacker)

一、信号集 (Signal Set)

多个信号可使用一个称之为信号集的数据结构来表示,POSIX.1 定义了数据类型 sigset_t 以表示一个信号集,并且定义了下列 5 个处理信号集的函数:

1
$ man 3 sigemptyset
2
NAME
3
       sigemptyset, sigfillset, sigaddset, sigdelset, sigismember - POSIX signal set operations
4
5
SYNOPSIS
6
       #include <signal.h>
7
8
       int sigemptyset(sigset_t *set);
9
       int sigfillset(sigset_t *set);
10
       int sigaddset(sigset_t *set, int signum);
11
       int sigdelset(sigset_t *set, int signum);
12
       int sigismember(const sigset_t *set, int signum);
  • 函数 sigemptyset() 初始化由参数 set 指向的信号集,清除其中所有信号。

  • 函数 sigfillset() 初始化由参数 set 指向的信号集,使其包括所有信号。

  • 必须使用 sigemptyset() 或者 sigfillset() 来初始化信号集。
    这是因为 C 语言不会对自动变量进行初始化,并且,借助于将静态变量初始化为 0 的机制来表示空信号集的作法在可移植性上存在问题,因为有可能使用位掩码之外的结构来实现信号集。

  • 函数 sigaddset() 将一个信号添加到已有的信号集中,sigdelset() 则从信号集中删除一个信号。

  • sigismember() 函数用来测试信号 sig 是否是信号集 set 的成员。

二、信号屏蔽字 (Signal Mask)

2.1 基础概念

每个进程都有一个信号屏蔽字(或称信号掩码,signal mask),它规定了当前要阻塞递送到该进程的信号集。对于每种信号,屏蔽字中都有一位与之对应。对于某种信号,若其对应位被设置,则它当前是被阻塞的。进程可以调用 sigprocmask() 检测或更改,或同时进行检测和更改进程的信号屏蔽字。

向信号屏蔽字中添加信号的3种方式:

  • 当调用信号处理器 (signal handler) 时,可能会引发信号自动添加到信号屏蔽字中的行为,暂不作深入介绍。

  • 使用 sigaction() 函数建立信号处理器时,可以指定一组信号集,当调用该处理器时会将该信号集里的信号阻塞,暂不作深入介绍。

  • 使用sigprocmask()系统调用,可以随时显式地向信号屏蔽字中添加或移除信号。

先来了解 sigprocmask():

1
$ man 2 sigprocmask
2
#include <signal.h>
3
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

相关知识点:

  • sigprocmask() 既可用于修改 进程的信号屏蔽字,也可用于获取现有的屏蔽字,或者同时执行这2个操作。

  • 参数 how 指定了 sigprocmask() 该如何操作信号屏蔽字。

    • SIG_BLOCK: 将参数 set 信号集内的信号添加到信号屏蔽字中;
    • SIG_UNBLOCK: 将参数 set 信号集内的信号从信号屏蔽字中移除;
    • SIG_SETMASK: 将参数 set 信号集赋给信号屏蔽字。
  • 若 set 参数不为空,则其指向一个 sigset_t 缓冲区,用于返回之前的信号屏蔽字。

  • SUSv3 规定,如果有任何正在等待的信号 (pending signals) 因调用了 sigprocmask() 解除了锁定,那么在此调用返回前至少会传递一次这些信号。

  • 系统将忽略试图阻塞 SIGKILL 和 SIGSTOP 信号的请求。如果试图阻塞这些信号,sigprocmask() 既不会予以关注,也不会产生错误。

  • 常见的使用方法:

    1
    sigset_t blockSet, prevMask;
    2
    sigemptyset(&blockSet);
    3
    4
    /* 1. Block SIGINT, save previous signal mask */
    5
    sigaddset(&blockSet, SIGINT);
    6
    if (sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1)
    7
        errExit("sigprocmask1");
    8
    9
    /* 2. Code that should not be interrupted by SIGINT */
    10
    11
    /* 3. Restore previous signal mask, unblocking SIGINT */
    12
    if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1)
    13
        errExit("sigprocmask2");

2.2 实验 demo

main() 函数:

1> 为所有信号注册同一个信号处理函数,用于验证信号集是否被成功屏蔽:

1
static void handler(int sig)
2
{
3
    if (sig == SIGINT)
4
        gotSigint = 1;
5
    else
6
        sigCnt[sig]++;
7
}
8
9
int main(int argc, char *argv[])
10
{
11
    int n, numSecs;
12
    sigset_t fullMask, emptyMask;
13
14
    printf("%s: PID is %ld\n", argv[0], (long) getpid());
15
16
    for (n = 1; n < NSIG; n++)
17
        (void) signal(n, handler); // UNSAFE
18
    ...
19
}

注意:siganl() 是不可靠的,这里为了简化程序而采用该接口。

2> 初始化信号集,然后屏蔽所有信号:

1
sigfillset(&fullMask);
2
if (sigprocmask(SIG_SETMASK, &fullMask, NULL) == -1) {
3
    perror("sigprocmask");
4
    exit(EXIT_FAILURE);
5
}
6
7
printSigset(stdout, "blocked:", &fullMask);
8
printf("%s: sleeping for %d seconds\n", argv[0], numSecs);
9
sleep(numSecs);

先屏蔽所有的信号,然后睡眠。睡眠期间,进程无法响应除 SIGSTOP 和 SIGKILL 之外的任何信号。

3> 睡眠结束后,用空信号集来解除所有的信号屏蔽:

1
sigemptyset(&emptyMask);   /* Unblock all signals */
2
if (sigprocmask(SIG_SETMASK, &emptyMask, NULL) == -1) {
3
    perror("sigprocmask");
4
    exit(EXIT_FAILURE);
5
}
6
printSigset(stdout, "blocked:", &emptyMask);
7
8
while (!gotSigint)  /* Loop until SIGINT caught */
9
        continue;
10
11
for (n = 1; n < NSIG; n++)
12
    if (sigCnt[n] != 0)
13
        printf("%s: signal %d caught %d time%s\n", argv[0], n,
14
                sigCnt[n], (sigCnt[n] == 1) ? "" : "s");
15
16
exit(EXIT_SUCCESS);
17
}

解除了对某个等待信号的屏蔽后,系统会立刻将该信号传递一次给进程。

打印信号集 printSigset():

1
void printSigset(FILE *of, const char *prefix, const sigset_t *sigset)
2
{
3
    int sig, cnt;
4
5
    cnt = 0;
6
    for (sig = 1; sig < NSIG; sig++) {
7
        if (sigismember(sigset, sig)) {
8
            cnt++;
9
            fprintf(of, "%s%d (%s)\n", prefix, sig, strsignal(sig));
10
        }
11
    }
12
13
    if (cnt == 0)
14
        fprintf(of, "%s<empty signal set>\n", prefix);
15
}

3. 运行效果:
屏蔽期间多次按下 ctrl + c (发送 SIGINT):

1
$ ./signal_set 5
2
./signal_set: PID is 18375
3
blocked:1 (Hangup)
4
blocked:2 (Interrupt)
5
blocked:3 (Quit)
6
...
7
blocked:64 (Real-time signal 30)
8
./signal_set: sleeping for 5 seconds
9
^C^C^Cblocked:<empty signal set>
10
./signal_set: signal 2 caught 1 time

在信号被屏蔽的 5 秒期间,连续按下 3 次 ctrl + c,所有信号都不会被处理。当过了 5 秒后,解除信号屏蔽,仅仅有一次 SIGINT 信号被成功地传递并处理。

三、待处理的信号 (Pending Signals)

如果某进程接受了一个该进程正在屏蔽的信号,那么会将该信号填加到进程的等待信号集 (set of pending signals) 中。当解除对该信号的屏蔽时,会随之将信号传递给此进程。可以使用 sigpending() 确定进程中处于等待状态的是哪些信号。

1
$ man 2 sigpending
2
NAME
3
       sigpending, rt_sigpending - examine pending signals
4
SYNOPSIS
5
       #include <signal.h>
6
7
       int sigpending(sigset_t *set);
8
DESCRIPTION
9
       sigpending() returns the set of signals that are pending for delivery to the calling thread (i.e., the signals
10
       which have been raised while blocked).  The mask of pending signals is returned in set.

sigpending() 为调用进程返回处于等待状态的信号集,并将其置于参数 set 指向的 sigset_t 中。

相关知识点:

  • 如果修改了对待处理信号的处置 (术语disposition),那么当后来解除对信号的锁定时,将根据新的处置来处理信号。

四、相关参考

  • 《Unix 环境高级编程-第10章 信号》
  • 《Linux/Unix 系统编程手册-第20章 信号:基本概念》
  • 《Linux 系统编程-第10章 信号》
  • 《Linux 程序设计-第11章 进程和信号》
  • 《Linux 环境编程:从应用到内核》
  • 《深入 Linux 内核架构 5.4.1信号》

五、如何高效学习:心理表征 (Mental Representations)

简单地说,心理表征就是人类对世界万物进行进行认知和记忆的模式。这里的世界万物既可以是某个物品、也可以是某个技能,某个行业,或者某个知识领域。对某个东西建立心理表征就是在完善对这事物的心理表征,你可以认为我现在写的这篇文件就是在完善我对 Linux 信号的心理表征。

心理表征的几个特点

  1. 刻意地反复练习会创建更多和更好的心理表征。

  2. 心理表征具有行业或领域的特定性。

    • 由于各个行业或领域之间心理表征的细节具有极大差异,我们难以给出一个十分清晰的顶层定义。但基本上,这些表征是信息预先存在的模式(比如事实、图片、规则、关系,等等),这些模式保存在长时记忆之中,可以用于有效且快速地顺应某些类型的局面。

    • 对于所有的心理表征,有一点是相同的:尽管短时记忆存在局限,但它们使得人们可以迅速地处理大量信息。 事实上,可能可以把心理表征定义为一个概念式的结构,设计用于回避短时记忆施加在心理加工上的一般局限。

    • 在任何一个行业或领域,技能与心理表征之间的关系是一种良性循环:你的技能越娴熟,创建的心理表征就越好;而心理表征越好,就越能有效地提升技能。

  3. 心理表征铸就杰出表现。

    • 将杰出人物与其他人区别开来的因素,正是前者心理表征的质量与数量。通过多年的练习,他们针对本行业或领域中自己可能遇到的各种不同局面,创建了高度复杂和精密的心理表征。
  4. 心理表征能让人进行无意识决策。

    • 经验丰富的软件工程师已经对某种语言的编码形成了心理表征,这使得他们在变成语法上无须有意识地思考,从而将更多的注意力集中在软件设计上,甚至在软件设计环节,也仍然有大量地无意识决策。

心理表征的几个作用

  1. 心理表征有助于找出规律。

    • 几乎在每一个行业或领域,杰出表现的标志是能在一系列事物中找出规律,这些事物,在无法创建高效心理表征的人们看来,可能是随机或令人困惑的。换句话讲,杰出人物能够看到“一片森林”,而其他所有人,却只看见“一棵树”。
  2. 心理表征有助于解释信息。

    • 心理表征可以帮助我们处理信息:理解和解读它,把它保存在记忆之中,组织它、分析它,并用它来决策。

    • 你对某个主题研究得越多,对该主题的心理表征也变得越细致,也越能更好地消化新的信息。

  3. 心理表征有助于制订计划。

    • 以写作为例,出色的写作者使用的方法与那些新手使用的方法存在着巨大的差异。初级的写作者的写作表征既简单又直接:写作者确定写作的主题,而且脑海有各种各样的思考,然后用文字将其描述出来。这种写作方法被称为“知识陈述”(knowledge telling),就是是把你脑海中浮现的所有观点一一告诉读者

    • 而优秀的写作人员由于长期练习而拥有了写作的心理表征,他们会先考虑什么概念和观点是重要的,文章的结构顺序应如何安排。

  4. 心理表征有助于高效学习。

    • 心理表征并不只是学习某项技能的结果;它们还可以帮助我们学习。在任何一个行业或领域,技能与心理表征之间的关系是一种良性循环:你的技能越娴熟,创建的心理表征就越好;而心理表征越好,就越能有效地提升技能。

如何建立心理表征

  1. 刻意练习,缺少这一点,就算你智商是 180 都没用。本文就是我对 写作和 Linux 系统编程的一次刻意练习和完善心理表征的过程

  2. 在工作中运用刻意练习原则,参考《刻意练习》-第6章;

  3. 在生活中运用刻意练习原则,参考《刻意练习》-第7章;

大家自行去阅读吧,不是自己理解到的东西是消化不了的。可能会有人意犹未尽了,关于 2/3 其实我也还有一些心得要分享,鉴于在一篇文章里大多数人的注意力无法集中太久,有机会的话会把这些心得放在后面的文章,下一篇文章:举例说明一下信号不排队的特性。

六、欢迎关注我的公众号 (嵌入式Hacker)

你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。如果你也对 嵌入式系统和开源软件 感兴趣,并且想和更多人互相交流学习的话,请关注我的公众号:嵌入式Hacker,一起来学习吧,无论是 关注或转发 , 还是赏赐,都是对作者莫大的支持,感谢 各位的大拇指 ,祝工作顺利,家庭和睦~

这是一篇有趣的文章吗?

欢迎关注我的其它发布渠道