APUE 笔记

Table of Contents

makefile 文件

SOURCE := $(wildcard *.cc)
TARGET := $(SOURCE:%.cc=%.out)
CFLAGS := -g
CXXFLAGS :=
INCLUDE_DIR :=
LIB_DIR :=
LIBS := -lpthread
CC := g++

all:$(TARGET)

$(TARGET):%.out:%.cc
    $(CC) $(CFLAGS)  $(INCLUDE_DIR) $(LIB_DIR) $< -o $@ $(LIBS)

clean:
    -rm $(TARGET)

1 chapter 7 进程环境

  1. main 函数的执行流程(程序员的自我修养)(todo)
  2. _exit 直接进入内核,exit 先执行标准 I/O 库的清理操作:对所有打开流调用 fclose 函数。
  3. 使用 atexit 来登记终止处理程序,每登记一次就会调用一次,如若程序调用 exec 函数族中的任意函数,则将清除所有已安装的终止处理程序。
  4. 如果 exec 没有出错,则 exec 之后的代码都不会执行。
  5. malloc、TCMalloc 原理以及应用。

2 chapter 8 进程控制

  1. 子进程获得父进程数据空间、堆、栈的副本。使用写时复制技术,这些由父子进程共享,内核将他们的访问权限改变为只读。如果父子进程试图修改这些区域,则内核只为修改区域的那块内存制作一个副本。
  2. 父子进程共享的属性。(L185)不继承:进程 ID,父进程 PID,tms_utime、tms_stime、tms_cutime、tms_ustime、文件锁、未处理的闹钟、未处理信号集。
  3. vfork 保证子进程先运行,在它调用 exec 或 exit 之后父进程才可能被调度运行。vfork 生成的子进程在调用 exec 或 exit 之前在父进程空间中运行,但如果子进程修改数据、进行函数调用、或者没有调用 exec 或 exit 就返回可能带来未知的结果。
  4. fork 两次来避免僵死进程和 waitpid 阻塞。

3 chapter 10 信号

3.1 引言

3.2 信号概念

L250:SIGKILL 和 SIGSTOP 是不能被忽略和捕捉的。

3.3 函数 signal

#include <cstdio>
#include <signal.h>
#include <unistd.h>

static void HandleUsr(int sig) {
    printf("received signal %d\n", sig);
    if(sig == SIGUSR1) {
        printf("received usr1 signal\n");
    }
    else if(sig == SIGUSR2) {
        printf("received usr2 signal\n");
    }
}

int main(int argc, char *argv[])
{
    printf("pid: %d\n", getpid());

    if(signal(SIGUSR1, HandleUsr) == SIG_ERR) {
        printf("can not catch SIGUSR1\n");
        return 1;
    }
    if(signal(SIGUSR2,HandleUsr) == SIG_ERR) {
        printf("can not catch SIGUSR2\n");
        return 1;
    }
    while(1) {
        pause();
    }
    return 0;
}

注意:不要在 eshell 中通过 kill 给上面的程序发信号。

signal 函数的限制:不改变信号的处理方式就不能确定信号的当前处理方式。当一个进程调用 fork 时,其子进程继承父进程的信号处理方式。

3.4 不可靠的信号

不可靠是指信号可能会丢失。

3.5 中断的系统调用

为什么系统调用会被中断?哪些系统调用会被中断?

唤醒阻塞的低速系统调用。低速系统调用是可能会使进程永远阻塞的系统调用。

为什么要设置自动重启功能?

为了帮助应用程序使其不必处理被中断的系统调用。有时候用户并不知道所使用的输入、输出设备是否是低速设备。

哪些系统调用中断以后会自动重启? ioctl、read、readv、write、writev、wait、waitpid。前五个只有对低速设备进行操作时才会被信号中断。

如何控制系统调用重启?

Posix 要求只有中断信号的 SA_RESTART 标识有效时,实现才重启系统调用。

3.6 可重入函数

L262:信号处理程序会临时中断正在执行的正常指令序列,等从信号处理函数返回后,继续执行捕获时中断的正常指令序列。

*但在信号处理程序中,不能判断捕捉信号时进程执行到何处。*,也就是说如果信号处理程序被中断就无法恢复执行了。所以需要一种能够在信号处理函数中安全运行,不被中断的函数。

L262:表 10-4 说明了 在信号处理程序中 保证调用安全的函数,这些函数是可重入的,并被称为是异步信号安全的:除了可重入以外,在信号处理操作期间,它阻塞任何引起不一致的信号的发送。

L263:函数不能重入的原因:

  • 已知它们使用静态数据结构。
  • 它们调用 malloc 或 free。
  • 它们是标准 I/O 函数。标准 I/O 函数的很多实现都以不可重入方式使用全局数据结构。

L263: 每个线程中 errno 只有一个,因此,作为一个通用的规则:当在信号处理程序中调用图 10-4 的函数时,应当在调用前保存 errno,在调用后恢复 errno。

在信号处理程序中调用一个非可重入函数,则其结果是不可预知的。

3.7 SIGCLD 语义

3.8 可靠信号术语和语义

L266: 内核在递送一个原来被阻塞的信号给进程时(而不是在产生该信号时),才决定对他的处理方式。

如果在进程解除对某个信号的阻塞之前,这种信号发生了多次,那么如何呢?

L267:Posix.1 并没有对投递给同一进程的信号进行排队。

L267:每个进程都有一个信号屏蔽字,它规定了当前要阻塞传递给该信号的信号集。

3.9 函数 kill 和 raise

L268:如果调用 kill 为调用进程产生信号,而且此信号是不被阻塞的,那么在 kill 返回之前,signo 或者其他的未决的、非阻塞信号被传递至该进程。

3.10 函数 alerm 和 pause

L268:每个进程只能有一个闹钟时间。注意 alarm 函数的参数和返回值的不同配置。

L273:在信号处理函数中使用 longjump 要预防它可能和其他信号处理程序交互的问题。

3.11 信号集

3.12 函数 sigprocmask

设置当前阻塞不能传递给进程的信号集。

3.13 函数 sigpending

#include <iostream>
#include <signal.h>
#include <unistd.h>

void HandleQuit(int sig) {
    std::cout << "get quit signal" << std::endl;
    if(signal(SIGQUIT, SIG_DFL) == SIG_ERR) {
        std::cout << "reset quit signal to default error" << std::endl;
    }
}

