Linux系统编程(七)--线程控制

2023-11-09

文章目录

声明:本章内容作为了解

1 线程属性

1.1 pthread_attr_t

pthread_create 的其中一个参数类型,指定线程属性,包含一下属性:

  • Detach state: 线程的分离属性
  • Scope: 线程竞争域
  • Inherit scheduler: 调度继承性
  • Scheduling policy: 调度策略
  • Scheduling priority: 线程优先级
  • Guard size: 警界缓冲区大小,默认是一个 PAGE_SIZE(4096KB)
  • Stack address: 栈起始地址(栈最低内存地址)
  • Stack size: 栈大小

1.2 不同属性的作用

(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_OTHERSCHED_BATCHSCHED_IDLE 这三种。
  • 实时调度策略,主要包括SCHED_FIFOSCHED_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 <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#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

2 互斥量的共享属性

用于线程互斥的互斥量也有相应的属性 pthread_mutexattr_t,这里只讨论三个方面:

  • 共享属性
  • 鲁棒属性
  • 互斥量的递归类型

2.1 属性的初始化与回收

互斥量属性的数据类型是 pthread_mutexattr_t,下面两个函数分别用于互斥量属性的初始化与回收。

int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

2.2 共享属性

除了互斥量有共享属性外,其它的线程互斥同步对象如读写锁、自旋锁、条件变量、屏障都有共享属性。该属性有两种情况:

  • 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.c 程序

程序 init 用来创建一块共享内存,可以给 init 程序传递一个命令行参数,也可以不传。如果传递参数,表示为互斥量指定鲁棒性属性,我们先不传递参数。

// init.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <pthread.h>

#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.c

destroy 程序主要用于回收互斥量和共享内存。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <pthread.h>

#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.c

buyticket 程序主要用于抢票,该程序需要从命令行传递参数,表示抢票人的名字。比如./buyticket skx 表示抢票人是 skx

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <pthread.h>

#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 <name>\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;
}
  • 编译——Makefile 文件
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 对共享内存进行回收。

3 互斥量的鲁棒属性

上面的程序中,当 skx 抢到第 2 张票后,立即按下 CTRL + C 中断进程,会有什么后果?

结果:如果其中一个进程在未释放互斥量的情况下挂掉了,将会导致另一个线程永远无法获得锁,然后就死锁了。

为了能够让进程在异常终止时,释放掉互斥锁,需要指定 ROBUST 属性。所谓的 ROBUST,指是的健壮的意思。

3.1 相关函数

可以通过下面两个函数设置和获取互斥量的鲁棒属性:

int pthread_mutexattr_getrobust(const pthread_mutexattr_t *attr, int *restrict robust);

int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, int robust);

3.2 互斥量状态一致性

在指定 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 的代码。

  • rbstbuyticket.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <pthread.h>

#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 <name>\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;
}
  • Makefile 文件

添加了两行,现在如下:

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
  • 未指定 robust 下的运行结果

请添加图片描述

  • 指定 robust 的运行结果(init 带参数)

请添加图片描述

4 递归型互斥量

互斥量的类型属性通常有四种:

  • PTHREAD_MUTEX_NORMAL
  • PTHREAD_MUTEX_ERRORCHECK
  • PTHREAD_MUTEX_RECURSIVE
  • PTHREAD_MUTEX_DEFAULT

其中第一种和第四种一般都是一样的,宏定义的值相同,是默认情况。第二种提供错误检查。第三种是我们本文需要讨论的。

4.1 相关函数

可以使用下面的函数对互斥量的类型属性进行设置和获取:

int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);

int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

4.2 递归类型的互斥量

一般情况下,我们在同一个线程中对同一个互斥量加两次锁,就会死锁。如果将互斥量类型属性设置为递归类型 PTHREAD_MUTEX_RECURSIVE 就不会出现此问题。

递归互斥量内部维护着一个计数器,当互斥量未上锁时,计数器值为 0。只有计数器为 0 的情况下,线程才能够获得锁。只有获得锁的线程,才能持续对互斥量加锁,每加一次锁,计数器的值加 1,每解一次锁,计数器的值减 1。

实验
程序 recsig 的功能是在信号处理函数内部对余票数量加 1。操作余票的时候,需要加锁。recsig 程序可以从命令行接受一个参数,表示使用递归互斥量,如果不传参数,表示使用普通互斥量。

