早,继续记录我的学习心得。
机械的练习:
只是埋头干!我一直在挥着球拍,努力去击球。我一直在看这道数学题,正试着解答。我一直在重复写代码,试图成为技术大牛。
有准确目的的练习:
意味着要比机械的练习更有目的性,考虑更周全,从而让自己变得更专注。
以玩模拟赛车 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 | |
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, ¤t->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 分钟的 “绿色锻炼” 就能减缓压力、改善心情、提高注意力、增强自控力 。任何能让你走到室外、回到大自然怀抱中的活动,都是绿色锻炼。
如果你坚信自己不适合运动,就把运动的定义扩大一些:
只要是不是坐着的且不在吃垃圾食品的活动就可以认为是在运动,例如扫地、散步、逛街。
鉴于大多数人的注意力无法在一篇文章里上集中太久,更多的内容请大家自行去阅读吧,不是自己理解到的东西是消化不了的。有机会的话我会把更多的读书心得放在后面的文章。
你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。如果你也对 嵌入式系统和开源软件 感兴趣,并且想和更多人互相交流学习的话,请关注我的公众号:嵌入式系统砖家,一起来学习吧,无论是 关注或转发 , 还是赏赐,都是对作者莫大的支持,感谢 各位的大拇指「在看」 ,,祝工作顺利,家庭和睦~