嵌入式Hacker (es-hacker)

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

0%

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

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

机械的练习:
只是埋头干!我一直在挥着球拍,努力去击球。我一直在看这道数学题,正试着解答。我一直在重复写代码,试图成为技术大牛。

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

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

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

学以致用,明确一下本文的目的:

  • 举例说明 Linux 不会对标准信号进行排队处理。

  • 文末分享《自控力》里提到的一个提升自控力的小技巧:进行有效锻炼 (重点)。

正文目录:

1
一、待处理的信号 (Pending Signals)
2
	1. 一个简单的例子 (sig_pending.c)
3
		1) 分解代码
4
		2) 运行效果
5
二、不对待处理的标准信号进行排队处理
6
	1. 仍是那个简单的例子 (sig_pending.c)
7
		1) 运行效果
8
	2. 查看 Linux 内核里 Signal Pending 相关的实现 (非重点)
9
		1) 相关数据结构
10
		2) 信号的产生
11
		3) 信号的传递
12
三、相关参考
13
四、如何高效地学习:了解自控力 (Self-control)
14
	1. 此书的基本信息
15
	2. 一个关于提高自控力的小技巧:锻炼身体

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

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

1
$ man 2 sigpending
2
    #include <signal.h>
3
    int sigpending(sigset_t *set);

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

1. 一个简单的例子 (sig_pending.c)

1) 分解代码:
1> main():

1
int main(void)
2
{
3
	sigset_t	newmask, oldmask, pendmask;
4
5
	if (signal(SIGQUIT, sig_quit) == SIG_ERR)
6
		err_sys("can't catch SIGQUIT");
7
8
	/* Block SIGQUIT and save current signal mask. */
9
	sigemptyset(&newmask);
10
	sigaddset(&newmask, SIGQUIT);
11
	if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
12
		err_sys("SIG_BLOCK error");
13
14
    /* SIGQUIT here will remain pending */
15
	sleep(5);
16
17
	if (sigpending(&pendmask) < 0)
18
		err_sys("sigpending error");
19
	if (sigismember(&pendmask, SIGQUIT))
20
		printf("\nSIGQUIT pending\n");
21
22
	/* Restore signal mask which unblocks SIGQUIT. */
23
	if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
24
		err_sys("SIG_SETMASK error");
25
	printf("SIGQUIT unblocked\n");
26
27
    /* SIGQUIT here will terminate with core file */
28
	sleep(5);
29
30
	exit(0);
31
}

main() 做了 5 件事:

  • 设置 SIGQUIT 的信号处理函数;
  • 屏蔽 SIGQUIT;
  • 睡眠 5 秒,用于等待 SIGQUIT 信号;
  • 睡眠结束,检测 SIGQUIT 是否处于 pending;
  • 解除屏蔽 SIGQUIT;

注意:在设置 SIGQUIT 为阻塞时,我们保存了老的屏蔽字。为了解除对该信号的阻塞,用老的屏蔽字重新设置了进程信号屏蔽字。另一种方法是用 SIG_UNBLOCK 使阻塞的信号不再阻塞。如果编写一个可能由其他人使用的函数,而且需要在函数中阻塞一个信号,则不能用 SIG_UNBLOCK 简单地解除对此信号的阻塞,这是因为此函数的调用者在调用本函数之前可能也阻塞了此信号。

2> 信号处理函数 sig_quit():

1
static void sig_quit(int signo)
2
{
3
	printf("caught SIGQUIT\n");
4
	if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
5
		err_sys("can't reset SIGQUIT");
6
}

2) 运行效果:

1
$ ./sig_pending 
2
^\                      // 按下 1 次 ctrl + \ (在5s之内)
3
SIGQUIT pending         // 从 sleep(5) 返回后
4
caught SIGQUIT          // 在信号处理程序中
5
SIGQUIT unblocked       // 从sigprocmask() 返回
6
^\Quit (core dumped)

2 个值得注意的点:

  • 信号处理函数是在 sigprocmask() unblock 信号返回之前被调用;
  • 用 signal() 设置信号处理函数,信号被处理时,会将信号处置重置为其默认行为。要想在同一信号“再度光临”时再次调用该信号处理器函数,程序员必须在信号处理器内部调用signal(),以显式重建处理器函数,但是这种处理方式是不安全的,真实的项目里应使用 sigaction(),后续的文章会举例讲解。

二、不对待处理的信号进行排队处理