resig 函数注册了两种信号,第一种是 SIGUSR1,另一种是 SIGINT。当程序启动的时候,会自己给自己发送 SIGUSR1 信号。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <signal.h>
#include <pthread.h>

#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,程序正常执行完成。

5 其它同步对象的属性

5.1 读写锁的属性

读写锁唯一的属性就是进程共享属性,这和互斥量进程共享属性是一样一样的。它有两种情况:

  • PTHREAD_PROCESS_PRIVATE
  • PTHREAD_PROCESS_SHARED

相关函数如下:

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);

5.2 条件变量的属性

  • 进程共享属性
  • 时钟属性

条件变量属性的初始化函数:

int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);

进程共享属性

它有两种情况:

  • PTHREAD_PROCESS_PRIVATE
  • PTHREAD_PROCESS_SHARED

相关函数如下:

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);

5.3 barrier 属性

barrier 唯一的属性就是进程共享属性,同互斥量进程共享属性是一样,它有两种情况:

  • PTHREAD_PROCESS_PRIVATE
  • PTHREAD_PROCESS_SHARED

相关函数如下:

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);

这节了解即可

6 可重入函数(二)

7 errno 变量与多线程

errno 变量一直被用于调用系统函数时出错后,被赋上错误码的整数变量。

一直以来我们认为 errno 变量都被定义为进程上下文中的一个全局整数。在使用 errno 的时候,通常会使用 #include <errno.h> 将其包含进来。

诸位有没有想过,既然它是全局的,在多线程中会不会出问题?为此先做一个实验进行验证。

实验:
在两个不同的线程中改写 errno 的值,另外采用了一个我们自定义的 mydata 变量进行对比。

// errno.c
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <pthread.h>

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 变量是多线程安全的。

8 只被执行一次的函数

本文内容以及后面的几篇文章都是在为 errno 的实现做铺垫。

8.1 问题提出

有时候,在执行多线程程序的时候,可能需要对某些共享资源进行初始化,一般来说,我们有两种方式:

  • 在创建线程之前进行初始化
  • 在线程函数中做初始化

对于第一种方式,肯定是最常用的了,不过有时候我们并不想这么做。因为一开始可能并不需要多线程程序,所以希望我们将多线程所使用的共享资源初始化动作推迟到线程函数中。如此将会导致一个问题,每个线程都执行初始化,岂不是被初始化很多次?

它的代码看起来像下面这样:

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.

8.2 pthread once

在这一套技术方案中,有两个基本要素:

  • 提供一个 pthread_once_t 类型全局对象,将其初始化为 PTHREAD_ONCE_INIT
  • 提供一个初始化函数 thread_init

每个线程函数调用初始化接口。初始化接口函数定义如下:

int phtread_once(pthread_once_t *initflag, void (*initfn)(void));

它的两个参数,就是上面讲到的两个基本要素。

实验:
在 once 程序中,我们只证明,thread_init 函数只会被执行一次。

// once.c
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>

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 初始化函数只执行了一次

9 线程私有变量

系统为每一个线程提供了一个私有“容器”,该容器中保存的是一个个的键值对。

9.1 键类型及相关函数

键的类型是 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);

注意,调用上面的函数,不会引起前面的析构函数的调用。

注意,调用上面的函数,不会引起前面的析构函数的调用。

9.2 保存键值对

线程容器中的值类型必须是 void*。可以通过下面的函数将键值对保存到线程自己的容器中。

int pthread_setspecific(pthread_key_t key, const void *value);

不同线程调用上面的函数时,只会将键值对保存到自己(指线程自己)的容器中。

不同线程调用上面的函数时,只会将键值对保存到自己(指线程自己)的容器中。

所以,我们把这种键值对称为线程私有数据。

9.3 根据键来获取值

可以通过下面的函数根据键来获取对应的值:

void* pthread_getspecific(pthread_key_t key);

注意,获取到的值类型仍然是 void* 类型。

实验:

// specdata.c
#include <stdio.h>
#include <pthread.h>

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)键的顺序是一致的。

10 模拟 errno

讲一下用到的技术吧:

  1. pthread once,参考 《只被执行一次的函数》
  2. 线程私有变量,参考 《线程私有变量》

本质上 errno 并不是一个真正意义上的变量,而是通过宏定义扩展为语句,而这一行语句实际上是在调用函数,该函数返回保存了指向 errno 变量的指针。

实验:

// myerrno.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>

// 实际上 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

11 再议 pthread_cancel

pthread_cancel 是用来终止指定的线程的,调用 pthread_cancel 后线程什么时候终止呢?

