Linux 系统编程之四多线程编程

大纲

信号

信号的概念

信号(Signal)是软件中断,是进程之间相互传递消息的一种方法,用于通知进程发生了事件,但是不能给进程传递任何数据。信号产生的原因有很多,在 Linux 系统下,可以用 killkillall 等命令发送信号。

killall 命令被弃用

  • 在很多 Linux 发行版中,killall 命令已经被移除,取而代之的是 pkill 命令。
  • 如果想要终止所有名为 process_name 的进程,可以使用命令 pkill -15 process_name

信号的作用

  • 服务程序运行在后台,如果想让它中止运行,直接杀掉它的进程不是一个好办法。因为程序被杀死的时候,程序突然终止运行,没有时间去执行善后工作(如释放资源)。
  • 如果向服务程序发送一个信号,服务程序收到这个信号后,调用一个自定义函数,在自定义函数中编写善后的代码(如释放资源),那么程序就可以有计划地退出执行。
  • 向服务程序发送 0 的信号,可以检测程序是否存活。

信号的类型

Linux 系统常用的信号类型如下表所示:

信号名信号值默认处理动作发出信号的原因
SIGHUP1A 终端挂起或者控制进程终止
SIGINT2A 键盘中断 ctrl + c 组合快捷键
SIGQUIT3C 键盘的退出键被按下
SIGILL4C 非法指令
SIGABRT6C 由 abort (3) 发出的退出指令
SIGFPE8C 浮点异常
SIGKILL9AEF 采用 “kill -9 进程编号” 强制杀死程序
SIGSEGV11C 无效的内存引用
SIGPIPE13A 管道破裂,写一个没有读端口的管道
SIGALRM14A 由 alarm (2) 发出的信号
SIGTERM15A 采用 “kill 进程编号” 或 “killall 程序名” 杀死程序
SIGUSR110A 用户自定义信号 1
SIGUSR212A 用户自定义信号 2
SIGCHLD17B 子进程结束信号
SIGCONT18 进程继续(曾被停止的进程)
SIGSTOP19DEF 终止进程
SIGTSTP20D 控制终端(tty)上按下停止键
SIGTTIN21D 后台进程企图从控制终端读
SIGTTOU22D 后台进程企图从控制终端写

在上表中,默认处理动作一项中的字母含义如下:

  • 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 所指的信号。

信号的发出

Linux 操作系统提供了 killkillall 命令向应用程序发送信号,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

void EXIT(int signal) {
printf("接收到 %d 信号,程序退出。\n", signal);
// 正常退出程序
exit(0);
}

int main() {
// 忽略所有信号,不希望程序被干扰
for (int i=1; i<= 64; i++) {
signal(i, SIG_IGN);
}

// 设置 ctrl + c、kill、killall 的处理函数
signal(SIGINT, EXIT);
signal(SIGTERM, EXIT);

while (1) {
printf("执行了一次任务。\n");
sleep(1);
}
}

案例代码二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>

void handler(int signal) {
printf("程序接收到 %d 信号。\n");
}

void alarmfunc(int signal) {
printf("接收到时钟信号%d。\n", signal);
}

int main() {
for (int i=0; i<=64; i++) {
signal(i, handler);
}

// 忽略指定的信号
signal(15, SIG_IGN);

// 恢复指定的信号
signal(15, SIG_DFL);

// 处理时钟信号
signal(SIGALRM, alarmfunc);

// 定时发出时钟信号
alarm(3);

while (1) {
printf("执行了一次任务。\n");
sleep(1);
}
}

Pthread 多线程编程

查找头文件

在 Linux 系统里,pthread.h 头文件的位置一般是 /usr/include/pthread.h,可以通过以下命令查看头文件的位置

1
# whereis pthread.h

案例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

using namespace std;

void printids(const char *s) {
pid_t pid = getpid();
pthread_t tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int) pid, (unsigned int) tid, (unsigned int) tid);
}

void *thr_fn(void *args) {
printids("new thread: ");
return ((void *) 0);
}

int main() {
pthread_t ntid;
int err = pthread_create(&ntid, NULL, thr_fn, NULL);
if (err != 0) {
printf("can't create thread: %d\n", err);
exit(1);
}
printids("main thread: ");
sleep(1);
return 0;
}

编译代码

由于 pthread 不是 Linux 系统默认的库,因此链接时需要使用静态库 libpthread.a。简而言之,在使用 pthread_create() 创建线程,以及调用 pthread_atfork() 函数建立 fork 处理程序时,需要通过 -lpthread 参数链接该库,同时还需要在 C++ 源文件里添加头文件 pthread.h

提示

为了可以正常编译使用了 pthread 的项目代码,不同构建工具的使用说明如下:

若使用 G++ 编译 C++ 项目,则编译命令的示例如下:

1
2
# 编译代码
$ g++ main.cpp -o main -lpthread

若使用 CMake 构建 C++ 项目,则 CMakeLists.txt 配置文件的示例内容如下:

1
2
3
set(CMAKE_CXX_FLAGS "-std=c++11 -lpthread")

add_executable(main main.cpp)

运行结果

程序运行输出的结果如下:

1
2
main thread:  pid 6189 tid 342021952 (0x1462d740)
new thread: pid 6189 tid 324765440 (0x135b8700)