int main(int argc, char *argv[])
{
    if(signal(SIGQUIT, HandleQuit) == SIG_ERR) {
        std::cout << "set quit signal error" << std::endl;
        return 1;
    }

    sigset_t newmask, oldmask;
    sigemptyset(&newmask);
    sigaddset(&newmask, SIGQUIT);
    if(sigprocmask(SIG_BLOCK, &newmask, &oldmask)) {
        std::cout << "set process signal mask error" << std::endl;
        return 1;
    }

    sleep(5);

    sigset_t pendingmask;
    if(sigpending(&pendingmask)) {
        std::cout << "get pending mask error" << std::endl;
        return 1;
    }
    if(sigismember(&pendingmask, SIGQUIT)){
        std::cout << "quit signal has been blocked" << std::endl;
    }

    sigprocmask(SIG_SETMASK, &oldmask, NULL);
    std::cout << "quit signal has unblocked" << std::endl;

    sleep(5);

    return 0;
}

获取进程当前阻塞的信号集。

3.14 函数 sigaction

检查或修改预指定信号相关的处理动作。深入了解 sigaction 结构的 sa_mask 字段。

#include <iostream>
#include <signal.h>
#include <unistd.h>

void HandleUsr(int sig) {
    std::cout << "get usr1 signal" << std::endl;
}

int main(int argc, char *argv[])
{
    std::cout << "pid: " << getpid() << std::endl;
    struct sigaction act;
    act.sa_handler = HandleUsr;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    if(sigaction(SIGUSR1, &act, NULL)) {
        std::cout << "change signal handle error" << std::endl;
    }

    pause();

    return 0;
}

3.15 函数 sigsetjump 和 siglongjmp

当捕捉到一个信号时,进入信号捕捉函数,此时当前信号被自动地加入到进程的信号屏蔽字当中。这阻止了后来产生的这种信号中断该信号处理程序。

3.16 函数 sigsuspend

如果希望对一个信号解阻塞,然后 pause 等待以前被阻塞的信号发生,该如何作合?使用 sigsuspend 函数可以在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。

进程的信号屏蔽字由参数 sigmask 指定。当程序返回时将进程的信号屏蔽字设置为调用 sigsuspend 之前的值。

3.17 函数 abort

3.18 函数 system

3.19 函数 sleep、nanosleep、和 clock_nanosleep

3.20 函数 sigqueue

3.21 作业控制信号

3.22 信号名和编号

4 chapter 11 线程

4.1 线程概念

L307 多个线程需要使用系统提供的复杂机制才能实现内存和文件描述符的共享,而多个线程自动的可以放问相同的存储地址和文件描述符。

L308 每个线程都包含有表示执行环境所必须的信息,其中包括进程中标识线程的线程 ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno 变量以及线程私有数据。一个进程的所有信息对该进程的所有线程都是共享的,包括可执行程序的代码,程序的全局内存和堆内存、栈以及文件描述符。

4.2 线程标识

4.3 线程创建

L309 线程创建时并不能保证哪个线程先运行:是先创建的线程,还是调用线程。新创建的线程可以访问进程的地址空间,并且继承线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。

#include <iostream>
#include <unistd.h>
#include <pthread.h>

void PrintTids(const char *str) {
    pid_t pid = getpid();
    pthread_t tid = pthread_self(); // 为什么不能通过调用者传递 tid 进来
    std::cout << str << " pid: " << pid << " tid:" << tid << std::endl;
}

void *WorkFn(void *arg) {
    PrintTids("new thread:");
    return ((void *)0);
}

int main(int argc, char *argv[])
{
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, WorkFn, NULL);
    if (ret) {
        std::cout << "thread create error" << std::endl;
    }
    PrintTids("Main Thread:");
    sleep(1);
    return 0;
}

4.4 线程终止

获取线程返回值:

#include <iostream>
#include <unistd.h>
#include <pthread.h>

void *ThrFn1(void *arg) {
    return ((void*)1);
}

void *ThrFn2(void *arg) {
    return ((void*)2);
}
int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    void *ret1, *ret2;
    if (pthread_create(&tid1, NULL, ThrFn1, NULL)) {
        std::cout << "thread 1 create errror" << std::endl;
        return 1;
    }
    if (pthread_join(tid1, &ret1)) {
        std::cout << "join 1 error" << std::endl;
        return 1;
    }
    if (pthread_create(&tid2, NULL, ThrFn2, NULL)) {
        std::cout << "thread 2 create error" << std::endl;
        return 2;
    }
    if (pthread_join(tid2, &ret2)) {
        std::cout << "join 2 error" << std::endl;
        return 1;
    }

    std::cout << "ret1 = " << (long)ret1 << std::endl;
    std::cout << "ret2 = " << (long)ret2 << std::endl;
    return 0;
}

L313 注意 pthread_create 与 pthread_exit 传递的参数内存地址在调用者返回值后必须是有效的。

#include <iostream>
#include <pthread.h>

struct Foo
{
    int a,b,c,d;
};

void DisplayFoo(const char *msg, const Foo *fp) {
    std::cout << msg << std::endl;
    std::cout << "foo addr is:" << fp << std::endl;
    std::cout << "foo.a: " << fp->a << std::endl;
    std::cout << "foo.b: " << fp->b << std::endl;
    std::cout << "foo.c: " << fp->c << std::endl;
    std::cout << "foo.d: " << fp->d << std::endl;
}

void* ThreadFunc1(void *arg) {
    Foo tmp = {1,2,3,4};
    DisplayFoo("thread 1 running", &tmp);
    return ((void*)&tmp);
}

void* ThreadFunc2(void *arg) {
    std::cout << "thread 2 running" << std::endl;
    return ((void*)2);
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    if(pthread_create(&tid1, NULL, ThreadFunc1, NULL)) {
        std::cout << "create error" << std::endl;
        return 1;
    }
    Foo *rval;
    pthread_join(tid1, (void**)&rval);
    if(pthread_create(&tid2, NULL, ThreadFunc2, NULL)) {
        std::cout << "create error2" << std::endl;
        return 2;;
    }
    pthread_join(tid2, NULL);

    DisplayFoo("main thread running", rval);

    return 0;
}

思考:内存具有自己独立的内存空间,每个进程有自己独立的栈,这些独立的栈在进程的内存空间上是如何分布的呢?1

#include <iostream>
#include <pthread.h>
#include <unistd.h>

void *ThreadFunc(void *arg) {
    while(1){}
    return (void*)0;
}

int main(int argc, char *argv[])
{
    const int tnum = 3;
    pthread_t tids[tnum];
    std::cout << "pid: " << getpid() << std::endl;
    for(int i=0; i<tnum; ++i) {
        if(pthread_create(&tids[i], NULL, ThreadFunc, NULL)) {
            std::cout << "error: create" << std::endl;
            return -1;
        }
    }
    while(1) {}
    return 0;
}

