Linux 系统编程之四多线程编程
大纲
信号
信号的概念
信号(Signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是不能给进程传递任何数据。信号产生的原因有很多,在 Linux 系统下,可以用 kill
、killall
等命令发送信号。
killall 命令被弃用
- 在很多 Linux 发行版中,
killall
命令已经被移除,取而代之的是pkill
命令。 - 如果想要终止所有名为
process_name
的进程,可以使用命令pkill -15 process_name
。
信号的作用
- 服务程序运行在后台,如果想让它中止运行,直接杀掉它的进程不是一个好办法。因为程序被杀死的时候,程序突然终止运行,没有时间去执行善后工作(如释放资源)。
- 如果向服务程序发送一个信号,服务程序收到这个信号后,调用一个自定义函数,在自定义函数中编写善后的代码(如释放资源),那么程序就可以有计划地退出执行。
- 向服务程序发送
0
的信号,可以检测程序是否存活。
信号的类型
Linux 系统常用的信号类型如下表所示:
信号名 | 信号值 | 默认处理动作 | 发出信号的原因 |
---|---|---|---|
SIGHUP | 1 | A | 终端挂起或者控制进程终止 |
SIGINT | 2 | A | 键盘中断 ctrl + c 组合快捷键 |
SIGQUIT | 3 | C | 键盘的退出键被按下 |
SIGILL | 4 | C | 非法指令 |
SIGABRT | 6 | C | 由 abort (3) 发出的退出指令 |
SIGFPE | 8 | C | 浮点异常 |
SIGKILL | 9 | AEF | 采用 “kill -9 进程编号” 强制杀死程序 |
SIGSEGV | 11 | C | 无效的内存引用 |
SIGPIPE | 13 | A | 管道破裂,写一个没有读端口的管道 |
SIGALRM | 14 | A | 由 alarm (2) 发出的信号 |
SIGTERM | 15 | A | 采用 “kill 进程编号” 或 “killall 程序名” 杀死程序 |
SIGUSR1 | 10 | A | 用户自定义信号 1 |
SIGUSR2 | 12 | A | 用户自定义信号 2 |
SIGCHLD | 17 | B | 子进程结束信号 |
SIGCONT | 18 | 进程继续(曾被停止的进程) | |
SIGSTOP | 19 | DEF | 终止进程 |
SIGTSTP | 20 | D | 控制终端(tty)上按下停止键 |
SIGTTIN | 21 | D | 后台进程企图从控制终端读 |
SIGTTOU | 22 | D | 后台进程企图从控制终端写 |
在上表中,默认处理动作一项中的字母含义如下:
A
:缺省的动作是终止进程。B
:缺省的动作是忽略该信号,将该信号丢弃,不做任何处理。C
:缺省的动作是终止进程并进行内核映像转储(Core Dump)。内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统,并且让进程退出执行,这样做的好处是为开发人员提供了方便,使得他们可以得到进程当时执行时的数据,允许他们确定转储的原因,并且可以调试他们的程序。D
:缺省的动作是停止进程,进入停止状态的程序还能重新继续,一般是在调试的过程中。E
:该信号不能被捕获。F
:该信号不能被忽略。
提示
使用 Linux 命令 kill -l
可以查看所有的信号类型。
信号的处理
进程对信号的处理方式有三种:
- (1) 对该信号的处理采用系统的默认操作,大部分的信号的默认操作是终止进程。
- (2) 设置中断的处理函数,收到信号后,由该函数来处理。
- (3) 忽略某个信号,对该信号不做任何处理,就像未发生过一样。
signal()
函数可以设置应用程序对信号的处理方式,其函数声明如下:
1 | sighandler_t signal(int signum, sighandler_t handler); |
- 函数参数
signum
表示信号的编号。 - 函数参数
handler
表示信号的处理方式,有以下三种情况:- (1)
SIG_DFL
:恢复函数参数signum
所指信号的处理方式为默认行为。 - (2) 一个自定义的信号处理函数,信号的编号为这个自定义函数的参数。
- (3)
SIG_IGN
:忽略处理函数参数signum
所指的信号。
- (1)
信号的发出
Linux 操作系统提供了 kill
和 killall
命令向应用程序发送信号,C 语言也提供了 kill
函数,用于在程序中向其它进程或者线程发送信号,其函数声明如下:
1 | int kill(pid_t pid, int sig); |
kill()
函数会将参数 sig
指定的信号发送给参数 pid
指定的进程。
函数参数
pid
pid
参数有以下几种情况:- (1)
pid > 0
:将信号发送给进程号为pid
的进程。 - (2)
pid = 0
:将信号发送给和当前进程相同进程组的所有进程,常用于父进程给子进程发送信号。特别注意,发送信号的进程也会收到自己发出的信号。 - (3)
pid = -1
:将信号广播发送给操作系统内的所有进程,例如在操作系统关机时,会向所有的登录窗口广播关机消息。
函数参数
sig
- 表示准备发送的信号码,假如其值为
0
,则不会有任何信号发送出去,但是操作系统会执行错误检查,通常会利用sig
值为0
来检验某个进程是否仍在运行(存活)。
- 表示准备发送的信号码,假如其值为
函数返回值
- 函数执行成功时,返回
0
。 - 函数执行失败时,返回
-1
。 - 当函数参数有误,会返回以下的错误码
errno
EINVAL
:指定的信号码无效(即函数参数sig
不合法)。EPERM
:权限不够,无法发送信号给指定的进程。ESRCH
:函数参数pid
所指定的进程或进程组不存在。
- 函数执行成功时,返回
信号的使用案例
案例代码一
- 在实际开发中,在
main()
函数开始的位置,开发人员通常会先屏蔽掉全部的信号。这么做的目的是不希望程序被干扰,然后再设置开发人员关心的信号的处理函数。 - 程序在运行的过程中,如果按下组合键
ctrl + c
,将向程序发出 SIGINT 信号,其编号是 2。 - 采用 “kill 进程编号” 或者 “killall 程序名” 向程序发出的是 SIGTERM 信号,其编号是 15。
- 采用 “kill -9 进程编号” 向程序发出的是 SIGKILL 信号,其编号是 9,此信号不能被忽略,也无法被捕获,程序将突然终止。
- 设置 SIGINT 和 SIGTERM 两个信号的处理函数,这两个信号通常可以使用同一个处理函数,函数的代码可以是负责释放资源。
1 |
|
案例代码二
1 |
|
Pthread 多线程编程
查找头文件
在 Linux 系统里,pthread.h
头文件的位置一般是 /usr/include/pthread.h
,可以通过以下命令查看头文件的位置
1 | # whereis pthread.h |
案例代码
1 |
|
编译代码
由于 pthread
不是 Linux 系统默认的库,因此链接时需要使用静态库 libpthread.a
。简而言之,在使用 pthread_create()
创建线程,以及调用 pthread_atfork()
函数建立 fork
处理程序时,需要通过 -lpthread
参数链接该库,同时还需要在 C++ 源文件里添加头文件 pthread.h
。
提示
为了可以正常编译使用了 pthread
的项目代码,不同构建工具的使用说明如下:
若使用 G++ 编译 C++ 项目,则编译命令的示例如下:
1 | # 编译代码 |
若使用 CMake 构建 C++ 项目,则 CMakeLists.txt
配置文件的示例内容如下:
1 | set(CMAKE_CXX_FLAGS "-std=c++11 -lpthread") |
运行结果
程序运行输出的结果如下:
1 | main thread: pid 6189 tid 342021952 (0x1462d740) |