本文主要目的就是验证 pthread_cancel 的退出时机。

11.1 取消点

实验:

下面的几个程序,在线程中编写了计算密集型程序(一个很大的循环),循环执行完毕后,打印该循环消耗的时间。接下来,又打印了一行。

如果 cannot be canceled 打印出来,表明线程并未被取消。

// cancel.c
#include <stdio.h>
#include <pthread.h>
#include <time.h>

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) 进行测试,如果真的收到了“取消信号”,则退出。

11.2 默认取消和异步取消

有一种办法,可以让线程还没到达取消点的时候直接退出。这时候需要将线程设置为异步取消的方式。具体方法为在线程内部调用下面的函数:

int pthread_setcanceltype(int type, int *oldtype);

其中,type 的值如下:

其中,type 的值如下:

  • PTHREAD_CANCEL_ASYNCHRONOUS
  • PTHREAD_CANCEL_DEFERRED

默认情况下,线程取消方式为默认值——PTHREAD_CANCEL_DEFERRED。要想让线程收到“取消信号”后立即退出,需要将 type 设置为 PTHREAD_CANCEL_ASYNCHRONOUS

实验:

// asyncancel.c
#include <stdio.h>
#include <pthread.h>
#include <time.h>

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 函数还未执行,或者说还没遇到取消点,线程就退出了。

11.3 禁止取消

有些线程不是你想取消就取消的。就算你发送了 pthread_cancel 通知了,它也不会退出。除非它自己想退出了。

这时候,可以在线程函数中禁止取消。下面的函数可以实现此功能:

int pthread_setcancelstate(int state, int *oldstate);

其中,state 可以为下面的值:

其中,state 可以为下面的值:

  • PTHREAD_CANCEL_ENABLE
  • PTHREAD_CANCEL_DISABLE

默认情况下,state 的值为 PTHREAD_CANCEL_ENABLE,即可以被取消。如果将 state 设置为 PTHREAD_CANCEL_DISABLE,则线程是无法被取消的。

实验:

// disablecancel.c
#include <stdio.h>
#include <pthread.h>
#include <time.h>

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后,线程是无法被取消的。

12 多线程与信号

12.1 多线程程序中的信号

在多线程中,每一个线程都有属于自己的阻塞信号集与未决信号集。当一个线程派生另一个线程的时候,会继承父线程的阻塞信号集,但是不会继承未决信号集,并且新线程会清空未决信号集。

12.2 相关函数

(1) 设置阻塞信号集的函数
在多线程程序中,如果要设置线程的阻塞信号集,不能再使用 sigprocmask 函数,而应该使用 pthread_sigmask,其定义如下:

int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);

这个函数的用法和 sigprocmask 是一样的。

  • how 参数

    • SIG_BLOCK 该选项表示将 set 参数指示的信号集中的信号添加到进程阻塞集中
    • SIG_UNBLOCK 该选项与功能 SIG_BLOCK 相反,表示将线程阻塞信号集中指定的信号删除
    • SIG_SETMASK 该选项表示将线程阻塞信号集直接设定为你指定的 set
  • 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 做了下面几个工作:

  • 在主线程阻塞了 SIGQUIT 信号
  • 在 fun1 线程中阻塞了 SIGINT 信号
  • 在 fun2 线程中什么也没阻塞
// th_sig.c
#include <unistd.h>
#include <stdio.h>
#include <signal.h>
#include <pthread.h>

// 打印信号集
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。这说明了两件事:

  • 线程继承了父线程阻塞信号集
  • kill 信号会发送给进程中的所有线程

13 多线程与信号(sigwait)

在上一节中,我们使用了 sigpending 从未决队列中取出信号,并打印挨个打印,这种做法相当的麻烦,而且不太科学。如果没有未决信号集中一直没有信号,岂不是在浪费 cpu?

幸好,sigwait 函数可以帮我们解决这个问题。

sigwait 函数

如果在线程中调用 sigwait,它会一直等待它指定的信号,直到未决信号集中出现指定的信号为止,同时 sigwait 还会从未决信号集中取出该信号返回,并将该信号从未决非信号集中删除。

如果多线程中调用 sigwait 等待同一个信号,只会有一个线程可以从 sigwait 中返回。

如果 sigwait 要等待的信号被捕获,要么 sigwait 函数返回,要么调用相应的信号处理函数,到底是哪种情况这取决于操作系统实现。一般情况下,我们需要将 sigwait 要等待的信号添加到阻塞集中,以避免歧义。