上面的测试程序(pid=6885)中我们创建了三个线程:

ls /proc/6885/task/
6885  6886  6887  6888

我们来查看一个进程的地址空间。

cat /proc/6885/maps
00400000-00401000 r-xp 00000000 08:01 1049772                            /home/phenix/projects/org-notes/org/reading-note/apue-notes/thread-stack-layout.out
00600000-00601000 r--p 00000000 08:01 1049772                            /home/phenix/projects/org-notes/org/reading-note/apue-notes/thread-stack-layout.out
00601000-00602000 rw-p 00001000 08:01 1049772                            /home/phenix/projects/org-notes/org/reading-note/apue-notes/thread-stack-layout.out
025d5000-02607000 rw-p 00000000 00:00 0                                  [heap]
7fa943e57000-7fa943e58000 ---p 00000000 00:00 0
7fa943e58000-7fa9446a8000 rw-p 00000000 00:00 0                          [stack:6888]
7fa9446a8000-7fa9446a9000 ---p 00000000 00:00 0
7fa9446a9000-7fa944ef9000 rw-p 00000000 00:00 0                          [stack:6887]
7fa944ef9000-7fa944efa000 ---p 00000000 00:00 0
7fa944efa000-7fa94574a000 rw-p 00000000 00:00 0                          [stack:6886]
7fa94574a000-7fa945760000 r-xp 00000000 08:01 1703977                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7fa945760000-7fa94595f000 ---p 00016000 08:01 1703977                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7fa94595f000-7fa945960000 r--p 00015000 08:01 1703977                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7fa945960000-7fa945961000 rw-p 00016000 08:01 1703977                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7fa945961000-7fa945a68000 r-xp 00000000 08:01 1704008                    /lib/x86_64-linux-gnu/libm-2.21.so
7fa945a68000-7fa945c67000 ---p 00107000 08:01 1704008                    /lib/x86_64-linux-gnu/libm-2.21.so
7fa945c67000-7fa945c68000 r--p 00106000 08:01 1704008                    /lib/x86_64-linux-gnu/libm-2.21.so
7fa945c68000-7fa945c69000 rw-p 00107000 08:01 1704008                    /lib/x86_64-linux-gnu/libm-2.21.so
7fa945c69000-7fa945e29000 r-xp 00000000 08:01 1704013                    /lib/x86_64-linux-gnu/libc-2.21.so
7fa945e29000-7fa946029000 ---p 001c0000 08:01 1704013                    /lib/x86_64-linux-gnu/libc-2.21.so
7fa946029000-7fa94602d000 r--p 001c0000 08:01 1704013                    /lib/x86_64-linux-gnu/libc-2.21.so
7fa94602d000-7fa94602f000 rw-p 001c4000 08:01 1704013                    /lib/x86_64-linux-gnu/libc-2.21.so
7fa94602f000-7fa946033000 rw-p 00000000 00:00 0
7fa946033000-7fa9461a6000 r-xp 00000000 08:01 2363772                    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7fa9461a6000-7fa9463a5000 ---p 00173000 08:01 2363772                    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7fa9463a5000-7fa9463af000 r--p 00172000 08:01 2363772                    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7fa9463af000-7fa9463b1000 rw-p 0017c000 08:01 2363772                    /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7fa9463b1000-7fa9463b5000 rw-p 00000000 00:00 0
7fa9463b5000-7fa9463cd000 r-xp 00000000 08:01 1704000                    /lib/x86_64-linux-gnu/libpthread-2.21.so
7fa9463cd000-7fa9465cd000 ---p 00018000 08:01 1704000                    /lib/x86_64-linux-gnu/libpthread-2.21.so
7fa9465cd000-7fa9465ce000 r--p 00018000 08:01 1704000                    /lib/x86_64-linux-gnu/libpthread-2.21.so
7fa9465ce000-7fa9465cf000 rw-p 00019000 08:01 1704000                    /lib/x86_64-linux-gnu/libpthread-2.21.so
7fa9465cf000-7fa9465d3000 rw-p 00000000 00:00 0
7fa9465d3000-7fa9465f7000 r-xp 00000000 08:01 1703999                    /lib/x86_64-linux-gnu/ld-2.21.so
7fa9467c9000-7fa9467cf000 rw-p 00000000 00:00 0
7fa9467f4000-7fa9467f6000 rw-p 00000000 00:00 0
7fa9467f6000-7fa9467f7000 r--p 00023000 08:01 1703999                    /lib/x86_64-linux-gnu/ld-2.21.so
7fa9467f7000-7fa9467f8000 rw-p 00024000 08:01 1703999                    /lib/x86_64-linux-gnu/ld-2.21.so
7fa9467f8000-7fa9467f9000 rw-p 00000000 00:00 0
7ffcb0876000-7ffcb0897000 rw-p 00000000 00:00 0                          [stack]
7ffcb0971000-7ffcb0973000 r--p 00000000 00:00 0                          [vvar]
7ffcb0973000-7ffcb0975000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

从上面的显示中可以看出,进程的内存空间从低到高依次是:进程代码段(标识含有 x)、只读数据段、可读写数据段、堆(大小 204800)、栈(包括动态库的栈空间)。

线程 6888 的栈(7fa944efa000-7fa94574a000)大小为 8M,为什么是 8M 呢,因为线程栈默认是 8M,可以从 ulimit 命令得出:

ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7798
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 7798
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

观点 1:线程的栈空间是在进程的栈区划分的。

栈应当是一个动态地概念, 只有在线程开始执行后, 才为其分配栈资源. 具体的分布是无法确定的. 我觉得这就像调用函数一样, 当调用函数时, 进程通过调整栈基址指针寄存器和栈顶指针寄存器, 为函数分配了一个栈空间。能够确定的是, 这片空间肯定是在进程的栈区内划分出来的一片地址空间.

单线程程序只不过是多线程的一种特殊形式, 每创建一个线程时,为每一线程在进程内的栈空间上化分出一片区域, 作为该线程的栈空间. 并且在线程的描述结构里面应当有保存某些寄存器如 esp,ebp 之类的数据结构的定义.

观点 2:线程的栈空间是在进程的堆上划分的。待研究。

L316 线程也可以安排退出时需要调用的函数,类似进程的 atexit 函数。

void pthread_cleanup_push(void (*rtn)(void*), void *arg)
void pthread_cleanup_pop(int execute)

当线程执行以下动作时,清理函数 trn 是由 pthread_cleanup_push 函数调度的,调用时只有一个参数 arg:

  • 调用 pthread_exit
  • 响应取消请求时
  • 用非零的 excute 参数调用 pthread_cleanup_pop