等待信号集只是一个掩码,仅表明一个信号是否发生,而未表明其发生的次数。换言之,如果同一信号在阻塞状态下产生多次,那么会将该信号记录在等待信号集中,并在稍后仅传递一次。后面会介绍实时信号,对实时信号所采取的是队列化管理。如果将某一实时信号的多个实例发送给一进程,那么将会多次传递该实时信号,暂不做深入介绍。

1. 仍是那个简单的例子 (sig_pending.c)

为了降低学习难度,跟前面的 Pending Signals 章节使用同一个例子,修改一下测试步骤:

1
$ ./sig_pending 
2
^\^\^\                  // 按下 3 次 ctrl + \ (在5s之内)
3
SIGQUIT pending         // 从 sleep(5) 返回后
4
caught SIGQUIT          // 只调用了一次信号处理程序
5
SIGQUIT unblocked       // 从sigprocmask() 返回
6
^\Quit (core dumped)

第二次运行该程序时,在进程休眠期间产生了 3 次 SIGQUIT 信号,但是取消对该信号的阻塞后,系统只向进程传送了一次 SIGQUIT,从中可以看出在 Linux 系统上没有对信号进行排队处理。

2. 查看 Linux 内核里 Signal Pending 相关的实现 (非重点)

1) 相关数据结构
内核用 struct task_struct 来描述一个进程,struct task_struct 中信号相关的成员 (Linux-4.14):

1
<sched.h>
2
struct task_struct {
3
...
4
	/* Signal handlers: */
5
	struct signal_struct		*signal;
6
	struct sighand_struct		*sighand;
7
	sigset_t			blocked;
8
	sigset_t			real_blocked;
9
	/* Restored if set_restore_sigmask() was used: */
10
	sigset_t			saved_sigmask;
11
	struct sigpending		pending;
12
	unsigned long			sas_ss_sp;
13
	size_t				sas_ss_size;
14
	unsigned int			sas_ss_flags;
15
...
16
};

我们将注意力集中中 struct sigpending pending 上。struct sigpending pending 建立了一个链表,该链表包含了所有已经产生、且有待内核处理的信号,其定义如下:

1
struct sigpending {
2
	struct list_head list;
3
	sigset_t signal;
4
};

成员 struct list_head list 通过双向链表管理所有待处理信号,每一种待处理的信号对应双向链表中的 1 个 struct sigqueue 节点。而成员 sigset_t signal 是位图 (bit mask,或称位掩码),它指定了仍然有待处理的所有信号的编号。某 1 bit = 1 表示该 bit 对应的信号待处理。sigset_t 所包含的比特位数目要 >= 所支持的信号数目。因此,内核使用了 unsigned long 数组来定义该数据类型:

1
typedef struct {
2
	unsigned long sig[_NSIG_WORDS];
3
} sigset_t;

struct sigqueue 的定义如下:

1
struct sigqueue {
2
	struct list_head list;
3
	int flags;
4
	siginfo_t info;
5
	...
6
};

siginfo_t 用于保存信号的额外信息,暂时不用关心。

注意:在 struct sigpending 链表中,struct sigqueue 对应的是一种类型的待处理信号,而不是某一个具体的信号。

示意图:

2) 信号的产生
当给进程发送一个信号时,这个信号可能来自内核,也可能来自另外一个进程。内核里有多个 API 能产生信号,这些 API 最终都会调用 send_signal()。我们重点关注信号是何时被设置为 pending 状态的。

linux/kernel/signal.c:

1
send_signal()
2
	__send_signal()
3
		struct sigqueue *q = __sigqueue_alloc();
4
		list_add_tail(&q->list, &pending->list); // 将待处理信号添加到 pending 链表中
5
		sigaddset(&pending->signal, sig); // 在位图中将信号对应的 bit 置 1
6
		complete_signal(sig, t, group);
7
			signal_wake_up();

send_signal() 会分配一个新的 struct sigqueue 实例,然后为其填充信号的额外信息,并添加到目标进程的 sigpending 链表且设置位图。

如果信号成功发送,没有被阻塞,就可以用 signal_wake_up() 唤醒目标进程,使得调度器可以选择目标进程运行。

3) 信号的传递:
这些知识放在这篇文章里已经完全超纲了,如果将所有的细节都暴露出来会让初学者感到极度的困惑。所以,我们只迈出一小步,将仅剩的一点注意力集中在内核在执行信号处理函数前是如何处理 pending 信号的。

在每次由内核态切换到用户态时,内核都会进行信号处理,最终的效果就是调用 do_signal() 函数
linux/kernel/signal.c:

1
do_signal()
2
	get_signal()