sigwait 函数原型如下:

int sigwait(const sigset_t *set, int *sig);

参数 set 表示要等待哪些信号,一旦 sigwait 函数返回,会从未决信号集中取出信号,放到参数 sig 指向的内存中。

实验

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <pthread.h>
#include <errno.h>

#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 + \ 后,整个进程终止了。

14 多线程与 fork

在多线程程序中使用 fork,可能会导致一些意外:

  • 子进程中只剩下一个线程,它是父进程中调用 fork 的线程的副本构成。这意味着在多线程环境中,会导致“线程蒸发”,莫名奇妙的失踪!
  • 因为线程蒸发,它们所持有的锁也可能未释放,这将导致子进程在获取锁时进入死锁。
    本文将验证这两个问题,并给出一个可行的解决方案。

14.1 线程蒸发

如果在多线程环境中执行 fork,派生的子进程是单线程,子进程中的线程是由父进程中调用 fork 的那个线程的副本构成,而子进程中所有其它的线程会消失。

实验:

程序 th_fork 的功能是在父进程中创建一个线程,不断打印自己的 pid 和 ppid。创建完线程后,父进程在主线程中执行 fork,子进程每 2 秒打印一个点。

// th_fork.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>

#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 函数线程已经正常结束了。

7.14.2 fork 死锁

如果在 fork 的时候,线程未释放持有的锁,将导致死锁。

实验:

程序 fork_lock 演示了这种情况。

// fork_lock.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>

#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++

子进程在请求锁后,再也没有回应,进入死锁。

14.3 解决方案

初始的解决方案是在 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 函数。

15 atfork 与 fork

linux 提供了一个称之为 pthread_atfork 的函数,它允许我们事先注册三个回调函数。一旦 fork 被调用,这三个回调函数会被执行。

函数原型

int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));

上面三个回调函数,分别为 prepare 函数,parent 函数,child 函数。这些函数的调用时机如下:

上面三个回调函数,分别为 prepare 函数,parent 函数,child 函数。这些函数的调用时机如下:

  • prepare 函数是在 fork 还会产生子进程时调用
  • parent 和 child 是在产生子进程后调用
  • parent 在父进程中调用,child 在子进程中调用

用伪代码来说明调用时机:

prepare();
pid = fork();

if (pid > 0) {
    parent();
}
else if (pid == 0) {
    child();
}

只不过 atfork 注册的这几个函数调用时间是在进入 fork 后,返回 fork 前而已。但是它们做的事情,几乎等同于上面的伪代码。

**实验:**利用 atfork 解决 fork 死锁

程序 atfork_lock 是解决上一篇文章中的 fork 死锁问题,你会发现,它的解决方案其实和上一文中的几乎差不多。只是,更推荐用 atfork 这种方式。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <errno.h>