L318 在 push 和 pop 之间返回的行为是未定以的,在 ubutnu 中,两类函数中间使用 return 也会调用 push 的函数。

#include <iostream>
#include <pthread.h>

void Cleanup(void *arg) {
    std::cout << "cleanup:" << (char *)arg << std::endl;
}

void* ThreadFunc1(void *arg) {
    std::cout << "thread 1 start" << std::endl;

    pthread_cleanup_push(Cleanup, const_cast<char*>("thread 1 first handle"));

    pthread_cleanup_push(Cleanup, const_cast<char*>("thread 1 second handle"));
    std::cout << "thread 1 push complete" << std::endl;
    if (arg)
        return ((void*)1);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(1);
    return  ((void*)1);
}

void* ThreadFunc2(void *arg) {
    std::cout << "thread 2 start" << std::endl;
    pthread_cleanup_push(Cleanup, const_cast<char*>("thread 2 first handle"));
    pthread_cleanup_push(Cleanup, const_cast<char*>("thread 2 second handle"));
    std::cout << "thread 2 push complete" << std::endl;
    if (arg)
        pthread_exit((void*)2);
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    return ((void*)2);
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    int arg = 1;
    if(pthread_create(&tid1, NULL, ThreadFunc1, (void*)&arg)) {
        std::cout << "create thread 1 error" << std::endl;
        return 1;
    }
    pthread_join(tid1, NULL);

    if(pthread_create(&tid2, NULL, ThreadFunc2, (void*)&arg)) {
        std::cout << "create thread 2 error" << std::endl;
        return 2;
    }
    pthread_join(tid2, NULL);

    return 0;
}

由于这两个函数实现为宏,所以必须在线程相同的作用域中以配对的形式使用。

思考:如果主线程先于子线程结束会发生什么情况?2

在一些论坛上看到许多人说子线程也会跟着退出,理由如下:

#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* ThreadFunc(void *arg) {
    while(true) {
        std::cout << "child thread alive" << std::endl;
        sleep(1);
    }
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t tid;
    if(pthread_create(&tid, NULL, ThreadFunc, NULL)) {
        std::cout << "create thread error" << std::endl;
        return 1;
    }
    pthread_detach(tid);

    std::cout << "main exit" << std::endl;
    return 0;
}

执行 可以看到主线程在打印“main exit”之后直接退出了,创建的线程没有执行。 但是,其实这种理解是错误的。原因在于他们混淆了线程退出和进程退出概念。实际的情况是主线程中的 main 函数执行完 return 后弹栈,然后调用 glibc 库函数 exit,exit 进行相关清理工作后调用 _exit 系统调用退出该进程。所以,这种情况实际上是因为进程运行完毕退出导致所有的线程也都跟着退出了,并非是因为主线程的退出导致子线程也退出。 实际上,子线程的状态依赖于它所在的进程,如果进程没有退出的话子线程依然正常运转。如果进程退出了,那么它所有的线程都会退出,所以子线程也就退出了。看下面的这个例子:

#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* ThreadFunc(void *arg) {
    pthread_t main_tid = *static_cast<pthread_t*>(arg);
    pthread_cancel(main_tid);
    while(true) {
        std::cout << "child thread alive" << std::endl;
        sleep(1);
    }
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t tid;
    pthread_t main_tid = pthread_self();

    if(pthread_create(&tid, NULL, ThreadFunc, static_cast<void*>(&main_tid))) {
        std::cout << "thread create error" << std::endl;
        return -1;
    }
    pthread_detach(tid);
    while(1) {
        std::cout << "main thread alive" << std::endl;
    }

    std::cout << "main exit" << std::endl;
    return 0;
}
cd apue-notes && make && ./main-thread-dead.out

把主线程的线程号传给子线程,在子线程中通过 pthread_cancel 终止主线程使其退出。

运行程序,可以发现在打印了一定数量的"main thread alive"之后进入了子线程的循环中。 主线程因为被子线程终止了,所有没有看到“main exit”的打印。

主线程被子线程终止了,但他们所依赖的进程并没有退出,所以子线程依然正常运转。

4.4.1 Linux 线程模型

实际上,posix 线程和一般的进程不同,在概念上没有主线程和子线程之分(虽然在实际实现上还是有一些区分),如果仔细观察 apue 或者 unp 等书会发现基本看不到“主线程”或者“子线程”等词语,在 csapp 中甚至都是用“对等线程”一词来描述线程间的关系。

在 Linux 2.6 以后的 posix 线程都是由用户态的 pthread 库来实现的。在使用 pthread 库以后,在用户视角看来,每一个 tast_struct 就对应一个线程( tast_struct 原本是内核对应一个进程的结构),而一组线程以及他们所共同引用的一组资源就是进程。从 Linux 2.6 开始,内核有了线程组的概念, tast_struct 结构中增加了一个 tgid(thread group id)字段。getpid(获取进程号)通过系统调用返回的也是 tast_struct 中的 tgid,所以 tgid 其实就是进程号。而 tast_struct 中的线程号 pid 字段则由系统调用 syscall(SYS_gettid) 来获取。

当线程收到一个 kill 致命信号时,内核会将处理动作施加到整个线程组上。为了应付“发送给进程的信号”和“发送给线程的信号”, tast_struct 里面维护了两套 signal_pending ,一套是线程组共用的,一套是线程独有的。通过 kill 发送的致命信号被放在线程组共享的 signal_pending 中,可以任意由一个线程来处理。而通过 pthread_kill 发送的信号被放在线程独有的 signal_pending 中,只能由本线程来处理。

关于线程与信号,apue 有这么几句:

每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。这意味着尽管单个线程可以阻止某些信号,但当线程修改了与某个信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改变。这样如果一个线程选择忽略某个信号,而其他的线程可以恢复信号的默认处理行为,或者是为信号设置一个新的处理程序,从而可以撤销上述线程的信号选择。

如果信号的默认处理动作是终止该进程,那么把信号传递给某个线程仍然会杀掉整个进程。

例如一个程序 a.out 创建了一个子线程,假设主线程的线程号为 9601,子线程的线程号为 9602(它们的 tgid 都是 9601),因为默认没有设置信号处理程序,所以如果运行命令 kill 9602 的话,是可以把 9601 和 9602 这个两个线程一起杀死的。如果不知道 Linux 线程背后的故事,可能就会觉得遇到灵异事件了。

另外系统调用 syscall( SYS_gettid) 获取的线程号与 pthread_self 获取的线程号是不同的,pthread_self 获取的线程号仅仅在线程所依赖的进程内部唯一,在 pthread_self 的 man page 中有这样一段话:

