声明:本章内容作为了解
pthread_create 的其中一个参数类型,指定线程属性,包含一下属性:
(1) Detach state
线程的分离属性,表示线程结束的时候,是否回收资源(比如它所使用的栈、线程结构体、线程过程函数的返回值等等)。
PTHREAD_CREATE_JOINABLE
:默认属性,表示结束的时候不回收资源,需要调用 pthread_join 函数显式回收。PTHREAD_CREATE_DETACHED
:表示线程结束了,直接释放所占用的资源,这种情况下不可再使用 pthread_join 函数去 wait 它了。(2) Scope
线程竞争域,该属性有两种情况:
PTHREAD_SCOPE_SYSTEM
:表示线程和整个操作系统中的线程进行竞争,比如 CPU (在调度的时候,考虑系统中所有的线程)
PTHREAD_SCOPE_PROCESS
: 表示线程只和当前进程内的线程进行竞争,比如线程调度的时候,只考虑当前进程中的线程。
在 Linux 中,只支持 PTHREAD_SCOPE_SYSTEM
,因为 Linux 中实现的线程,实际上就是轻量级进程。
用于设置和获取 scope 值的函数如下:
int pthread_attr_setscope(pthread_attr_t *attr,int scope);
int pthread_attr_getscope(const pthread_attr_t *attr,int *scope);
(3) Inherit scheduler
调度继承性,该属性有两种情况:
PTHREAD_INHERIT_SCHED
: 继承进程的调度策略PTHREAD_EXPLICIT_SCHED
: 不使用继承的调度策略,而是使用自己提供的调度策略。用于设置和获取 inherit scheduler 值的函数如下:
int pthread_attr_getinheritsched(const pthread_attr_t *attr,int *inheritsched);
int pthread_attr_setinheritsched(pthread_attr_t *attr,int inheritsched);
(4) Scheduling policy
调度策略,该属性有以下几个值:
SCHED_OTHER
SCHED_BATCH
SCHED_IDLE
SCHED_FIFO
SCHED_RR
这些调度策略可分成两大类:
SCHED_OTHER
、SCHED_BATCH
和SCHED_IDLE
这三种。SCHED_FIFO
和SCHED_RR
。关于这些调度策略的含义,本文不详细讨论。
用于设置和获取 policy 值的函数如下:
int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
(5) Scheduling priority
调度优先级,通常该值通过下面的函数设定和获取。
pthread_attr_setschedparm(pthread_attr_t *attr,
const struct sched_param *param)
pthread_attr_getschedparm(pthread_attr_t *attr,
struct sched_param *param)
struct sched_param {
...
int sched_priority;
...
}
对于普通调度策略来说,该值只能是 0。对于动态优先级来说,该值的范围是 [1,99] 但是 POSIX 规定,该范围需要通过 sched_get_priority_min 和 sched_get_priority_max 函数来获取。在具体实现中,最小是 32。
(6) Guard size
警界缓冲区大小,默认是一个 PAGE_SIZE(4096KB). 用来设置和得到线程栈末尾的警戒缓冲区大小。如果线程栈运行到了警界区,就会收到信号。
可以使用下面两个函数来对其进行设置和获取:
int pthread_attr_getguardsize(const pthread_attr_t *attr, size_t *guardsize);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
(7) Stack address and Stack size
Stack address 表示栈起始地址(栈最低内存地址),一般来说在我们的 x86 或 x64 处理器上,栈都是往低地址的方向生长,所以该值一般表示栈的末尾(栈顶)。
可以使用下面两个函数来对其进行设置和获取:
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
Stack size 表示栈的大小,还可以使用下面的函数:
对于栈的大小,还可以使用下面的函数:
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
实验:
程序 threadattr 可以用来查看线程属性。该程序可以通过命令行传入一个参数,表示栈大小。另外,带参数的情况下,还会修改线程很多默认属性。
// threadattr.c
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#include
#include
#include
#include
#include
#define handle_error_en(en, msg) \
do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)
static void display_pthread_attr(pthread_attr_t* attr, char* prefix)
{
int s, i;
size_t v;
void* stkaddr;
struct sched_param sp;
s = pthread_attr_getdetachstate(attr, &i);
if (s != 0)
handle_error_en(s, "pthread_attr_getdetachstate");
printf("%sDetach state = %s\n", prefix,
(i == PTHREAD_CREATE_DETACHED) ? "PTHREAD_CREATE_DETACHED" :
(i == PTHREAD_CREATE_JOINABLE) ? "PTHREAD_CREATE_JOINABLE" :
"???");
s = pthread_attr_getscope(attr, &i);
if (s != 0)
handle_error_en(s, "pthread_attr_getscope");
printf("%sScope = %s\n", prefix,
(i == PTHREAD_SCOPE_SYSTEM) ? "PTHREAD_SCOPE_SYSTEM" :
(i == PTHREAD_SCOPE_PROCESS) ? "PTHREAD_SCOPE_PROCESS" :
"???");
s = pthread_attr_getinheritsched(attr, &i);
if (s != 0)
handle_error_en(s, "pthread_attr_getinheritsched");
printf("%sInherit scheduler = %s\n", prefix,
(i == PTHREAD_INHERIT_SCHED) ? "PTHREAD_INHERIT_SCHED" :
(i == PTHREAD_EXPLICIT_SCHED) ? "PTHREAD_EXPLICIT_SCHED" :
"???");
s = pthread_attr_getschedpolicy(attr, &i);
if (s != 0)
handle_error_en(s, "pthread_attr_getschedpolicy");
printf("%sScheduling policy = %s\n", prefix,
(i == SCHED_OTHER) ? "SCHED_OTHER" :
(i == SCHED_FIFO) ? "SCHED_FIFO" :
(i == SCHED_RR) ? "SCHED_RR" :
"???");
s = pthread_attr_getschedparam(attr, &sp);
if (s != 0)
handle_error_en(s, "pthread_attr_getschedparam");
printf("%sScheduling priority = %d\n", prefix, sp.sched_priority);
s = pthread_attr_getguardsize(attr, &v);
if (s != 0)
handle_error_en(s, "pthread_attr_getguardsize");
printf("%sGuard size = %d bytes\n", prefix, v);
s = pthread_attr_getstack(attr, &stkaddr, &v);
if (s != 0)
handle_error_en(s, "pthread_attr_getstack");
printf("%sStack address = %p\n", prefix, stkaddr);
printf("%sStack size = 0x%x bytes\n", prefix, v);
}
static void* thread_start(void* arg)
{
int s;
pthread_attr_t gattr;
/* pthread_getattr_np() is a non-standard GNU extension that
* retrieves the attributes of the thread specified in its
* first argument */
s = pthread_getattr_np(pthread_self(), &gattr);
if (s != 0)
handle_error_en(s, "pthread_getattr_np");
printf("Thread attributes:\n");
display_pthread_attr(&gattr, "\t");
exit(EXIT_SUCCESS); /* Terminate all threads */
}
int main(int argc, char* argv[])
{
pthread_t thr;
pthread_attr_t attr;
pthread_attr_t* attrp; /* NULL or &attr */
int s;
attrp = NULL;
/* If a command-line argument was supplied, use it to set the
* stack-size attribute and set a few other thread attributes,
* and set attrp pointing to thread attributes object */
if (argc > 1) {
int stack_size;
void* sp;
attrp = &attr;
s = pthread_attr_init(&attr);
if (s != 0)
handle_error_en(s, "pthread_attr_init");
s = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (s != 0)
handle_error_en(s, "pthread_attr_setdetachstate");
s = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
if (s != 0)
handle_error_en(s, "pthread_attr_setinheritsched");
stack_size = strtoul(argv[1], NULL, 0);
s = posix_memalign(&sp, sysconf(_SC_PAGESIZE), stack_size);
if (s != 0)
handle_error_en(s, "posix_memalign");
printf("posix_memalign() allocated at %p\n", sp);
s = pthread_attr_setstack(&attr, sp, stack_size);
if (s != 0)
handle_error_en(s, "pthread_attr_setstack");
}
s = pthread_create(&thr, attrp, &thread_start, NULL);
if (s != 0)
handle_error_en(s, "pthread_create");
if (attrp != NULL) {
s = pthread_attr_destroy(attrp);
if (s != 0)
handle_error_en(s, "pthread_attr_destroy");
}
pause(); /* Terminates when other thread calls exit() */
}
$ gcc threadattr.c -o threadattr -lphtread
$ ./threadattr
Thread attributes:
Detach state = PTHREAD_CREATE_JOINABLE
Scope = PTHREAD_SCOPE_SYSTEM
Inherit scheduler = PTHREAD_INHERIT_SCHED
Scheduling policy = SCHED_OTHER
Scheduling priority = 0
Guard size = 4096 bytes
Stack address = 0x7ffa105ae000
Stack size = 0x800000 bytes
$ ./threadattr 0x300000
posix_memalign() allocated at 0x7fa63f168000
Thread attributes:
Detach state = PTHREAD_CREATE_DETACHED
Scope = PTHREAD_SCOPE_SYSTEM
Inherit scheduler = PTHREAD_EXPLICIT_SCHED
Scheduling policy = SCHED_OTHER
Scheduling priority = 0
Guard size = 0 bytes
Stack address = 0x7fa63f168000
Stack size = 0x300000 bytes
用于线程互斥的互斥量也有相应的属性 pthread_mutexattr_t,这里只讨论三个方面:
互斥量属性的数据类型是 pthread_mutexattr_t,下面两个函数分别用于互斥量属性的初始化与回收。
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
除了互斥量有共享属性外,其它的线程互斥同步对象如读写锁、自旋锁、条件变量、屏障都有共享属性。该属性有两种情况:
PTHREAD_PROCESS_PRIVATE
: 默认值,表示互斥量只能在本进程内部使用。PTHREAD_PROCESS_SHARED
:表示互斥量可以在不同进程间使用。对于第一种情况,我们早已经学会。第二种情况,需要结合前面的进程间通信技术才有用。一般需要在共享内存中分配互斥量,然后再为互斥量指定 PTHREAD_PROCESS_SHARED
属性就可以了。
实验:
本实验中的程序分为了 4 个部分,分别是 init、destroy、buyticket 和 rbstbuyticket.
init 程序的作用是申请共享内存,并在共享内存中分配互斥量等。
destroy 用来回收共享内存中的互斥量,并销毁共享内存。
buyticket 和 rbstbuyticket 是从共享内存的数据中抢票的。
.
|- init.c
|- destroy.c
|- buyticket.c
|- rbstbuyticket.c
|- Makefile // Makefile 文件主要用来一次性编译完所有文件。
本部分实验,只用的到前三个程序,最后一个 rbstbuyticket.c 在讲鲁棒属性的时候用的到。
程序 init 用来创建一块共享内存,可以给 init 程序传递一个命令行参数,也可以不传。如果传递参数,表示为互斥量指定鲁棒性属性,我们先不传递参数。
// init.c
#include
#include
#include
#include
#include
#include
#include
#define PERR(msg) do { perror(msg); exit(-1); } while(0)
#define PPERR(err, msg) do { err = errno; perror(msg); exit(-1); } while(0)
struct ticket {
int remain;
pthread_mutex_t lock;
};
// 打印共享属性
void printshared(pthread_mutexattr_t* attr) {
int err, shared;
err = pthread_mutexattr_getpshared(attr, &shared);
if (err != 0) PPERR(err, "pthread_mutexattr_getshared");
if (shared == PTHREAD_PROCESS_PRIVATE)
puts("shared = PTHREAD_PROCESS_PRIVATE");
else if (shared == PTHREAD_PROCESS_SHARED)
puts("shared = PTHREAD_PROCESS_SHARED");
else
puts("shared = ???");
}
// 打印鲁棒属性
void printrobust(pthread_mutexattr_t* attr) {
int err, robust;
err = pthread_mutexattr_getrobust(attr, &robust);
if (err != 0) PPERR(err, "pthread_mutexattr_getrobust");
if (robust == PTHREAD_MUTEX_STALLED)
puts("robust = PTHREAD_MUTEX_STALLED");
else if (robust == PTHREAD_MUTEX_ROBUST)
puts("robust = PTHREAD_MUTEX_ROBUST");
else
puts("robust = ???");
}
int main(int argc, char* argv[]) {
int err, shared, robust = 0, flag = 1;
if (argc >= 2) robust = 1;
key_t key = 0x8888;
// 创建共享内存
int id = shmget(key, getpagesize(), IPC_CREAT | IPC_EXCL | 0666);
if (id < 0) PERR("shmget");
// 挂接共享内存
struct ticket* t = (struct ticket*)shmat(id, NULL, 0);
if ((int)t == -1) PERR("shmat");
// 设置余票数量为 5
t->remain = 5;
pthread_mutexattr_t mutexattr;
err = pthread_mutexattr_init(&mutexattr);
if (err != 0) PPERR(err, "pthread_mutexattr_init");
printshared(&mutexattr);
printrobust(&mutexattr);
// 将互斥量的共享属性设置为可以进程间共享使用。
shared = PTHREAD_PROCESS_SHARED;
err = pthread_mutexattr_setpshared(&mutexattr, shared);
if (err != 0) PPERR(err, "pthread_mutexattr_setshared");
// 如果有命令行参数,就将鲁棒性设置为 PTHREAD_MUTEX_ROBUST
// 本文暂时不设置此值
if (robust) {
err = pthread_mutexattr_setrobust(&mutexattr, PTHREAD_MUTEX_ROBUST);
if (err != 0) PPERR(err, "pthread_mutexattr_setshared");
}
puts("modify attribute ------------------>");
printshared(&mutexattr);
printrobust(&mutexattr);
pthread_mutex_init(&t->lock, &mutexattr);
err = pthread_mutexattr_destroy(&mutexattr);
if (err != 0) PPERR(err, "pthread_mutexattr_destroy");
err = shmdt((void*)t);
if (err != 0) PERR("shmdt");
return 0;
}
destroy 程序主要用于回收互斥量和共享内存。
#include
#include
#include
#include
#include
#include
#include
#define PERR(msg) do { perror(msg); exit(-1); } while(0)
#define PPERR(err, msg) do { err = errno; perror(msg); exit(-1); } while(0)
struct ticket {
int remain;
pthread_mutex_t lock;
};
int main() {
int err;
key_t key = 0x8888;
int id = shmget(key, 0, 0);
if (id < 0) PERR("shmget");
struct ticket* t = (struct ticket*)shmat(id, NULL, 0);
if ((int)t == -1) PERR("shmat");
err = pthread_mutex_destroy(&t->lock);
if (err != 0) PPERR(err, "pthread_mutex_destroy");
err = shmdt((void*)t);
if (err != 0) PERR("shmdt");
err = shmctl(id, IPC_RMID, NULL);
if (err != 0) PERR("shmctl");
return 0;
}
buyticket 程序主要用于抢票,该程序需要从命令行传递参数,表示抢票人的名字。比如./buyticket skx
表示抢票人是 skx
#include
#include
#include
#include
#include
#include
#include
#define PERR(msg) do { perror(msg); exit(-1); } while(0)
#define PPERR(err, msg) do { err = errno; perror(msg); exit(-1); } while(0)
struct ticket {
int remain;
pthread_mutex_t lock;
};
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s \n" , argv[0]);
exit(-1);
}
char* name = argv[1];
int err, shared, flag = 1;
key_t key = 0x8888;
int id = shmget(key, 0, 0);
if (id < 0) PERR("shmget");
struct ticket* t = (struct ticket*)shmat(id, NULL, 0);
if ((int)t == -1) PERR("shmat");
// 只要票数大于 0 就不断抢票。
while (flag) {
pthread_mutex_lock(&t->lock);
int remain = t->remain;
if (remain > 0) {
sleep(1);
printf("%s buy a ticket\n", name);
--remain;
sleep(3);
t->remain = remain;
}
else flag = 0;
pthread_mutex_unlock(&t->lock);
sleep(2);
}
err = shmdt((void*)t);
if (err != 0) PERR("shmdt");
return 0;
}
main:init destroy buyticket
init:init.c
gcc init.c -o init -lpthread
destroy:destroy.c
gcc destroy.c -o destroy -lpthread
buyticket:buyticket.c
gcc buyticket.c -o buyticket -lpthread
注意,上面所有以 gcc 开头的行前面是个 tab 符号,即按下键盘上的 Tab 键所产生的,千万不能是空格。
完成些文件后,保存退出,然后在命令行键入
$ make
此时就会产生三个程序:init、destroy 和 buyticket
初始化
./init
开启两个进程进行抢票
开启两个终端假设为终端 A 和终端 B. 在终端 A 中键入
./buyticket allen
终端 B 中键入
./buyticket luffy
记住这两个进程在执行时间上不能差的太远,别等到其中一个运行结束才开始另一个。
运行结果
最后别忘记使用 ./destroy
对共享内存进行回收。
上面的程序中,当 skx 抢到第 2 张票后,立即按下 CTRL + C 中断进程,会有什么后果?
结果:如果其中一个进程在未释放互斥量的情况下挂掉了,将会导致另一个线程永远无法获得锁,然后就死锁了。
为了能够让进程在异常终止时,释放掉互斥锁,需要指定 ROBUST 属性。所谓的 ROBUST,指是的健壮的意思。
可以通过下面两个函数设置和获取互斥量的鲁棒属性:
int pthread_mutexattr_getrobust(const pthread_mutexattr_t *attr, int *restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);
在指定 robust 属性的情况下,如果其中某个进程在未释放锁的情况下退出了,另一个进程仍然可以获得锁,但是此时 pthread_mutex_lock 将返回 EOWNERDEAD
,通知获得锁的线程,有一个其它进程的线程挂掉了,互斥量现在变成了 inconsistent 的状态。这时候,需要对互斥量做 consistent 处理,否则,一旦再次解锁后,互斥量将永久不可用。
翻译成代码就是这样的:
if (EOWNERDEAD == pthread_mutex_lock(&lock)) {
pthread_mutex_consistent(&lock);
}
consistent 函数原型如下:
int pthread_mutex_consistent(pthread_mutex_t *mutex);
它表示将 robust mutex 标记为 consistent 状态。
实验:
我们仍使用上一篇文章中的 init、destroy 程序,下面给出 rbstbuyticket.c 的代码。
#include
#include
#include
#include
#include
#include
#include
#define PERR(msg) do { perror(msg); exit(-1); } while(0)
#define PPERR(err, msg) do { err = errno; perror(msg); exit(-1); } while(0)
struct ticket {
int remain;
pthread_mutex_t lock;
};
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s \n" , argv[0]);
exit(-1);
}
char* name = argv[1];
int err, shared, flag = 1;
key_t key = 0x8888;
int id = shmget(key, 0, 0);
if (id < 0) PERR("shmget");
struct ticket* t = (struct ticket*)shmat(id, NULL, 0);
if ((int)t == -1) PERR("shmat");
while (flag) {
if (EOWNERDEAD == (err = pthread_mutex_lock(&t->lock))) {
puts("EOWNERDEAD");
pthread_mutex_consistent(&t->lock);
}
else if (ENOTRECOVERABLE == err) {
puts("ENOTRECOVERABLE");
}
int remain = t->remain;
if (remain > 0) {
sleep(1);
printf("%s buy a ticket\n", name);
--remain;
t->remain = remain;
sleep(3);
}
else flag = 0;
pthread_mutex_unlock(&t->lock);
sleep(2);
}
return 0;
}
添加了两行,现在如下:
main:init destroy buyticket rbstbuyticket
init:init.c
gcc init.c -o init -lpthread
destroy:destroy.c
gcc destroy.c -o destroy -lpthread
buyticket:buyticket.c
gcc buyticket.c -o buyticket -lpthread
rbstbuyticket:rbstbuyticket.c
gcc rbstbuyticket.c -o rbstbuyticket -lpthread
$ make
互斥量的类型属性通常有四种:
其中第一种和第四种一般都是一样的,宏定义的值相同,是默认情况。第二种提供错误检查。第三种是我们本文需要讨论的。
可以使用下面的函数对互斥量的类型属性进行设置和获取:
int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
一般情况下,我们在同一个线程中对同一个互斥量加两次锁,就会死锁。如果将互斥量类型属性设置为递归类型 PTHREAD_MUTEX_RECURSIVE
就不会出现此问题。
递归互斥量内部维护着一个计数器,当互斥量未上锁时,计数器值为 0。只有计数器为 0 的情况下,线程才能够获得锁。只有获得锁的线程,才能持续对互斥量加锁,每加一次锁,计数器的值加 1,每解一次锁,计数器的值减 1。
实验
程序 recsig 的功能是在信号处理函数内部对余票数量加 1。操作余票的时候,需要加锁。recsig 程序可以从命令行接受一个参数,表示使用递归互斥量,如果不传参数,表示使用普通互斥量。
resig 函数注册了两种信号,第一种是 SIGUSR1,另一种是 SIGINT。当程序启动的时候,会自己给自己发送 SIGUSR1 信号。
#include
#include
#include
#include
#include
#include
#define PPERR(err, msg) do { errno = err; perror(msg); exit(-1); } while(0)
struct ticket {
int remain;
pthread_mutex_t lock;
};
struct ticket t;
void printtype(pthread_mutexattr_t* attr) {
int err, type;
char* s = "???";
err = pthread_mutexattr_gettype(attr, &type);
if (err != 0) PPERR(err, "pthread_mutexattr_gettype");
if (type == PTHREAD_MUTEX_NORMAL) {
s = "PTHREAD_MUTEX_NORMAL";
printf("MUTEX TYPE = %s\n", s);
}
if (type == PTHREAD_MUTEX_ERRORCHECK) {
s = "PTHREAD_MUTEX_ERRORCHECK";
printf("MUTEX TYPE = %s\n", s);
}
if (type == PTHREAD_MUTEX_RECURSIVE) {
s = "PTHREAD_MUTEX_RECURSIVE";
printf("MUTEX TYPE = %s\n", s);
}
if (type == PTHREAD_MUTEX_DEFAULT) {
s = "PTHREAD_MUTEX_DEFAULT";
printf("MUTEX TYPE = %s\n", s);
}
}
void setrecursive(pthread_mutexattr_t* attr) {
int err, type;
type = PTHREAD_MUTEX_RECURSIVE;
err = pthread_mutexattr_settype(attr, type);
if (err != 0) PPERR(err, "pthread_mutexattr_settype");
}
void handler(int sig) {
if (sig == SIGUSR1) puts("receive SIGUSR1");
else if (sig == SIGINT) puts("receive SIGINT");
pthread_mutex_lock(&t.lock);
printf("%s enter handler\n", sig == SIGUSR1 ? "SIGUSR1" : "SIGINT");
t.remain++;
sleep(3);
printf("%s exit handler\n", sig == SIGUSR1 ? "SIGUSR1" : "SIGINT");
pthread_mutex_unlock(&t.lock);
}
int main(int argc, char* argv[]) {
int recursive = 0;
if (argc >= 2) recursive = 1;
t.remain = 3;
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
printtype(&attr);
if (recursive == 1) {
puts("modify type --------------------->");
setrecursive(&attr);
printtype(&attr);
}
pthread_mutex_init(&t.lock, &attr);
pthread_mutexattr_destroy(&attr);
signal(SIGUSR1, handler);
signal(SIGINT, handler);
puts("send SIGUSR1");
kill(getpid(), SIGUSR1);
pthread_mutex_destroy(&t.lock);
printf("remain = %d\n", t.remain);
return 0;
}
$ gcc recsig.c -o recsig -lpthread
不带参数运行
$ ./recsig
当进入信号处理函数的时候,按下 CTRL+ C,接下来,程序死锁。原因在于信号处理函数在执行的时候,收到了 SIGINT 信号,又加了一次锁,注意它们都属于同一个线程,所以导致了死锁。
$ ./recsig 1
当进入信号处理函数的时候,按下 CTRL+ C,程序正常执行完成。
当进入信号处理函数的时候,按下 CTRL+ C,程序正常执行完成。
读写锁唯一的属性就是进程共享属性,这和互斥量进程共享属性是一样一样的。它有两种情况:
相关函数如下:
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_getshared(const pthread_rwlockattr_t *attr, int *pshared);
int pthread_rwlockattr_setshared(pthread_rwlockattr_t *attr, int pshared);
条件变量属性的初始化函数:
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
进程共享属性
它有两种情况:
相关函数如下:
int pthread_condattr_getshared(const pthread_condattr_t *attr, int *pshared);
int pthread_condattr_setshared(pthread_condattr_t *attr, int pshared);
时钟属性
这个属性主要针对 pthread_cond_timewait 函数,它用来控制采用的是哪个时钟。
时钟 ID | 选项 | 说明 |
---|---|---|
CLOCK_REALTIME | 实时系统时间(类似于 time 函数) | |
CLOCK_MONOTONIC | _POSIX_MONOTONIC_CLOCK | 没有负跳数的实时系统时间 |
CLOCK_PROCESS_CPUTIME_ID | _POSIX_CPUTIME | 调用进程的 CPU 时间 |
CLOCK_THREAD_CPUTIME_ID | _POSIX_THREAD_CPUTIME | 调用线程的 CPU 时间 |
相关函数如下:
int pthread_condattr_getclock(const pthread_condattr_t *attr, clockid_t *clock_id);
int pthread_condattr_setclock(pthread_condattr_t *attr, clockid_t clock_id);
barrier 唯一的属性就是进程共享属性,同互斥量进程共享属性是一样,它有两种情况:
相关函数如下:
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);
int pthread_barrierattr_getshared(const pthread_barrierattr_t *attr, int *pshared);
int pthread_barrierattr_setshared(pthread_barrierattr_t *attr, int pshared);
这节了解即可
errno 变量一直被用于调用系统函数时出错后,被赋上错误码的整数变量。
一直以来我们认为 errno 变量都被定义为进程上下文中的一个全局整数。在使用 errno 的时候,通常会使用 #include
将其包含进来。
诸位有没有想过,既然它是全局的,在多线程中会不会出问题?为此先做一个实验进行验证。
实验:
在两个不同的线程中改写 errno 的值,另外采用了一个我们自定义的 mydata 变量进行对比。
// errno.c
#include
#include
#include
#include
int mydata;
void* fun1() {
errno = 5;
mydata = 5;
sleep(1);
printf("fun1: errno = %d, mydata = %d\n", errno, mydata);
return NULL;
}
void* fun2() {
errno = 10;
mydata = 10;
sleep(1);
printf("fun2: errno = %d, mydata = %d\n", errno, mydata);
return NULL;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, fun1, NULL);
pthread_create(&tid2, NULL, fun2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
$ gcc errno.c -o errno -lpthread
$ ./errno
fun1: errno = 5, mydata = 10
fun2: errno = 10, mydata = 10
上面的实验证明了 errno 变量是多线程安全的。
本文内容以及后面的几篇文章都是在为 errno 的实现做铺垫。
有时候,在执行多线程程序的时候,可能需要对某些共享资源进行初始化,一般来说,我们有两种方式:
对于第一种方式,肯定是最常用的了,不过有时候我们并不想这么做。因为一开始可能并不需要多线程程序,所以希望我们将多线程所使用的共享资源初始化动作推迟到线程函数中。如此将会导致一个问题,每个线程都执行初始化,岂不是被初始化很多次?
它的代码看起来像下面这样:
int* foo;
void thread_init() {
foo = (int*)malloc(sizeof(int));
...
}
void* th_fn1(void* arg) {
thread_init();
...
}
void* th_fn2(void* arg) {
thread_init();
...
}
如果直接这样做的话, foo 指针就会被多次分配内存,导致错误。有没有一种办法,让 thread_init 只执行一次?Linux 已经为我们准备了一套解决方案 —— pthread once.
在这一套技术方案中,有两个基本要素:
PTHREAD_ONCE_INIT
每个线程函数调用初始化接口。初始化接口函数定义如下:
int phtread_once(pthread_once_t *initflag, void (*initfn)(void));
它的两个参数,就是上面讲到的两个基本要素。
实验:
在 once 程序中,我们只证明,thread_init 函数只会被执行一次。
// once.c
#include
#include
#include
pthread_once_t init_done = PTHREAD_ONCE_INIT;
// 只被执行一次的函数
void thread_init() {
puts("I'm thread init");
}
void* fun(void* arg) {
pthread_once(&init_done, thread_init);
printf("Hello, I'm %s\n", (char*)arg);
return NULL;
}
int main() {
pthread_t tid1, tid2, tid3;
pthread_create(&tid1, NULL, fun, "allen");
pthread_create(&tid2, NULL, fun, "luffy");
pthread_create(&tid3, NULL, fun, "zoro");
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_join(tid3, NULL);
return 0;
}
编译运行
$ gcc once.c -o once -lpthread
$ ./once
I'm thread init
Hello, I'm allen
Hello, I'm luffy
Hello, I'm zoro
可以看到 thread_init 初始化函数只执行了一次
系统为每一个线程提供了一个私有“容器”,该容器中保存的是一个个的键值对。
键的类型是 pthread_key_t,它只能通过函数 pthread_key_create 进行初始化。它的定义如下:
int pthread_key_create(pthread_key_t *key, void (*destructor(void*));
键可被任意一个线程使用。
键可被任意一个线程使用。
注意到上面的函数还有第二个参数,它需要传递一个析构函数,比如像下面这样:
void destructor(void* arg) {
...
}
当线程运行结束(return 或 pthread_exit)时,该函数会自动调用。析构函数的参数,是与该键关联的值(value)。
pthread_key_create 第二个参数可以为空。
通过 pthread_key_delete 函数,可能删除指定的键:
int pthread_key_delete(pthread_key_t key);
注意,调用上面的函数,不会引起前面的析构函数的调用。
注意,调用上面的函数,不会引起前面的析构函数的调用。
线程容器中的值类型必须是 void*。可以通过下面的函数将键值对保存到线程自己的容器中。
int pthread_setspecific(pthread_key_t key, const void *value);
不同线程调用上面的函数时,只会将键值对保存到自己(指线程自己)的容器中。
不同线程调用上面的函数时,只会将键值对保存到自己(指线程自己)的容器中。
所以,我们把这种键值对称为线程私有数据。
可以通过下面的函数根据键来获取对应的值:
void* pthread_getspecific(pthread_key_t key);
注意,获取到的值类型仍然是 void* 类型。
实验:
// specdata.c
#include
#include
pthread_key_t key;
void destructor(void* arg) {
printf("destructor: hello %d\n", (int)arg);
}
void* fun1(void* arg) {
int data = 5;
pthread_setspecific(key, (void*)data);
int x = (int)pthread_getspecific(key);
printf("fun1: x = %d\n", x);
}
void* fun2(void* arg) {
int data = 10;
pthread_setspecific(key, (void*)data);
int x = (int)pthread_getspecific(key);
printf("fun2: x = %d\n", x);
}
int main() {
pthread_t tid1, tid2;
// 初始化 key
pthread_key_create(&key, destructor);
pthread_create(&tid1, NULL, fun1, NULL);
pthread_create(&tid2, NULL, fun2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
注意,获取到的值类型仍然是 void* 类型。
编译运行
$ gcc specdata.c -o specdata -lpthread
$ ./specdata
fun2: x = 10
destructor: hello 10
fun1: x = 5
destructor: hello 5
可以看到,线程 fun1 和 fun2 中各自都向其容器中保存了不同的数据,然后又根据 key 将其提取出来并打印。线程运行结束后,会调用 destructor 函数,该函数的参数就是容器中的值。
实际上,如果一个线程容器中有多个键值对,当线程结束时,会多次调用 destructor 函数,调用的顺序和你初始化(pthread_key_create)键的顺序是一致的。
讲一下用到的技术吧:
本质上 errno 并不是一个真正意义上的变量,而是通过宏定义扩展为语句,而这一行语句实际上是在调用函数,该函数返回保存了指向 errno 变量的指针。
实验:
// myerrno.c
#include
#include
#include
#include
#include
// 实际上 myerrno 就是一个宏定义
#define myerrno (*_myerrno())
pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
// 使用 pthread once 对键进行初始化
void thread_init() {
puts("I'm thread_init");
pthread_key_create(&key, free); // 这里注册了析构函数就是 free
}
// 该函数用来获取真正的 myerrno 的地址
int* _myerrno() {
int* p;
pthread_once(&init_done, thread_init);
// 如果根据键拿到的是一个空地址,说明之前还未分配内存
p = (int*)pthread_getspecific(key);
if (p == NULL) {
p = (int*)malloc(sizeof(int));
pthread_setspecific(key, (void*)p);
}
/**************************************/
return p;
}
void* fun1() {
errno = 5;
myerrno = 5; // 这一行被扩展成 (*_myerrno()) = 5
sleep(1);
// printf 后面的 myerrno 会被扩展成 (*_myerrno())
printf("fun1: errno = %d, myerrno = %d\n", errno, myerrno);
return NULL;
}
void* fun2() {
errno = 10;
myerrno = 10; // 这一行被扩展成 (*_myerrno()) = 10
printf("fun2: errno = %d, myerrno = %d\n", errno, myerrno);
return NULL;
}
int main() {
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, fun1, NULL);
pthread_create(&tid2, NULL, fun2, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
return 0;
}
$ gcc myerrno.c -o myerrno -lpthread
$ ./myerrno
I'm thread_init
fun2: errno = 10, myerrno = 10
fun1: errno = 5, myerrno = 5
pthread_cancel 是用来终止指定的线程的,调用 pthread_cancel 后线程什么时候终止呢?
本文主要目的就是验证 pthread_cancel 的退出时机。
实验:
下面的几个程序,在线程中编写了计算密集型程序(一个很大的循环),循环执行完毕后,打印该循环消耗的时间。接下来,又打印了一行。
如果 cannot be canceled 打印出来,表明线程并未被取消。
// cancel.c
#include
#include
#include
void* fun(void *arg) {
int i, j, start;
start = time(0);
for (i = 0; i < 1000; ++i) {
for (j = 0; j < 1000000; ++j);
}
printf("finished, consume %ld s\n", time(0) - start);
printf("cannot be canceled\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, fun, NULL);
pthread_cancel(tid);
puts("cancel thread");
pthread_join(tid, NULL);
return 0;
}
$ gcc cancel.c -o cancel -lpthread
$ ./cancel
cancel thread
finished, consume 2 s
从图 1 中可以看到,当主线程发出取消请求后,线程并未立即停止,而是一直等到打印了 finished, consume 2 s 才停止。
取消点
出现上面的结果,原因在于 pthread_cancel 在默认情况下,只是通知线程,至于线程什么时候会取消,只有在遇到了取消点(把它想象成某个函数)的时候才会停止。在密集计算的循环中,并没有调用任何取消点函数。
当执行第一个 printf 函数时,线程退出了,说明在 printf 的调用链中,肯定有一个步骤调用了取消点函数。实际上,printf 在底层是调用了 write 接口,write 函数中存在取消点。
如果一直没有机会调用包含取消点的函数,那就意味着线程即使收到了“取消信号”也不会退出。这时候可以显示的在线程内部调用 void pthread_testcancel(void)
进行测试,如果真的收到了“取消信号”,则退出。
有一种办法,可以让线程还没到达取消点的时候直接退出。这时候需要将线程设置为异步取消的方式。具体方法为在线程内部调用下面的函数:
int pthread_setcanceltype(int type, int *oldtype);
其中,type 的值如下:
其中,type 的值如下:
默认情况下,线程取消方式为默认值——PTHREAD_CANCEL_DEFERRED。要想让线程收到“取消信号”后立即退出,需要将 type 设置为 PTHREAD_CANCEL_ASYNCHRONOUS
实验:
// asyncancel.c
#include
#include
#include
void* fun(void* arg) {
int i, j, start, oldtype;
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldtype);
start = time(0);
for (i = 0; i < 1000; ++i) {
for (j = 0; j < 1000000; ++j);
}
printf("finished, consume %ld s\n", time(0) - start);
printf("cannot be canceled\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, fun, NULL);
sleep(1);
pthread_cancel(tid);
puts("cancel thread");
pthread_join(tid, NULL);
return 0;
}
$ gcc asyncancel.c -o asyncancel -lpthread
$ ./asyncancel
cancel thread
可以看到,printf 函数还未执行,或者说还没遇到取消点,线程就退出了。
有些线程不是你想取消就取消的。就算你发送了 pthread_cancel 通知了,它也不会退出。除非它自己想退出了。
这时候,可以在线程函数中禁止取消。下面的函数可以实现此功能:
int pthread_setcancelstate(int state, int *oldstate);
其中,state 可以为下面的值:
其中,state 可以为下面的值:
默认情况下,state 的值为 PTHREAD_CANCEL_ENABLE,即可以被取消。如果将 state 设置为 PTHREAD_CANCEL_DISABLE,则线程是无法被取消的。
实验:
// disablecancel.c
#include
#include
#include
void* fun(void* arg) {
int i, j, start, oldstate;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
start = time(0);
for (i = 0; i < 1000; ++i) {
for (j = 0; j < 1000000; ++j);
}
printf("finished, consume %ld s\n", time(0) - start);
printf("cannot be canceled\n");
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, fun, NULL);
pthread_cancel(tid);
puts("cancel thread");
pthread_join(tid, NULL);
return 0;
}
$ gcc disablecancel.c -o disablecancel -lpthread
$ ./disablecancel
cancel thread
finished, consume 3 s
cannot be canceled
可以看到,将线程设置为 PTHREAD_CANCEL_DISABLE
后,线程是无法被取消的。
在多线程中,每一个线程都有属于自己的阻塞信号集与未决信号集。当一个线程派生另一个线程的时候,会继承父线程的阻塞信号集,但是不会继承未决信号集,并且新线程会清空未决信号集。
(1) 设置阻塞信号集的函数
在多线程程序中,如果要设置线程的阻塞信号集,不能再使用 sigprocmask 函数,而应该使用 pthread_sigmask,其定义如下:
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
这个函数的用法和 sigprocmask 是一样的。
how 参数
set 参数:表示你指定的信号集合
oldset:返回旧的阻塞信号集
返回值 int:0 表示成功,-1 失败。
(2) 获取未决信号的函数
该函数仍然是 sigpending,没有变化。它的原型如下:
int sigpending(sigset_t *set);
(3) 信号发送函数
kill 函数只能给指定的进程发送函数,而是使用 pthread_kill 可以给指定的线程发送函数。它的原型如下:
2.3 信号发送函数
kill 函数只能给指定的进程发送函数,而是使用 pthread_kill 可以给指定的线程发送函数。它的原型如下:
int pthread_kill(pthread_t thread, int sig);
实验
程序 th_sig 做了下面几个工作:
// th_sig.c
#include
#include
#include
#include
// 打印信号集
void printsigset(const sigset_t* set) {
int i;
for (i = 1; i <= 64; i++) {
if (i == 33) putchar(' ');
if (sigismember(set, i) == 1)
putchar('1');
else
putchar('0');
}
puts("");
}
// fun1 线程
void* fun1(void* arg) {
// 阻塞 SIGINT 信号
sigset_t mask, st;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
pthread_sigmask(SIG_BLOCK, &mask, NULL);
while (1) {
printf("I'm fun1:\t");
sigpending(&st);
printsigset(&st);
sleep(3);
}
}
// fun2 线程
void* fun2(void* arg) {
sigset_t st;
while (1) {
printf("I'm fun2:\t");
sigpending(&st);
printsigset(&st);
sleep(3);
}
}
int main() {
// 创建线程前阻塞 SIGQUIT
sigset_t mask, st;
sigemptyset(&mask);
sigaddset(&mask, SIGQUIT);
pthread_sigmask(SIG_BLOCK, &mask, NULL);
pthread_t tid1, tid2;
pthread_create(&tid1, NULL, fun1, NULL);
sleep(0.1);
pthread_create(&tid2, NULL, fun2, NULL);
sleep(2);
pthread_kill(tid1, SIGINT);
while (1) {
printf("I'm main:\t");
sigpending(&st);
printsigset(&st);
sleep(3);
}
return 0;
}
左侧是线程的名字,右侧是打印的未决队列,也就是未被信号处理函数处理的信号。其中第 1 列是 SIGHUP 信号,第 2 列表示 SIGINT 信号,第 3 列表示 SIGQUIT 信号。
当主线程休眠 2 秒后,给线程 fun1 发送了一个 SIGINT 信号,此时 fun1 的未决信号集中第 2 列变成了 1 (图上的第 4 行)。
在后面某个时候,按下了 CTRL + \,表示发送信号 SIGQUIT 给前台进程组中的进程。接下来,可以发现,所有的线程未决信号集中的第 3 列都变成了 1。这说明了两件事:
在上一节中,我们使用了 sigpending 从未决队列中取出信号,并打印挨个打印,这种做法相当的麻烦,而且不太科学。如果没有未决信号集中一直没有信号,岂不是在浪费 cpu?
幸好,sigwait 函数可以帮我们解决这个问题。
sigwait 函数
如果在线程中调用 sigwait,它会一直等待它指定的信号,直到未决信号集中出现指定的信号为止,同时 sigwait 还会从未决信号集中取出该信号返回,并将该信号从未决非信号集中删除。
如果多线程中调用 sigwait 等待同一个信号,只会有一个线程可以从 sigwait 中返回。
如果 sigwait 要等待的信号被捕获,要么 sigwait 函数返回,要么调用相应的信号处理函数,到底是哪种情况这取决于操作系统实现。一般情况下,我们需要将 sigwait 要等待的信号添加到阻塞集中,以避免歧义。
sigwait 函数原型如下:
int sigwait(const sigset_t *set, int *sig);
参数 set 表示要等待哪些信号,一旦 sigwait 函数返回,会从未决信号集中取出信号,放到参数 sig 指向的内存中。
实验
#include
#include
#include
#include
#include
#include
#define PPERR(err, msg) do { errno = err; perror(msg); exit(-1); } while(0)
void* sig_thread(void* arg) {
sigset_t* st = arg;
int err, sig;
for (;;) {
// sigwait 函数一直等待指定的信号出现(在未决信号集中)
err = sigwait(st, &sig);
if (err != 0) PPERR(err, "sigwait");
// 在这里可以执行相应的处理程序对信号进行处理。
printf("handle sig: %d\n", sig);
}
return NULL;
}
int main() {
int err;
pthread_t tid;
sigset_t st;
sigemptyset(&st);
sigaddset(&st, SIGINT);
sigaddset(&st, SIGTSTP);
// 将我们想要处理的信号加入阻塞集,防止因为默认处理导致进程退出
err = pthread_sigmask(SIG_BLOCK, &st, NULL);
if (err != 0) PPERR(err, "pthread_sigmask");
pthread_create(&tid, NULL, sig_thread, (void*)&st);
pthread_join(tid, NULL);
return 0;
}
$ gcc sigwait.c -o sigwait -lpthread
参数 set 表示要等待哪些信号,一旦 sigwait 函数返回,会从未决信号集中取出信号,放到参数 sig 指向的内存中。
程序启动后,分别按下 CTRL+ C、CTRL + Z、CTRL + \,因为我们的程序没有处理 SIGQUIT 信号,所以按下 CTRL + \ 后,整个进程终止了。
在多线程程序中使用 fork,可能会导致一些意外:
如果在多线程环境中执行 fork,派生的子进程是单线程,子进程中的线程是由父进程中调用 fork 的那个线程的副本构成,而子进程中所有其它的线程会消失。
实验:
程序 th_fork 的功能是在父进程中创建一个线程,不断打印自己的 pid 和 ppid。创建完线程后,父进程在主线程中执行 fork,子进程每 2 秒打印一个点。
// th_fork.c
#include
#include
#include
#include
#include
#define PERR(err, msg) do { errno = err; perror(msg); exit(-1); } while(0)
void* fun(void* arg) {
while (1) {
printf("I'm %d, my father is %d\n", getpid(), getppid());
sleep(1);
}
return NULL;
}
int main() {
int err;
pid_t pid;
pthread_t tid;
pthread_create(&tid, NULL, fun, NULL);
puts("parent about to fork ...");
pid = fork();
if (pid < 0) PERR(errno, "fork");
else if (pid == 0) {
// child
int status;
err = pthread_join(tid, (void**)&status);
if (err != 0) PERR(err, "pthread_join");
while (1) {
puts(".");
sleep(2);
}
exit(0);
}
pthread_join(tid, NULL);
}
编译运行
$ gcc th_fork.c -o th_fork -lpthread
skx@ubuntu:~/pra/learn_linux/102$ ./th_fork
parent about to fork ...
I'm 3081, my father is 2281
.
I'm 3081, my father is 2281
I'm 3081, my father is 2281
.
I'm 3081, my father is 2281
.
I'm 3081, my father is 2281
I'm 3081, my father is 2281
.
I'm 3081, my father is 2281
I'm 3081, my father is 2281
.
I'm 3081, my father is 2281
I'm 3081, my father is 2281
.
I'm 3081, my father is 2281
可以看到,子进程中只有一个线程在打点,子进程的 fun 函数线程已经“蒸发”掉了。
另外,在子进程中调用了 pthread_join 函数并没有报错,看起来就好像是子进程中的 fun 函数线程已经正常结束了。
如果在 fork 的时候,线程未释放持有的锁,将导致死锁。
实验:
程序 fork_lock 演示了这种情况。
// fork_lock.c
#include
#include
#include
#include
#include
#define PERR(err, msg) do { errno = err; perror(msg); exit(-1); } while(0)
int total = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* fun(void* arg) {
while (1) {
pthread_mutex_lock(&lock);
total++;
puts("fun: total++");
// 时间稍稍长一点,在这个时候父进程发起 fork 就导致持锁线程蒸发
sleep(5);
pthread_mutex_unlock(&lock);
sleep(1);
}
return NULL;
}
int main() {
int err;
pid_t pid;
pthread_t tid;
pthread_create(&tid, NULL, fun, NULL);
// 推迟 1 秒,让线程进入临界区。
sleep(1);
puts("parent about to fork ...");
pid = fork();
if (pid < 0) PERR(errno, "fork");
else if (pid == 0) {
// child
int status;
while (1) {
// 由于 fun 线程蒸发,子进程获取锁就会死锁
puts("child require lock...");
pthread_mutex_lock(&lock);
total++;
puts("child: total++");
sleep(2);
pthread_mutex_unlock(&lock);
sleep(1);
}
exit(0);
}
pthread_join(tid, NULL);
}
$ gcc fork_lock.c -o fork_lock -lpthread
$ ./fork_lock
fun: total++
parent about to fork ...
child require lock...
fun: total++
fun: total++
fun: total++
fun: total++
fun: total++
子进程在请求锁后,再也没有回应,进入死锁。
初始的解决方案是在 fork 前先请求所有的锁,然后再 fork,另外我们希望 fork 完成后,再对所有锁进行解锁。看起来像下面这样(假设程序中只用了三个互斥量):
pthread_mutex_lock(&lock1);
pthread_mutex_lock(&lock2);
pthread_mutex_lock(&lock3);
pid = fork();
pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock3);
if (pid < 0) {
perror("fork");
}
else if (pid > 0) {
...
}
else if (pid == 0) {
...
}
因此,我们将 pid = fork() 那一行改为下面三行:
pthread_mutex_lock(&lock);
pid = fork();
pthread_mutex_unlock(&lock);123
其它的地方不变,重新编译运行,程序正常。
$ gcc fork_lock.c -o fork_lock -lpthread
$ ./fork_lock
fun: total++
parent about to fork ...
child require lock...
child: total++
fun: total++
child require lock...
child: total++
child require lock...
child: total++
fun: total++
child require lock...
child: total++
将 fork 放入临界区,程序正常,实际上,linux 为我们提供一更加方便的机制,让我们不用每次 fork 的时候都自己加锁——atfork 函数。
linux 提供了一个称之为 pthread_atfork 的函数,它允许我们事先注册三个回调函数。一旦 fork 被调用,这三个回调函数会被执行。
函数原型
int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
上面三个回调函数,分别为 prepare 函数,parent 函数,child 函数。这些函数的调用时机如下:
上面三个回调函数,分别为 prepare 函数,parent 函数,child 函数。这些函数的调用时机如下:
用伪代码来说明调用时机:
prepare();
pid = fork();
if (pid > 0) {
parent();
}
else if (pid == 0) {
child();
}
只不过 atfork 注册的这几个函数调用时间是在进入 fork 后,返回 fork 前而已。但是它们做的事情,几乎等同于上面的伪代码。
**实验:**利用 atfork 解决 fork 死锁
程序 atfork_lock 是解决上一篇文章中的 fork 死锁问题,你会发现,它的解决方案其实和上一文中的几乎差不多。只是,更推荐用 atfork 这种方式。
#include
#include
#include
#include
#include
#define PERR(err, msg) do { errno = err; perror(msg); exit(-1); } while(0)
int total = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void prepare() {
int err;
puts("preparing locks...");
err = pthread_mutex_lock(&lock);
if (err != 0) PERR(err, "prepare lock failed");
}
void parent() {
int err;
puts("parent unlocking locks...");
err = pthread_mutex_unlock(&lock);
if (err != 0) PERR(err, "parent unlock failed");
}
void child() {
int err;
puts("child unlocking locks...");
err = pthread_mutex_unlock(&lock);
if (err != 0) PERR(err, "child unlock failed");
}
void* fun(void* arg) {
while (1) {
pthread_mutex_lock(&lock);
total++;
puts("fun: total++");
sleep(5);
pthread_mutex_unlock(&lock);
sleep(1);
}
return NULL;
}
int main() {
int err;
pid_t pid;
pthread_t tid;
pthread_create(&tid, NULL, fun, NULL);
err = pthread_atfork(prepare, parent, child);
if (err != 0) PERR(err, "atfork");
sleep(1);
puts("parent about to fork ...");
pid = fork();
if (pid < 0) PERR(errno, "fork");
else if (pid == 0) {
// child
int status;
while (1) {
pthread_mutex_lock(&lock);
total++;
puts("child: total++");
sleep(2);
pthread_mutex_unlock(&lock);
sleep(1);
}
exit(0);
}
pthread_join(tid, NULL);
}
$ gcc atfork_lock.c -o atfork_lock -lpthread
$ ./atfork_lock
fun: total++
parent about to fork ...
preparing locks...
parent unlocking locks...
child unlocking locks...
child: total++
fun: total++
child: total++
child: total++
fun: total++
child: total++
child: total++
fun: total++