3
		dequeue_signal(current, &current->blocked, &ksig->info);
4
    handle_signal()
5
		signal_setup_done();
6
			signal_delivered();
  • dequeue_signal() 是关键点:

    1
    dequeue_signal()
    2
    	int sig = next_signal(pending, mask);
    3
    	collect_signal(sig, pending, info, resched_timer);
    4
    		sigdelset(&list->signal, sig);	// 取消信号的 pending 状态
    5
    		list_del_init(&first->list);	// 删除 pending 链表中的 struct sigqueue 节点
    6
    		copy_siginfo(info, &first->info);
  • handle_signal() 会操作进程在用户态下的栈,使得在从内核态切换到用户态之后运行信号处理程序,而不是正常的程序代码。

  • do_signal() 返回时,信号处理函数就会被执行。

三、相关参考

  • 《Unix 环境高级编程-第10章 信号》
  • 《Linux/Unix 系统编程手册-第20章 信号:基本概念》
  • 《Linux 系统编程-第10章 信号》
  • 《Linux 程序设计-第11章 进程和信号》
  • 《深入理解 Linux 内核 第11章 信号》
  • 《深入 Linux 内核架构 5.4.1信号》
  • 《Linux 内核源代码情景分析 6.4信号》

四、如何高效地学习:了解自控力 (Self-control)

最近我在阅读一本书:《自控力-斯坦福大学最受欢迎心理学课程》,推荐大家阅读。

1. 此书的基本信息:

基本信息:

1
作者:  [美] 凯利·麦格尼格尔 / [美] 凯利·麦格尼格尔
2
出版社: 文化发展出版社(原印刷工业出版社)
3
出品方: 磨铁图书
4
副标题: 斯坦福大学最受欢迎心理学课程
5
原作名: The Willpower Instinct:How Self-control Works,Why it Matters,and What You Can do to Get More of It
6
译者: 王岑卉
7
出版年: 2012-8
8
页数: 263
9
定价: 39.80元
10
装帧: 平装
11
ISBN: 9787514205039
12
豆瓣评分 8.2 45433人评价

作者:
凯利•麦格尼格尔教授 (Kelly McGonigal, Ph.D.) 是斯坦福大学备受赞誉的心理学家,也是医学健康促进项目的健康教育家。她为专业人士和普通大众开设的心理学课程,包括“意志力科学” (The Science of Willpower) 和“在压力下好好生活” (Living Well with Stress) ,都是斯坦福大学继续教育学院历史上最受欢迎的课程。

目录:

1
01 我要做,我不要,我想要:什么是意志力?为什么意志力至关重要?
2
02意志力的本能: 人生来就能抵制奶酪蛋糕的诱惑
3
03累到无力抵抗:为什么自控力和肌肉一样有极限?
4
04容忍罪恶:为何善行之后会有恶行?
5
05大脑的弥天大谎: 为什么我们误把渴望当幸福?
6
06 “那又如何”:情绪低落为何会使人屈服于诱惑?
7
07出售未来:及时享乐的经济学
8
08传染:为什么意志力会传染?
9
09别读这章:“我不要”力量的局限性

书的内容挺好的,但是我个人认为章节名起得有点不好,降低了阅读欲望。

2. 一个关于提高自控力的小技巧:锻炼身体

提高自控力的 “神药”:
锻炼身体。

这听上去挺操蛋的,大多数人就是想要更多的自控力来促使自己锻炼身体,这里反而要求通过锻炼身体来提升自控力。别急,下面这个关于有效锻炼的观点就有点意思了:

什么样的锻炼最有效: 你愿意去做的锻炼。

如果你设定了一个目标,但一周都坚持不下来的话,那是毫无意义的。而且,对于究竟要锻炼多久,科学研究也没有达成共识。2010年,一项针对10个不同研究的分析发现,改善心情、缓解压力的最有效的锻炼是每次5分钟,而不是每次几小时。

某些科学家认为: 5 分钟的 “绿色锻炼” 就能减缓压力、改善心情、提高注意力、增强自控力 。任何能让你走到室外、回到大自然怀抱中的活动,都是绿色锻炼。

如果你坚信自己不适合运动,就把运动的定义扩大一些:
只要是不是坐着的且不在吃垃圾食品的活动就可以认为是在运动,例如扫地、散步、逛街。

鉴于大多数人的注意力无法在一篇文章里上集中太久,更多的内容请大家自行去阅读吧,不是自己理解到的东西是消化不了的。有机会的话我会把更多的读书心得放在后面的文章。

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

这是一篇有趣的文章吗?

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