Thread IDs are guaranteed to be unique only within a process. A thread ID may be reused after a terminated thread has been joined, or a detached thread has terminated.

所以在内核中唯一标识线程 ID 的线程号只能通过系统调用 syscall(SYS_gettid) 获取。

4.5 线程同步

思考:同步与互斥的区别?

L319:如果修改操作是原子操作,就不会出现竞争,也就不需要互斥。如果数据总是顺序出现的,就不需要额外的同步。在现代计算机系统中,存储访问需要多个总线周期,多处理器的总线周期通常在多个处理器上是交叉的,所以我们并不能保证数据是顺序一致的。

L319 的例子很好的说明了如果没有互斥和同步可能会导致的问题。

互斥解决的问题是竞争,同步解决的为难题是乱序。

那么问题来了,互斥的机制能不能解决同步问题?显然不能,有人可能说图 11-9 中线程 a 先获取锁,增量完之后线程 b 再使用不就可以了,变量仍然会递增两次。但是。。。。线程的执行顺序是不确定的,如果要求线程 a,b 顺序执行,那么互斥量并不能保证,虽然最后递增的结果是正确的。

思考:为什么运算操作要在寄存器中运行?

cpu 指令只能操作寄存器。

4.5.1 互斥量

思考:一个线程对资源加锁了,另一个线程不加锁可以访问么?

当然可以,L320 只有将所有线程都设计成遵守相同数据访问规则的,互斥机制才能正常工作。如果允许某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其他的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。所以 要保证所有线程访问资源的规则是一致的

L320 操作系统不会为我们做数据访问的串行化 。所以才需要同步。

#include <iostream>
#include <pthread.h>
#include <unistd.h>

struct Foo
{
    int count;
    pthread_mutex_t lock;
};

void* ThreadFunc(void *arg) {
    Foo *tmp = (Foo*)arg;
    for(int i=0; i<5; ++i) {
        pthread_mutex_lock(&(tmp->lock));
        ++(tmp->count);
        std::cout << tmp->count << std::endl;
        pthread_mutex_unlock(&(tmp->lock));
    }
    return ((void*)0);
}

int main(int argc, char *argv[])
{
    Foo f;
    f.count = 0;
    if(pthread_mutex_init(&(f.lock), NULL)) {
        std::cout << "mutex init error" << std::endl;
        return 1;
    }
    pthread_t tids[2];
    for(int i=0; i<2; ++i) {
        if(pthread_create(&tids[i], NULL, ThreadFunc, (void*)&f)) {
            std::cout << "thread create error " << i << std::endl;
            return 2;
        }
        pthread_join(tids[i], NULL);
    }

    pthread_mutex_destroy(&(f.lock));
    return 0;
}

思考:互斥量不 unlock 直接 destory 会怎么样?

思考:互斥量解锁时如何唤醒阻塞的其他线程,有什么策略么?可能是线程优先级。

L321:可以通过确保对象在释放内存前不会被找到这种方式来避免上述问题?什么意思

4.5.2 避免死锁

L322:程序中使用多个互斥量,可以通过仔细控制互斥量加锁的顺序来避免死锁的发生。要在锁粒度和性能之间做平衡。

4.5.3 函数 pthread_mutex_timelock

4.5.4 读写锁

与互斥量相比,读写锁具有更高的并行性。非常适合于堆数据结构读的次数大于写的情况。

#include <iostream>
#include <pthread.h>
#include <unistd.h>

struct Foo
{
    int count;
    pthread_rwlock_t rwLock;
};

void* ReadFunc(void *arg) {
    sleep(2);
    Foo *tmp = static_cast<Foo*>(arg);
    pthread_rwlock_rdlock(&tmp->rwLock);
    std::cout << tmp->count << std::endl;
    pthread_rwlock_unlock(&tmp->rwLock);
    return NULL;
}

void* WriteFunc(void *arg) {
    sleep(10);
    Foo *tmp = static_cast<Foo*>(arg);
    pthread_rwlock_wrlock(&tmp->rwLock);
    ++tmp->count;
    std::cout << tmp->count << std::endl;
    pthread_rwlock_unlock(&tmp->rwLock);
    return NULL;
}
int main(int argc, char *argv[])
{
    Foo f;
    f.count = 0;
    pthread_rwlock_init(&(f.rwLock), NULL);
    pthread_t tids[8];
    for(int i=0; i<8; ++i) {
        if(i==5) {
            pthread_create(&tids[i], NULL, WriteFunc, (void*)&f);
            pthread_detach(tids[i]);

        }
        else {
            pthread_create(&tids[i], NULL,ReadFunc, (void*)&f);
            pthread_detach(tids[i]);
        }
        sleep(1);
    }
    while(true) {
        std::cout << "main thread" << std::endl;
        sleep(1);
    }

    return 0;
}

4.5.5 带有超时的读写锁

4.5.6 条件变量

互斥量和读写锁已经解决的多线程共享资源产生的竞争问题,那条件变量的用户何在呢?

L332:条件变量是线程可用的另一种 同步机制 (不是互斥机制)。条件变量给多个线程提供了一个会合的场所。条件变量与互斥量一起使用时,允许线程 以无竞争的方式等待特定的条件发生 (共享资源:条件)。条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉这种改变,因为互斥量必须在锁定以后才能计算条件。

#include <iostream>
#include <pthread.h>

struct Foo {
    long count;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
};

void* ThreadFunc(void *arg) {
    Foo *tmp = static_cast<Foo*>(arg);
    pthread_mutex_lock(&tmp->mutex); //必须先锁住条件,然后才能判断
    while(tmp->count != 0) {//不使用 if 判断,防止虚假唤醒
        pthread_cond_wait(&tmp->cond, &tmp->mutex);
    }
    std::cout << "cond finish" << std::endl;
    pthread_mutex_unlock(&tmp->mutex);

    return NULL;
}


int main(int argc, char *argv[])
{
    Foo f;
    f.count = 10000000;
    pthread_mutex_init(&f.mutex, NULL);
    pthread_cond_init(&f.cond, NULL);

    pthread_t tids[5];
    for(int i=0; i<5; ++i) {
        if(pthread_create(&tids[i], NULL, ThreadFunc, static_cast<void*>(&f))) {
            std::cout << "create thread error" << std::endl;
            return 1;
        }
    }

    while(true) {
        pthread_mutex_lock(&f.mutex);//对数据访问规则的一致性
        f.count--;
        pthread_mutex_unlock(&f.mutex);
        if(f.count == 0) {
            pthread_cond_broadcast(&f.cond);
            break;
        }
    }

    for(int i=0; i<5; ++i) {
        pthread_join(tids[i], NULL);
    }
    pthread_cond_destroy(&f.cond);
    pthread_mutex_destroy(&f.mutex);

    return 0;
}