#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++
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux系统编程(七)--线程控制 的相关文章

  • 如何使用 GOPATH 的 Samba 服务器位置?

    我正在尝试将 GOPATH 设置为共享网络文件夹 当我进入 export GOPATH smb path to shared folder I get go GOPATH entry is relative must be absolute
  • 如何在 Bash 中给定超时后终止子进程?

    我有一个 bash 脚本 它启动一个子进程 该进程时不时地崩溃 实际上是挂起 而且没有明显的原因 闭源 所以我对此无能为力 因此 我希望能够在给定的时间内启动此进程 如果在给定的时间内没有成功返回 则将其终止 有没有simple and r
  • Gtk-ERROR **:检测到 GTK+ 2.x 符号

    我正在使用 gcc 编译我的 c 应用程序 并使用以下标志 gcc evis c pkg config cflags libs gtk 2 0 libs clutter gtk 1 0 libs gthread 2 0 Wall o evi
  • 是否可以创建一个脚本来保存和恢复权限?

    我正在使用 Linux 系统 需要对一组嵌套文件和目录进行一些权限实验 我想知道是否没有某种方法可以保存文件和目录的权限 而不保存文件本身 换句话说 我想保存权限 编辑一些文件 调整一些权限 然后将权限恢复到目录结构中 将更改的文件保留在适
  • 当 grep "\\" XXFile 我得到“尾随反斜杠”

    现在我想查找是否有包含 字符的行 我试过grep XXFile但它暗示 尾随反斜杠 但当我尝试时grep XXFile没关系 谁能解释一下为什么第一个案例无法运行 谢谢 区别在于 shell 处理反斜杠的方式 当你写的时候 在双引号中 sh
  • 按进程名称过滤并记录 CPU 使用情况

    Linux 下有选项吗顶部命令 https www man7 org linux man pages man1 top 1 html我可以在哪里按名称过滤进程并将每秒该进程的 CPU 使用情况写入日志文件 top pgrep 过滤输出top
  • 需要一些建议来开始在 ARM(使用 Linux)平台上编程

    我 也许 很快就会在托管 Linux 发行版的 ARM 平台上工作 我不知道哪个发行版 我知道该项目涉及视频流 但我无法告诉你更多信息 其实我只收到通知 还没见到任何人 我从来没有在这样的平台上工作过 所以我的想法是在项目开始之前进行测试
  • 在两次之间每分钟执行一次 Cronjob

    我需要在 crontab 中每分钟运行一个 bash 脚本8 45am and 9 50am每天的 Code 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 8 home pull sh gt ho
  • 子目录中的头文件(例如 gtk/gtk.h 与 gtk-2.0/gtk/gtk.h)

    我正在尝试使用 GTK 构建一个 hello world 其中包括以下行 include
  • 如何构建任务“gems:install”

    我正在将 Rails 应用程序部署到 Linux 服务器 并且缺少一些 rake 任务 包括 rake gems install 和 rake db 我正在运行来自 GEM 的 Rails 2 3 4 为什么是这样 我该如何解决 我可以以某
  • Urwid:使光标不可见

    我正在使用 urwid 它是一个用于在 ncurses 中设计终端用户界面的 Python 框架 但有一件事我在 urwid 中无法做到 而这在 Curses 中很容易做到 使光标不可见 现在 选择按钮时光标是可见的 而且看起来很丑 有办法
  • Windows CE 与嵌入式 Linux [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 现在我确信我们都清楚 Linux 与 Windows 桌面的相对优点 然而 我对嵌入式开发世界的了解却少得多 我主要对行业解决方案感兴
  • Capistrano 3 部署无法连接到 GitHub - 权限被拒绝(公钥)

    我使用 Capistrano v3 和 capistrano symfony gem 设置了以下部署脚本 我正在使用 Ubuntu 14 4 部署到 AWS EC2 实例 我正在连接从 AWS 下载的 pem 文件 我的deploy rb中
  • 无法在 Perl 中找到 DBI.pm 模块

    我使用的是 CentOS 并且已经安装了 Perl 5 20 并且默认情况下存在 Perl 5 10 我正在使用 Perl 5 20 版本来执行 Perl 代码 我尝试使用 DBI 模块并收到此错误 root localhost perl
  • Awk - 计算两个文件之间的每个唯一值和匹配值

    我有两个文件 首先 我尝试获取第 4 列中每个唯一字段的计数 然后匹配第二个文件的第二列中的唯一字段值 File1 第 4 列的每个唯一值和 File2 第 2 列包含我需要在两个文件之间匹配的值 所以本质上 我试图 gt 如果 file2
  • 后台分叉无法正常工作[重复]

    这个问题在这里已经有答案了 我运行这个程序 在前景和背景中 int main int pid printf App Start pid d n getpid while 1 pid fork if pid 0 printf Child n
  • PyPI 上的轮子平台约束有什么限制吗?

    是否有任何地方 PEP 或其他地方 声明关于 Linux 轮子上传范围的限制 PyPI http pypi io 应该有 具体来说 上传是否被认为是可接受的做法linux x86 64轮子到 PyPI 而不是manylinux1 x86 6
  • 码头无故停止

    我需要经验丰富的码头用户的建议 我在负载均衡器 亚马逊云 后面维护着 2 台 Linux 机器 使用 Jetty 9 0 3 有时我的 Jetty 容器会被 Thread 2 无故关闭 同时地 显示以下日志并且容器无故停止 没有错误 没有例
  • 找出 Linux 上的默认语言

    有没有办法从C语言中找出Linux系统的默认语言 有 POSIX API 可以实现这个功能吗 例如 我想要一个人类可读格式的字符串 即德语系统上的 German 或 Deutsch 法语系统上的 French 或 Francais 等 有类
  • 从 Python 访问 802.11 无线管理帧

    我想从 Linux 上的 Python 嗅探 802 11 管理 探测请求 帧 这可以从 Scapy 中实现 如下所示 coding utf 8 from scapy all import def proc p if p haslayer

随机推荐