0%
20.9 信号集和信号的处理
说明
20 信号 讲了信号的基本概念、列举了一部分信号,并且说明了发送信号的方式。这一节主要是讲和信号处理相关的 API。
信号集
信号集对应类型 sigset_t
,操作它的函数都是以其指针为参数的。相关的函数有:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum);
必须用 sigemptyset
或者 sigfillset
对 sigset_t
类型的数据完成初始化,直接对其赋值 0 会有可移植性的问题。
GNU C 库还实现了三个函数:sigandset
/ sigorset
/ sigisemptyset
。
21 如何正确处理信号?
设计处理器函数
处理器函数应该尽可能简单,尤其是不要调用 stdio 库函数(因为它们一般不是异步信号安全的)。以下是几种处理方式:
- 修改全局数据结构。程序周期性检查这些结构。
- 清理资源并终止程序,或者使用非本地跳转返回到主程序中的预定位置。
两个概念
可重入(Reentrant)函数
SUSv3 对可重入函数的定义是:函数由两条或多条线程调用时,即便是交叉执行,其效果也与各线程以未定义顺序依次调用时一致。
有点不好理解的是:malloc
族函数是线程安全的,但是不是可重入的,因为它管理的是全局的数据结构,这个数据结构不能在多个线程上同时修改。Glibc 2.x 的 malloc 是首先选择一个 arena,然后试图获取它的锁,然后从其中分配内存;如果不同线程获取的 arenas 不同,那么可以同时得到锁(不是同一个锁),也自然不会冲突。printf
、scanf
这些 stdio 的函数也是不可重入的,在执行的时候会加锁,因为它们要管理全局的缓冲区。还有一些通过静态分配的数据结构返回信息的函数,它们也是不可重入的,比如 crypt()
、getpwnam()
、gethostbyname()
以及 getservbyname()
。
举个例子:如果在调用了 malloc
,获取到锁之后,被信号打断,转入信号处理器中,在信号处理器中又调用 malloc
,而且是尝试获取同一个锁,就会因为无法获得锁而一直卡在信号处理器中无法退出!所以可重入是比线程安全更加严格的概念。
20 信号
信号
信号是对进程的通知机制,也被称为软中断,分成标准信号(传统信号,编号范围是 1~31)和实时信号。
部分信号:
- SIGINT:ctrl + c。
- SIGQUIT:ctrl + </kbd>,和 SIGINT 比多了核心转储。
- SIGTERM:
kill
命令默认发送的信号,程序可以注册处理这个信号,因此可以实现优雅退出。 - SIGKILL:必杀信号。
- SIGSTOP:必停信号。
- SIGPIPE:写管道有错,可能是读的进程把管道关了。
- SIGFPE:名字是浮点数错误,实际上泛指除 0 错误。而且 x86-64 上浮点数除 0 默认不出错,需要用
feenableexcept()
启用异常。 - SIGALRM:实时计时器过期。
- SIGVTALRM::虚拟计时器过期。
- SIGHUP:会话结束(终端断开)时发送给程序的信号,默认行为为杀死程序。在一些守护程序上还有重载配置的作用。
- SIGTSTP:ctrl + z。
- SIGUSR1 和 SIGUSR2:留给程序员自己用的,系统绝对不会发送的信号。
- SIGWINCH:终端环境的窗口尺寸发生变化。
一个信号默认的处理行为是三者之一:1. 忽略;2. 终止;3. 内核转储。
处理信号
使用 signal
或者 sigaction
注册信号处理函数,一般条件下 sigaction
是更好的选择。处理信号时一般不要调用 printf
等来自 stdio.h 的函数。
发送信号
kill
系统调用可以向指定进程发送给定的信号:
- 进程号 > 0:指定进程。
- 进程号 = 0:同组所有进程,包括自己。
- 进程号 < -1:取绝对值,然后向指定的进程组中所有进程。
- 进程号 = -1:系统范围内除了 init 和进程自己以外的、所有的当前进程有权限可以发送信号的目标进程。
- 信号值 = 0:不发送信号,但是检查是否能够成功向目标进程发送信号。可能的
errno
值有:EPERM
表示进程存在但是没有权限;ESRCH
表示进程不存在。
其他发送信号的规则:
- 特权级(
CAP_KILL
)进程可以向任何进程发送信号。 - init(1 号)进程是一个特例,只有它注册了某个信号的处理函数,才能向它发送这个信号。(原文是:It can be sent only signals for which it has a handler installed. 我觉得中文版翻译的不是很好,所以这里写的是我自己的理解。)
- 如果信号发送者和接收者满足一定关系,则信号是可以发送的。条件是:发送者的实际用户 ID 或者有效用户 ID 是接收者的实际用户 ID 或者保存的 set-user-ID,如下图所示。如果允许以接收者的有效用户 ID 作为判断依据,特权进程可能会设置自己的有效用户 ID 为普通用户,但是这时普通用户就能够向这个进程发送信号了。(书中说的是:将目标进程有效用户 ID 排除在检查范围之外,这一举措的辅助作用在于防止用户某甲向用户某乙的进程发送信号,而该进程正在执行的 set-user-ID 程序又属于用户某甲。我觉得这个说法很牵强,因为判断机制里面已经允许按照保存的 set-user-ID 来判断了。)
- SIGCONT 信号很特殊。非特权进程始终可以向同一个会话中的其他任何一个进程发送这个信号。
检查进程的存在
前面说了,使用 kill
向进程发送信号 0(实际上没有这个信号,但是 0 可以作为参数)可以测试是否能够成功发送信号。只要 errno
不是 ESRCH
就说明进程存在。
其他检查进程是否存在的方式:
wait()
系统调用:用来监控子进程。- 信号量和文件锁:如果能获取到锁说明进程已经终止。
- FIFO 等 IPC 通道:写入关闭说明进程终止。
- /proc/PID 接口:这一项由于用到了 PID,所以会受到循环使用 PID 的影响,导致结果在极端条件下不准确(但是大多数时候不会)。
其他发送信号的方式
raise()
函数可以用来进程自己发送信号。当系统只支持单线程时,相当于 kill(getpid(), sig)
,当系统支持多线程时,相当于 pthread_kill(pthread_self(), sig)
。相比起来,kill
总是将信号发给进程,具体线程可以是进程的任何一个。由于 raise()
函数是向进程自己发送信号,信号将立即传递。处理完成之后才会回到调用者。
killpg()
可以用于向某一进程组的所有成员发送信号,相当于 kill(-pgrp, sig)
。如果给的 pgrp 为 0,那么就会向当前进程所在进程组的所有进程发送信号。
显示信号字符串
和 errno 一样,我们可以调用函数来显示描述信号的字符串。
char *strsignal(int sig);
const char *sigdescr_np(int sig);
const char *sigabbrev_np(int sig);
书里只讲到 strsignal,它还会获取当前的 locale,但是在获取 locale 这一点上不是线程安全的。后面两个前者是获取详细描述,后者是获取简略描述(比如 SIGINT 的描述是 "INT"
),它们都不考虑 locale,是线程安全的。
psignal
和 perror
的功能则类似。psignal
也是考虑 locale 的。
20.1 使用 SIGTERM 和核心转储
核心转储介绍
GDB 可以加载核心转储以复现程序终止时的内存,也可以为给定的进程创建核心转储。
核心转储是可以通过发送信号实现的。调用 abort()
时,会向进程自身发送 SIGABRT 信号,该信号的默认行为是终止进程并为其生成核心转储。在终端中,SIGQUIT(ctrl + /)和 SIGINT(ctrl + c)相比,也多出来了生成核心转储的行为。可以从以下尝试中体会出两个信号的差异:
(py310) xxx /data/apue $ ulimit -c unlimited
(py310) xxx /data/apue $ ./build/main
^C
# 返回码 130
(py310) xxx /data/apue $ ./build/main
^\Quit (core dumped)
# 返回码 131
可以检查核心转储文件的路径。既可以通过 /proc 文件系统查看路径,也可以通过 sysctl
命令查看路径。在 WSL 中打印出来的结果见下方:
(py310) xxx $ cat /proc/sys/kernel/core_pattern
/mnt/wslg/dumps/core.%e
(py310) xxx $ sysctl kernel.core_pattern
kernel.core_pattern = /mnt/wslg/dumps/core.%e
19 inotify 监控文件事件
基本流程
- 使用
inotify_init
创建一个 inotify 实例,返回值是文件描述符,用来读取 inotify 监控项信息。 - 使用
inotify_add_watch
对 inotify 实例创建或者修改监控项目。参数 fd 指代 inotify 实例,参数 pathname 表示要监控的文件或者目录,用户必须有其读取权限(只检查一次,创建监控项目成功之后即便权限发生变化监控也不会被移除)。函数的返回值是一个监控描述符(watch descriptor,wd)。 - 不断使用
read
系统调用去读取 fd 关联文件的信息,然后将字节流中的地址转换成struct inotify_event *
类型,然后判断其信息。给read
提供的缓冲区需要足够大,至少需要容纳sizeof(struct inotify_event) + NAME_MAX + 1
个字节,不过read
缓冲区越大读取的效率越高。
struct inotify_event {
int wd; /* Watch descriptor */
uint32_t mask; /* Mask describing event */
uint32_t cookie; /* Unique cookie associating related
events (for rename(2)) */
uint32_t len; /* Size of name field */
char name[]; /* Optional null-terminated name */
};
- 使用
inotify_rm_watch
移除监控事件。 - 使用
close
关闭inotify_init
创建的 fd。
inotify_init1
Linux 还有一个系统调用 inotify_init1
,它比 inotify_init 多出来一个 flags 参数,可以用来指定 IN_CLOEXEC
或者 IN_NONBLOCK
。这避免了稍后用 fcntl
设置 flags 的繁琐操作。
读取监控事件
受监控目录有文件发生事件时,name
字段是一个零字符结尾字符串;受监控目录本身发生事件时,len
为 0,name
没有有效内容。可以监控的事件类型相当多,具体可以参考手册。特殊事件包括:IN_IGNORED
(监控项被内核或应用移除)、IN_Q_OVERFLOW
(事件队列溢出)、IN_UNMOUNT
(包含监控对象的文件系统被卸载)等。
18.1-2 i-node 和链接
i-node
i 节点一般写作 i-node,中间有个短横线。i-node 表的编号开始于 1,因为 0 用来表示未使用的条目,然后 i-node 1 用来记录文件系统的坏块,i-node 2 用来记录根目录,即 /。由于可能有多个路径不同、名称不同的文件(通过硬链接)指向同一个 i-node,因此 i-node 中并不记录本文件的名称。
符号链接
符号链接的所有权和权限在大多数情况下会被忽略。仅在符号链接所在目录具有粘着位,又要删除或者重命名符号链接时,才会考虑符号链接本身的所有权。
符号链接解引用的次数是有限的,SUSv3 规定对路径中的每个符号链接部件至少 _POSIX_SYMLOOP_MAX
即 8 次。内核 2.6.18 之前,Linux 最多支持 5 次,2.6.18 时支持了 8 次。Linux 还规定一个完整路径中符号链接引用解除次数最多 40 次。
符号链接使用 symlink
创建,使用 readlink
读取。
link
和 unlink
(不会解除符号链接引用)
link
创建(硬)链接,unlink
解除链接(也就是删除文件)。SUSv3 规定 link
要解符号链接引用,但是 Linux 并没有这样实现。现在 SUSv4 规定 link
调用是否解除链接由实现定义。
17 ACL setfacl getfacl
ACL 介绍
ACL 在 Linux 内核 2.6 被支持,是用文件的
扩展属性 实现的,扩展属性名为 system.posix_acl_access
。
要想在 ext2、ext3、ext4 或 reiserfs 文件系统上创建 ACL,装配相应的文件系统时需要带
mount –o acl
选项。
ACL 的功能是传统访问权限的超集,优先于传统访问权限生效。每个 ACL 项目(Access Control Entry,即 ACE)指定一个 rwx(3 个二进制位)的权限。其中:
ACL_USER_OBJ
、ACL_GROUP_OBJ
和ACL_OTHER
(在没有指定其他项目时)相当于传统权限,这个时候 ACL 被称为最小 ACL。- 支持对特定用户(
ACL_USER
)或者特定组(ACL_GRUOP
)指定权限。 - 支持掩码。
ACL_MASK
记录了可由ACL_USER
、ACL_GROUP_OBJ
以及ACL_GROUP
型 ACE 所能授予的最高权限。但如果 ACL 含有标记类型为ACL_USER
或ACL_GROUP
的记录,那么就必须包含一条ACL_MASK
型的 ACE。如果包含了掩码项目,ACL 就成为扩展 ACL。 ACL_USER_OBJ
、ACL_GROUP_OBJ
、ACL_OTHER
和ACL_MASK
都只能出现一次。
ACL 权限匹配算法
ACL 的权限检查同样是具有短路特性的。
小白入坑挑键盘经历
发表于:
更新于:
更新
- 2024 年 6 月 26 日:解决在桌面上移动键盘时脚撑发出刺耳摩擦声的办法:在脚撑上面贴上透明胶布。
- 2024 年 6 月 29 日:现在键盘因为多次拔键拔轴已经是战损状态了,尤其是卫星轴和定位板之间卡不紧、有松动,问题相当大。很扎心:
以后选键盘还是选个一开始就能接受轴体的,不要再换轴了吧 (*^_^*) 。总觉得 V75K 的卫星轴专门顺着阿尼亚轴的短行程调过(或者是挑选过),换阿尼亚轴匹配度更高一点。我现在没有别的轴体了(卖了),就试了鲸海轴,空格左侧是有 空键程 的,换成阿尼亚轴就好一点。
买键盘的历史
本人小白,以前都是用薄膜键盘,这两个月买过以下键盘:
- 京东京造 K3,佳达隆矮茶轴,对于长时间用笔记本薄膜键盘的我来说非常容易上手。但是 84 按键(无论从手感还是视觉上都)非常紧凑,有时候找不到方向键,按 ctrl 和 shift 按键也经常按错。大写锁定灯在按键下方,不容易看到。另外对于 340 元这个价位来说,这样一把塑料机身的键盘显得有点廉价。
因为是矮轴(2024 年 6 月 1 日:我现在才知道并不是所有矮轴都是无边框)因为是 keychron 的无边框设计,难买适配的防尘罩。也担心放到书包里面被电脑压弯,不过主要退货原因还是经常按错按键。 - 宁芝有线 35g 87 键。刚到手在桌面上就放不平,在 NGA 上也搜到了相同问题,对宁芝的品控非常担心,秒退。
- Keychron K1 Pro,87 键佳达隆矮茶轴。因为上次 84 键太紧凑不太好找方向键,所以这次买了 87 键的。好处是现在能够更容易看到 CapsLock 指示灯。到手的时候备用键帽有质量问题换不上去,请客服补发了一次。不知道为什么,感觉按键敲下去就是没有 K3 好用。而且可能是因为矮轴机身薄 + 填充足,想要给料但是机身又塞不下,所以是鼓起的!已退。关于我觉得没有 K3 好用,可能和这个截图中的评论有关:
- IKBC 红轴有线,选了个好看的配色,还送了键帽。键盘左侧区域有钢丝音,尤其是 tab 按键,OEM 键帽用起来也挺顺手。
拉出大脚撑,在桌面上移动键盘,就会发出粉笔垂直划黑板的声音 1 ,其他键盘的脚撑就不会有刺耳的声音。没有明显质量问题(钢丝音是普遍现象我觉得也还能忍),而且刚到手的那段时间我确实觉得还不错(不过买了 V75K 就不喜欢 IKBC 了),所以留了。 - ATK V75K 闪银。外观非常好看、便宜(相对于 1~3)、三模、料足,用的原厂键帽会比 IKBC 要矮一些。阿尼亚轴手感还是舒服的,但是我不喜欢麻将音 2,换了静音灰雪轴。空格左右的卫星轴好像有点高度不一致,从左右两次按下去键帽,可以感觉得到左侧能按的更低一些。然后不小心掰断了空格按键,用胶布贴起来了。后来送修了 3。自己虽然买了空格键帽,但是是在键盘送修之后才到货的。设计缺点也还是有,比如 80 按键(75% 配列)右侧没有 ctrl,而且也没有 home 和 end 按键,注释和批量选中代码有点不方便;CapsLock 按键灯在按键的正下方,侧着看很难看到,使用起来等同于没有灯。很喜欢所以留了。
Caution
简直是品控中招体质。
选购键盘的想法
- 小配列键盘好看便携,但是可能会不习惯。小配列键盘的大写锁定按键灯一般都很难看到(除非专门设计在侧边),而买常见尺寸的键盘没这个问题。我觉得小配列可能比较适合发烧友,因为键少换的快(我这个 80 键的全部换轴一次要 40 分钟),而且每次买键帽或者买轴都会买的更少、价格更低。小键盘放在桌面上也有一种工艺品的感觉,但是要和按键少这个缺点放在一起好好考虑一下。京造 K3 容易按错可能是因为距离相近、而且键帽都是一样的高度,不像 V75K 正常轴体、用原厂键帽,仅凭手感就能区分方向键和上面紧邻的回车。
- 矮轴键盘对于从薄膜键盘转过来的人非常容易上手,但是因为售价普遍偏高、难买防尘盖,不建议入。如果不能接受 IKBC 这样的高键帽,可以看看 XDA 或者原厂键帽这种比较低一点的,不一定非要去买矮轴。NuPhy 看起来倒还不错(不知道上手怎么样),做工精致、有边框、送防尘罩,但是价格超出我的预算了。
- 轴体选择。我到现在都没有思路……静音灰雪轴和阿尼亚轴我都不是特别喜欢,但是感觉买新的轴应该也差不多,不如省下钱不买了。买热插拔的键盘的确会有一种买轴的诱惑。
16.01 文件扩展属性 EA getfattr setfattr
什么是扩展属性
扩展属性(Extended Attributes)可以将任何字符串键和任何数据值的信息和文件 inode 关联起来,值可以不是零结尾的字符串。其键的命名方式是 namespace.name
,其中 namespace
只能是以下四种之一:
user
:权限管理方式和读写文件的内容是一样的。trusted
:和user
类似,但是需要管理员权限,即CAP_SYS_ADMIN
。system
:键的第二分量只能是内核认可的字符串。security
:用来支持操作系统的安全模块。
在 ext2、ext3、ext4 或 Reiserfs 文件系统上,如欲将 user EA 与一文件关联,在装配底层文件系统时需带有 user_xattr 选项。 比如:
mount -o user_xattr <device> <directory>
Note
书上没有说扩展属性在 Linux 中是怎么实现的。