L333:传递 pthread_cond_wait 的互斥量对条件进行保护。调用者把锁住的互斥量传给函数,函数然后自动把调用线程放到等待的线程列表上,对互斥量解锁。(这样就使得判断和休眠成了原子操作) 这就关闭了条件检查和线程进入休眠状态等待条件改变之间的时间通道,这样线程就不会错过条件的任何变化。 pthread_cond_wait 返回时,互斥量再次被锁住。

虚假唤醒3

上面线程代码使用了 pthread_cond_wait(&cond,&mutex); 在条件变量上休眠等待主线程发送信号过来。但是在 pthread_cond_wait 调用等待的时候,线程是释放锁的。(当他返回时才会再次获得锁)。那么就存在一个问题:假想一下。当主线程发送信号过来后。在子线程 在 pthread_cond_wait 上等待发现信号发过来了,那么子线程将醒来并运行(注意这个时候 pthread_cond_wait 还未返回,那么锁是释放的,因为 pthread_cond_wait 在等待是会释放锁,返回时才会重新获得锁),那么如果这时候另一个线程改变了 i(对 i 进行了增减操作。)那么此时 i 不在是 0。但是切换到子线程时他并不知情,他会仍旧认为条件是满足的。也就是说 我们不应该仅仅依靠 pthread_cond_wait 的返回就认为条件满足。

4.5.7 自旋锁

L335:自旋锁与互斥量类似,但它不是通过休眠是进程阻塞,而是在获取锁之前一致处于忙等待阻塞状态。自旋锁主要用于以下情况:锁被持有的时间短,而且线程并不希望在重新调上花费太多的成本。自旋锁通常作为底层原语实现其他类型的锁。

L335:当自旋锁用在非抢占式内核中时是非常有用的:除了提供互斥机制以外,它们会阻塞中断,这样中断处理程序就不会让系统陷入死锁状态,因为它需要获取已被加锁的自旋锁。

L336: 对已经加锁的自旋锁加锁结果是未定义的,对没有加锁的自旋锁解锁结果也是未定义的。不要调用在持有自旋锁的情况下可能会进入休眠状态的函数。

4.5.8 屏障

L336:屏障是用户协调多个进程并行工作的同步机制。

那么都是为了同步,它和 cond 有什么不同呢? 屏障允许每个线程等待,直到所有的合作线程都到打某一点,然后从该点继续执行。 简单来说:

  • cond 情景:条件达到了,大家一起干,干完大家就没事了(pthread_exit)。
  • barrier 情景:先大家干到统一进度,然后在各自干各自的。想想写作业,有时候要可恶的老师要求所有人作业写完了全班才能下课回家,先写完的只能等着。

5 chapter 12 线程控制

5.1 引言

5.2 线程限制

5.3 线程属性

#include <iostream>
#include <pthread.h>

void *func(void *arg) {
    return NULL;
}
int main(int argc, char *argv[])
{
    pthread_t tid;
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    if(pthread_create(&tid, &attr, func, NULL)) {
        std::cout << "pthread create error" << std::endl;
        return 1;
    }
    size_t stack_size;
    void *stackaddr;

    pthread_attr_getstack(&attr, &stackaddr, &stack_size);
    pthread_attr_destroy(&attr);
    std::cout << stackaddr << " " << stack_size << std::endl;

    return 0;
}

属性值不能直接设置,须使用相关函数进行操作,初始化的函数为 pthread_attr_init ,这个函数必须在 pthread_create 函数之前调用。之后须用 pthread_attr_destroy 函数来释放资源。线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省 1M 的堆栈、与父进程同样级别的优先级。

线程属性标识符:pthread_attr_t 包含在 pthread.h 头文件中。

// 线程属性结构如下:
typedef struct
{
    int                   detachstate;      // 线程的分离状态
    int                   schedpolicy;     // 线程调度策略
    struct sched_param     schedparam;      // 线程的调度参数
    int                   inheritsched;    // 线程的继承性
    int                   scope;           // 线程的作用域
    size_t                guardsize;       // 线程栈末尾的警戒缓冲区大小
    int                   stackaddr_set;   // 线程的栈设置
    void*                 stackaddr;       // 线程栈的位置
    size_t                stacksize;       // 线程栈的大小
}pthread_attr_t;
  • 线程的作用域(scope)

    作用域属性描述特定线程将与哪些线程竞争资源。线程可以在两种竞争域内竞争资源:

    • 进程域(process scope):与同一进程内的其他线程。
    • 系统域(system scope):与系统中的所有线程。一个具有系统域的线程将与整个系统中所有具有系统域的线程按照优先级竞争处理器资源,进行调度。
    • Solaris 系统,实际上,从 Solaris 9 发行版开始,系统就不再区分这两个范围。
  • 线程的绑定状态(binding state)

    关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process):轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。

    • 非绑定状态

      默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。

    • 绑定状态

      绑定状况下,则顾名思义,即某个线程固定的 "绑" 在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为 CPU 时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。

  • 线程的分离状态(detached state)

    线程的分离状态决定一个线程以什么样的方式来终止自己。

    • 非分离状态

      线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当 pthread_join() 函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。

    • 分离状态

      分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。应该根据自己的需要,选择适当的分离状态。

    • 线程分离状态的函数: pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate) 。第二个参数可选为 PTHREAD_CREATE_DETACHED (分离线程)和 PTHREAD_CREATE_JOINABLE (非分离线程)。

      这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在 pthread_create 函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用 pthread_create 的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用 pthread_cond_timewait 函数,让这个线程等待一会儿,留出足够的时间让函数 pthread_create 返回。设置一段等待时间,是在多线程编程里常用的方法。但是注意不要使用诸如 wait() 之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。

  • 线程的优先级(priority)
    • 新线程的优先级为默认为 0。
    • 新线程不继承父线程调度优先级 (PTHREAD_EXPLICIT_SCHED)
    • 仅当调度策略为实时(即 SCHED_RRSCHED_FIFO )时才有效,并可以在运行时通过 pthread_setschedparam() 函数来改变,缺省为 0。
  • 线程的栈地址(stack address)
    • POSIX.1 定义了两个常量 _POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE 检测系统是否支持栈属性。
    • 也可以给 sysconf 函数传递 _SC_THREAD_ATTR_STACKADDR 或 _SC_THREAD_ATTR_STACKSIZE 来进行检测。
    • 当进程栈地址空间不够用时,指定新建线程使用由 malloc 分配的空间作为自己的栈空间。通过 pthread_attr_setstackaddr 和 pthread_attr_getstackaddr 两个函数分别设置和获取线程的栈地址。传给 pthread_attr_setstackaddr 函数的地址是缓冲区的低地址(不一定是栈的开始地址,栈可能从高地址往低地址增长)。
  • 线程的栈大小(stack size)
    • 当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用。
    • 当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
    • 函数 pthread_attr_getstacksize 和 pthread_attr_setstacksize 提供设置。
  • 线程的栈保护区大小(stack guard size)
    • 在线程栈顶留出一段空间,防止栈溢出。
    • 当栈指针进入这段保护区时,系统会发出错误,通常是发送信号给线程。
    • 该属性默认值是 PAGESIZE 大小,该属性被设置时,系统会自动将该属性大小补齐为页大小的整数倍。
    • 当改变栈地址属性时,栈保护区大小通常清零。
  • 线程的调度策略(schedpolicy) POSIX 标准指定了三种调度策略:先入先出策略 (SCHED_FIFO)、循环策略 (SCHED_RR) 和自定义策略 (SCHED_OTHER)。 SCHED_FIFO 是基于队列的调度程序,对于每个优先级都会使用不同的队列。 SCHED_RR 与 FIFO 相似,不同的是前者的每个线程都有一个执行时间配额。 SCHED_FIFO 和 SCHED_RR 是对 POSIX Realtime 的扩展。 SCHED_OTHER 是缺省的调度策略。
    • 新线程默认使用 SCHED_OTHER 调度策略。线程一旦开始运行,直到被抢占或者直到线程阻塞或停止为止。
    • SCHED_FIFO

      如果调用进程具有有效的用户 ID 0,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM) 的先入先出线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,则会继续处理该线程,直到该线程放弃或阻塞为止。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS)) 的线程或其调用进程没有有效用户 ID 0 的线程,请使用 SCHED_FIFO,SCHED_FIFO 基于 TS 调度类。

    • SCHED_RR

      如果调用进程具有有效的用户 ID 0,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM) 的循环线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,并且这些线程没有放弃或阻塞,则在系统确定的时间段内将一直执行这些线程。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS) 的线程,请使用 SCHED_RR (基于 TS 调度类)。此外,这些线程的调用进程没有有效的用户 ID 0。

  • 线程并行级别(concurrency)

    应用程序使用 pthread_setconcurrency() 通知系统其所需的并发级别。

5.4 同步属性

5.4.1 互斥量属性

共享属性

从多个进程彼此间共享的内存数据块中分配的互斥量就可以用于这些进程的同步。

健壮属性

与在多个进程间共享的互斥量有关,当持有互斥量的进程终止时,解决互斥量状态恢复的问题。

在创建 pthread mutex 的时候,指定为 ROBUST 模式。

pthread_mutexattr_t ma;

pthread_mutexattr_init(&ma);
pthread_mutexattr_setpshared(&ma, PTHREAD_PROCESS_SHARED);
pthread_mutexattr_setrobust(&ma, PTHREAD_MUTEX_ROBUST);

pthread_mutex_init(&c->lock, &ma);

需要注意的地方是:如果持有 mutex 的线程退出,另外一个线程在 pthread_mutex_lock 的时候会返回 EOWNERDEAD。这时候你需要调用 pthread_mutex_consistent 函数来清除这种状态,否则后果自负。

写成代码就是这样子:

int r = pthread_mutex_lock(lock);
if (r == EOWNERDEAD)
    pthread_mutex_consistent(lock);
类型属性4

控制互斥量的锁定特性,如错误检查,死锁检测,递归加锁等。

L348: pthread_cond_wait 不要使用可递归的互斥互斥量。递归互斥量可能会导致死锁。

5.4.2 读写锁属性

只支持进程共享属性。

5.4.3 条件变量属性

支持进程共享属性和时钟属性。

5.4.4 屏障属性

只支持进程共享属性。

5.5 重入

L354: 如果一个函数在相同的时间点可以被多个线程安全的调用,就称该函数是线程安全的。

L355:如果一个函数对多个线程来说是可重入的,就说这个函数是线程安全的。如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。

L355: flockfile 以线程安全的方式来管理 FILE 对象。 L358:使用递归互斥量在异步信号中断的情况下阻止其他线程改变保护的数据结构。

5.6 线程特定数据

两个原因:

  • 有时候需要维护基于每线程的数据,而不需要担心与其他线程的同步访问问题。
  • 提供了让基于进程的接口适应多线程环境的机制。

    除了使用寄存器以外,一个线程没有办法阻止另一个线程访问它的数据。线程特定数据也不例外。虽然底层的实现部分并不能阻止这种访问能力,但管理线程特定数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其他线程的线程特定数据。

#include <iostream>
#include <cstdio>
#include <cstring>

int main(int argc, char *argv[])
{
    char str[] = "hello,this is a test string";
    char *p = strtok(str,", ");
    while(p) {
        std::cout << p << std::endl;
        p = strtok(NULL, " ");
    }
    return 0;
}

这里的 strtok 使用全局变量来保存要待分析的字符串,这是不是线程安全的。如果多线程同时使用这个函数会修改待分析的字符串。那么如何一个线程安全的 strtok 呢?这就是线程私有数据的作用了。

线程私有变量的使用场景就是:像全局变量一样使用,但是各个线程各部相同的变量.

#include <iostream>
#include <pthread.h>
#include <unistd.h>
const char *data;

char RetChar(const char *str) {
    if(str) {
        data = str;
    }
    return *data++;
}

void* ThreadFunc(void *arg) {
    char c = RetChar(static_cast<char*>(arg));
    while(c) {
        std::cout << c << std::endl;
        sleep(1);
        c = RetChar(NULL);
    }
    return NULL;
}
int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, ThreadFunc, const_cast<char*>("hello"));
    pthread_create(&tid2, NULL, ThreadFunc, const_cast<char*>("world"));
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

实现上面程序的线程安全版本

#include <iostream>
#include <pthread.h>
#include <unistd.h>

static pthread_key_t key;
static pthread_once_t initflag = PTHREAD_ONCE_INIT;

static void KeyInit(void) {
    pthread_key_create(&key, NULL);
}

char RetChar(const char *str) {
    pthread_once(&initflag, KeyInit);

    if(!pthread_getspecific(key)) {
        pthread_setspecific(key, str);
    }

    char *c = static_cast<char*>(pthread_getspecific(key));
    char rval = *c;
    pthread_setspecific(key, ++c);
    return rval;
}

void* ThreadFunc(void *arg) {
    char c = RetChar(static_cast<char*>(arg));
    while(c) {
        std::cout << c << std::endl;
        sleep(1);
        c = RetChar(NULL);
    }
    return NULL;
}
int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, ThreadFunc, const_cast<char*>("hello"));
    pthread_create(&tid2, NULL, ThreadFunc, const_cast<char*>("world"));
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

5.7 取消选项

5.8 线程和信号

L364: 每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。

L364:进程的信号是传递到单个线程的。

5.9 线程和 fork

L367: 在子进程内部,只存在一个线程,它是 由父进程中调用 fork 的线程的副本构成的 。如果父进程中的线程占有锁,子进程将同样占有这些锁。子进程地址空间在创建时就得到了父进程定义的所有锁的副本。问题是 子进程并不包含占有锁的那些线程的副本 ,所以子进程没有办法知道它占有了哪些锁、需要释放哪些锁。

L367: POSIX.1 声明,在 fork 返回和子进程调用其中一个 exec 函数之间,子进程只能调用异步信号安全的函数。

L367:使用 pthread_atfork 来清除锁的状态。prepare 在 fork 前获得父进程所有的锁变量,这样 fork 的时候父子进程中所有锁的状态就是一致的。parent 在 fork 创建子进程后,返回之前在父进程上下文中定义,对所有锁进行解锁,child 在 fork 返回前在子进程上下文中对锁进行解锁。

L368:那么这样看起来好像是父进程加锁一次,然后父子进程解锁两次。其实不是的,当父进程和子进程对它们的锁的副本解锁的死后哦,新的内存是分配给子进程的(写时复制),所以看起来执行序列应该是父加锁、子加锁、父解锁、子解锁。

L368:可以多次调用 pthread_atfork 来设置多套 fork 处理程序。prepare 调用顺序与注册顺序相反,parent 和 child 按注册顺序调用。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <sys/wait.h>

pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void prepare(void) {
    std::cout << "preparing locking...." << std::endl;
    if(pthread_mutex_lock(&lock1) || pthread_mutex_lock(&lock2)) {
        std::cout << "prepare error" << std::endl;
    }
}

void parent(void) {
    std::cout << "parent unlocking ....." << std::endl;
    if(pthread_mutex_unlock(&lock1) || pthread_mutex_unlock(&lock2)) {
        std::cout << "parent error" << std::endl;
    }
}

void child(void) {
    std::cout << "child unlocking..." << std::endl;
    if(pthread_mutex_unlock(&lock1) || pthread_mutex_unlock(&lock2)) {
        std::cout << "child error" << std::endl;
    }
}

void* ThreadFunc(void *arg) {
    std::cout << "thread running" << std::endl;
    pause();
    return NULL;
}

int main(int argc, char *argv[])
{
    if(pthread_atfork(prepare, parent, child)) {
        std::cout << "pthread_atfork error" << std::endl;
        return 1;
    }
    pthread_t tid;

    if(pthread_create(&tid, NULL, ThreadFunc, NULL)) {
        std::cout << "pthread_create error" << std::endl;
        return 2;
    }
    sleep(2);                   // 让子线程先执行
    std::cout << "parent about to forking..." << std::endl;
    pid_t pid = fork();
    if (pid == 0) {
        std::cout << "child process return" << std::endl;
    }
    else if(pid > 0) {
        waitpid(pid, NULL, 0);
        std::cout << "parent process return" << std::endl;
    }
    else {
        std::cout << "fork error" << std::endl;
        return 1;
    }

    return 0;
}

pthread_atfork 机制的意图是使 fork 之后的锁状态一致,但还是存在有限的情况,比如不能处理复杂的同步机制,不能处理递归锁等。

最后总结:在多线程环境里面尽量不要使用 fork,或者 fork 之后立刻执行 exec 系列函数。

5.10 线程和 I/O

使用 pread、pwrite 等原子操作来读写文件。

6 chapter 13 守护进程

7 chapter 14 高级 I/O

7.1 引言

7.2 非阻塞 I/O

7.3 记录锁

7.4 I/O 多路转接

7.5 异步 I/O

7.6 函数 readv 和 writev

7.7 函数 readn 和 writen

7.8 存储映射 I/O

L423:对指定映射存储区的保护要求不能超过文件 open 模式访问权限。映射存储区位于堆和栈之间:这属于实现袭击额,各种实现之间可能不同。

L425:修改的页并不会立即写回到文件中。何时写回由内核的守护进程决定。

8 chapter 15 进程间通信

8.1 引言

8.2 管道

管道的局限性:半双工和只能在具有公共祖先的两个进程间使用。

#include <iostream>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd[2];
    if (pipe(fd)) {
        std::cout << "create pipe error" << std::endl;
        return 1;
    }
    pid_t pid = fork();
    if (pid < 0) {
        std::cout << "fork error" << std::endl;
        return 1;
    }
    else if (pid > 0) {
        close(fd[0]);
        char msg[] = "hello,pipe!\n";
        write(fd[1], msg, sizeof(msg));
    }
    else {
        close(fd[1]);
        char msg[128] = {0};
        int n = read(fd[0], msg, sizeof(msg));
        write(STDOUT_FILENO, msg, n);
    }
    return 0;
}
L434: 无论何时调用 dup2 和 close 将一个描述符复制到另一个上,作为一种保护性的编程措施,都要现将两个描述符进行比较。

8.3 函数 popen 和 pclose

8.4 协同进程

8.5 FIFO

8.6 XSI IPC

L448:每个内核中的 IPC 结构都使用一个非负整数的标识符加以引用。标识符是 IPC 对象的内部名,与每个 IPC 对象关联的 key 是其外部名。

L450:XSI IPC 有下列问题:

  • IPC 结构是在系统范围内起作用的,没有引用计数,释放不方便。
  • 这些 IPC 结构在文件系统中没有名字,不能使用常规的操作文件描述符的函数修改它们的属性。需要在内核中添加新的系统调用。

8.7 消息队列

L455:最后总结:在新的应用程序中不要使用消息队列。

8.8 信号量

L455:信号量是一个计数器,用于为多个进程提供对共享数据的访问。

如果多个进程间共享一个资源,可以使用映射到两个进程地址空间中的信号量、记录锁或者互斥量。但是比较之下,最优的方法是使用文件锁。记录锁的性质确保当一个锁的持有者进程终止时,内核会自动释放该锁。不使用互斥量原因如下:首先,在多个进程间共享的内存中使用互斥量来恢复一个终止的进程更难。其次,进程共享的互斥量属性还没有得普遍支持。

8.9 共享存储

共享存储快是因为数据不需要在进程间复制。

8.10 POSIX 信号量

8.11 客户进程-服务器进程属性

Footnotes:

Author: lsl

Created: 2016-08-07 Sun 19:30

Validate