UP | HOME

《UNIX 高级环境编程(第三版)》笔记

Table of Contents

第一章 UNIX 基础知识

1.2 UNIX 体系结构

操作系统是一种软件,它控制计算机硬件资源,提供程序运行环境。我们称之为“内核(kernel)”。

内核的接口称之为系统调用(system call)。公用函数库构建在系统调用接口之上,应用程序既可以调用系统调用,也可以调用公用函数库。

理解:系统调用是最底层的功能,公用函数库是对系统调用的封装,借以简化特定功能。

shell 是一个特殊的 应用程序 ,为运行其他应用程序提供了一个接口。

理解:shell 提供了交互的功能,如果没有 shell,也没图形界面的话,其他应用程序可能无法手动调用。

1.4 文件和目录

UNIX 文件系统的大多数实现并不在目录项中存放属性,这是因为当一个文件具有多个硬链接时,很难保持多个属性副本之间的同步。(理解这段话?)

1.6 程序和进程

fork

The fork() function shall create a new process. The new process (child process) shall be an exact copy of the calling process (parent process) except as detailed below:

fork()函数将创建一个新进程。新进程(子进程)应该是调用进程(父进程)的精确副本,但以下说明除外:

  • The child process shall have a unique process ID.

    子进程应具有唯一的进程 ID。

  • The child process ID also shall not match any active process group ID.

    子进程 ID 也不能与任何活动进程组 ID 相匹配。

  • The child process shall have a different parent process ID, which shall be the process ID of the calling process.

    子进程应该有一个不同的父进程 ID,它应该是调用进程的进程 ID。

  • The child process shall have its own copy of the parent's file descriptors. Each of the child's file descriptors shall refer to the same open file description with the corresponding file descriptor of the parent.

    子进程应拥有自己父文件描述符的副本。每个子进程的文件描述符都应指向与父对应文件描述符相同的打开文件描述。

  • The child process shall have its own copy of the parent's open directory streams. Each open directory stream in the child process may share directory stream positioning with the corresponding directory stream of the parent.

    子进程应拥有自己父开放目录流的副本。子进程中的每个打开的目录流可以与父对应的目录流共享目录流定位。

  • The child process shall have its own copy of the parent's message catalog descriptors.

    子进程应拥有自己父进程的消息目录描述符的副本。

  • The child process' values of tms_utime, tms_stime, tms_cutime, and tms_cstime shall be set to 0.

    子进程的 tms_utime,tms_stime,tms_cutime 和 tms_cstime 的值应设置为 0。

  • The time left until an alarm clock signal shall be reset to zero, and the alarm, if any, shall be canceled; see alarm().

    alarm clock 信号应剩余时间重置为零,并且如果有 alarm 的话,应取消。

  • All semadj values shall be cleared.

    所有 semadj 值都应清除。

  • File locks set by the parent process shall not be inherited by the child process.

    父进程设置的文件锁不应由子进程继承。

  • The set of signals pending for the child process shall be initialized to the empty set.

    子进程挂起的信号集初始化为空集。

  • Interval timers shall be reset in the child process.

    间隔计时器应在子进程中重置。

  • Any semaphores that are open in the parent process shall also be open in the child process.

    任何在父进程中打开的信号量也应该在子进程中打开。

  • The child process shall not inherit any address space memory locks established by the parent process via calls to mlockall() or mlock().

    子进程不应继承由父进程通过调用 mlockall()或 mlock()建立的任何地址空间内存锁。

  • Memory mappings created in the parent shall be retained in the child process. MAP_PRIVATE mappings inherited from the parent shall also be MAP_PRIVATE mappings in the child, and any modifications to the data in these mappings made by the parent prior to calling fork() shall be visible to the child. Any modifications to the data in MAP_PRIVATE mappings made by the parent after fork() returns shall be visible only to the parent. Modifications to the data in MAP_PRIVATE mappings made by the child shall be visible only to the child.

在父进程中创建的内存映射应保留在子进程中。从父级继承的 MAP_PRIVATE 映射也应该是子进程中的 MAP_PRIVATE 映射,父进程中在调用 fork()之前对这些映射中的数据所做的任何修改都应该对子级可见。在 fork()返回后由父级所做的对 MAP_PRIVATE 映射中的数据的任何修改应仅对父级可见。子进程对 MAP_PRIVATE 映射中做的数据修改只能由子进程看到。

  • For the SCHED_FIFO and SCHED_RR scheduling policies, the child process shall inherit the policy and priority settings of the parent process during a fork() function. For other scheduling policies, the policy and priority settings on fork() are implementation-defined.

    对于 SCHED_FIFO 和 SCHED_RR 调度策略,子进程应在 fork()函数期间继承父进程的策略和优先级设置。对于其他调度策略,fork()上的策略和优先级设置是实现定义的。

  • Per-process timers created by the parent shall not be inherited by the child process.

    父进程创建的进程定时器不应该被子进程继承。

  • The child process shall have its own copy of the message queue descriptors of the parent. Each of the message descriptors of the child shall refer to the same open message queue description as the corresponding message descriptor of the parent.

    子进程应拥有自己的父进程的消息队列描述符的副本。每个子进程的消息描述符应该指向与父对应消息描述符相同的开放消息队列描述。

  • No asynchronous input or asynchronous output operations shall be inherited by the child process.

    子进程不应继承异步输入或异步输出操作。

  • A process shall be created with a single thread. If a multi-threaded process calls fork(), the new process shall contain a replica of the calling thread and its entire address space, possibly including the states of mutexes and other resources. Consequently, to avoid errors, the child process may only execute async-signal-safe operations until such time as one of the exec functions is called. Fork handlers may be established by means of the pthread_atfork() function in order to maintain application invariants across fork() calls. 父进程应该是单线程。如果多线程进程调用 fork(),则新进程应包含调用线程的副本及其整个地址空间,可能包括互斥锁和其他资源的状态。因此,为避免错误,子进程可能只会执行异步信号安全操作,直到调用其中一个 exec 函数为止。可以通过 pthread_atfork()函数建立 fork 处理程序,以便跨 fork()调用维护应用程序不变量。

    When the application calls fork() from a signal handler and any of the fork handlers registered by pthread_atfork() calls a function that is not asynch-signal-safe, the behavior is undefined.

    当应用程序从信号处理程序调用 fork()并且由 pthread_atfork()注册的任何 fork 处理程序调用不是异步信号安全的函数时,行为是未定义的。

  • If the Trace option and the Trace Inherit option are both supported:

    如果跟踪选项和跟踪继承选项都支持:

    • If the calling process was being traced in a trace stream that had its inheritance policy set to POSIX_TRACE_INHERITED, the child process shall be traced into that trace stream, and the child process shall inherit the parent's mapping of trace event names to trace event type identifiers. If the trace stream in which the calling process was being traced had its inheritance policy set to POSIX_TRACE_CLOSE_FOR_CHILD, the child process shall not be traced into that trace stream. The inheritance policy is set by a call to the posix_trace_attr_setinherited() function.

      如果调用进程在其继承策略设置为 POSIX_TRACE_INHERITED 的跟踪流中进行跟踪,则应将子进程跟踪到该跟踪流中,并且子进程应继承父进程的跟踪事件名称映射以跟踪事件类型标识符。如果跟踪调用进程的跟踪流将其继承策略设置为 POSIX_TRACE_CLOSE_FOR_CHILD,则不应将子进程跟踪到该跟踪流中。继承策略通过调用 posix_trace_attr_setinherited()函数来设置。

  • If the Trace option is supported, but the Trace Inherit option is not supported:

    如果支持 Trace 选项,但不支持 Trace Inherit 选项:

    • The child process shall not be traced into any of the trace streams of its parent process.

      子进程不应追溯到其父进程的任何跟踪流中。

  • If the Trace option is supported, the child process of a trace controller process shall not control the trace streams controlled by its parent process.

    如果支持跟踪选项,则跟踪控制器进程的子进程不应控制由其父进程控制的跟踪流。

  • The initial value of the CPU-time clock of the child process shall be set to zero.

    子进程的 CPU 时间时钟的初始值应设置为零。

  • The initial value of the CPU-time clock of the single thread of the child process shall be set to zero.

    子进程的单线程的 CPU 时钟的初始值应设置为零。

All other process characteristics defined by IEEE Std 1003.1-2001 shall be the same in the parent and child processes. The inheritance of process characteristics not defined by IEEE Std 1003.1-2001 is unspecified by IEEE Std 1003.1-2001.

所有其他由 IEEE Std 1003.1-2001 定义的过程特性在父进程和子进程中应该是相同的。IEEE Std 1003.1-2001 未定义的过程特性的继承是不可靠的

After fork(), both the parent and the child processes shall be capable of executing independently before either one terminates.

在 fork()之后,父进程和子进程在任何一个终止之前都应该能够独立执行。

一个和 fork 相关的面试题

execve

All process attributes are preserved during an execve(), except the following:

执行 execve() 期间会保留所有进程属性,但以下内容除外:

  • The dispositions of any signals that are being caught are reset to the default (signal(7)).

    捕捉到的任何信号都重置为默认值。

  • Any alternate signal stack is not preserved (sigaltstack(2)).

    不保留任何备用的信号堆栈。

  • Memory mappings are not preserved (mmap(2)).

    不保留内存映射。

  • Attached System V shared memory segments are detached (shmat(2)).

    附加的 System V 内存会分离。

  • POSIX shared memory regions are unmapped (shm_open(3)).

    POSIX 共享内存区域会 unmapped。

  • Open POSIX message queue descriptors are closed (mq_overview(7)).

    打开的 POSIX 消息队列会关闭。

  • Any open POSIX named semaphores are closed (sem_overview(7)).

    任何打开的 POSIX 命名信号都是关闭的。

  • POSIX timers are not preserved (timer_create(2)).

    不保留 POSIX 计时器。

  • Any open directory streams are closed (opendir(3)).

    任何打开的目录流都会关闭。

  • Memory locks are not preserved (mlock(2), mlockall(2)).

    不保留内存锁。

  • Exit handlers are not preserved (atexit(3), on_exit(3)).

    不保留退出处理程序。

  • The floating-point environment is reset to the default (see fenv(3)).

    浮点环境重置为默认值。

The process attributes in the preceding list are all specified in POSIX.1-2001. The following Linux-specific process attributes are also not preserved during an execve():

前面列表中的进程属性都在 POSIX.1-2001 中指定。在 execve()期间,也不会保留以下特定于 Linux 的进程属性:

  • The prctl(2) PR_SET_DUMPABLE flag is set, unless a set-user-ID or set-group ID program is being executed, in which case it is cleared.

    除非正在执行 set-user-ID 或 set-group ID 程序,否则将设置 prctl(2)PR_SET_DUMPABLE 标志,在这种情况下它将被清除。

  • The prctl(2) PR_SET_KEEPCAPS flag is cleared.

    prctl(2)PR_SET_KEEPCAPS 标志被清除。

  • (Since Linux 2.4.36 / 2.6.23) If a set-user-ID or set-group-ID program is being executed, then the parent death signal set by prctl(2) PR_SET_PDEATHSIG flag is cleared.

    (自 Linux 2.4.36 / 2.6.23 起)如果正在执行 set-user-ID 或 set-group-ID 程序,则清除由 prctl(2)PR_SET_PDEATHSIG 标志设置的父死亡信号。

  • The process name, as set by prctl(2) PR_SET_NAME (and displayed by ps -o comm), is reset to the name of the new executable file.

    由 prctl(2)PR_SET_NAME(并由 ps -o comm 显示)设置的进程名称将重置为新可执行文件的名称。

  • The SECBIT_KEEP_CAPS securebits flag is cleared. See capabilities(7).

    SECBIT_KEEP_CAPS securebits 标志被清除。

  • The termination signal is reset to SIGCHLD (see clone(2)).

    终止信号被重置为 SIGCHLD(参见 clone(2))。

Note the following further points: 请注意以下几点:

  • All threads other than the calling thread are destroyed during an execve(). Mutexes, condition variables, and other pthreads objects are not preserved.

    在 execve()期间,除调用线程之外的所有线程都将被销毁。互斥体,条件变量和其他 pthreads 对象不会被保留。

  • The equivalent of setlocale(LC_ALL, "C") is executed at program start-up.

    setlocale(LC_ALL,“C”)的等价物在程序启动时执行。

  • POSIX.1-2001 specifies that the dispositions of any signals that are ignored or set to the default are left unchanged. POSIX.1-2001 specifies one exception: if SIGCHLD is being ignored, then an implementation may leave the disposition unchanged or reset it to the default; Linux does the former.

    POSIX.1-2001 规定,任何被忽略或设置为默认值的信号的配置都保持不变。POSIX.1-2001 规定了一个例外:如果 SIGCHLD 被忽略,那么实现可能会保持处置不变或将其重置为默认值; Linux 做前者。

  • Any outstanding asynchronous I/O operations are canceled (aio_read(3), aio_write(3)).

    任何未完成的异步 I / O 操作都会被取消(aio_read(3),aio_write(3))。

  • For the handling of capabilities during execve(), see capabilities(7).

    有关在 execve()期间处理功能的信息,请参阅功能(7)。

  • By default, file descriptors remain open across an execve(). File descriptors that are marked close-on-exec are closed; see the description of FD_CLOEXEC in fcntl(2). (If a file descriptor is closed, this will cause the release of all record locks obtained on the underlying file by this process. See fcntl(2) for details.) POSIX.1-2001 says that if file descriptors 0, 1, and 2 would otherwise be closed after a successful execve(), and the process would gain privilege because the set-user_ID or set-group_ID permission bit was set on the executed file, then the system may open an unspecified file for each of these file descriptors. As a general principle, no portable program, whether privileged or not, can assume that these three file descriptors will remain closed across an execve().

    默认情况下,文件描述符在 execve()中保持打开状态。标记为 close-on-exec 的文件描述符将被关闭; 请参阅 fcntl(2)中的 FD_CLOEXEC 说明。 (如果文件描述符被关闭,这将导致此进程释放在底层文件上获得的所有记录锁。有关详细信息,请参阅 fcntl(2)。)POSIX.1-2001 表示如果文件描述符为 0,1 和 2,在成功的 execve()之后将关闭 ,并且因为在执行的文件上设置了 set-user_ID 或 set-group_ID 权限位,该进程将获得权限,然后系统可以为这些文件描述符中打开未指定的文件。作为一般原则,不管是否有权限的可移植程序都可以假设这三个文件描述符将在 execve()中保持关闭状态。

第七章 进程环境

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

第八章 进程控制

  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 阻塞。

第九章 进程关系

第十章 信号

引言

信号概念

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

函数 signal

<script src="https://gist.github.com/phenix3443/4b2bad69799ba85096688570f0873f2d.js"></script>

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

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

不可靠的信号

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

中断的系统调用

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

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

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

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

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

如何控制系统调用重启?

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

可重入函数

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

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

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

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

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

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

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

SIGCLD 语义

可靠信号术语和语义

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

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

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

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

函数 kill 和 raise

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

函数 alerm 和 pause

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

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

信号集

函数 sigprocmask

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

函数 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;
}

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

函数 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;
}

函数 sigsetjump 和 siglongjmp

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

函数 sigsuspend

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

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

函数 abort

函数 system

函数 sleep、nanosleep、和 clock_nanosleep

函数 sigqueue

作业控制信号

信号名和编号

第十一章 线程

11.2 线程概念

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

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

11.3 线程标识

  • int pthread_equal(pthread_t t1, pthread_t t2);
  • pthread_t pthread_self(void);

11.4 线程创建

  • int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
    • The new thread inherits a copy of the creating thread's signal mask (pthread_sigmask(3)). The set of pending signals for the new thread is empty (sigpending(2)). The new thread does not inherit the creating thread's alternate signal stack (sigaltstack(2)). The new thread inherits the calling thread's floating-point environment (fenv(3)).

      新线程继承创建线程的信号掩码(pthread_sigmask(3))的副本。新线程的挂起信号集为空(sigpending(2))。新线程不继承创建线程的备用信号堆栈(sigaltstack(2))。新线程继承了调用线程的浮点环境(fenv(3))。

    • Unless real-time scheduling policies are being employed, after a call to pthread_create(), it is indeterminate which thread—the caller or the new thread—will next execute.

      除非正在使用实时调度策略,否则在调用 pthread_create()之后,接下来要执行的调用者或新线程的线程是不确定的。

    • A thread may either be joinable or detached. If a thread is joinable, then another thread can call pthread_join(3) to wait for the thread to terminate and fetch its exit status. Only when a terminated joinable thread has been joined are the last of its resources released back to the system. When a detached thread terminates, its resources are automatically released back to the system: it is not possible to join with the thread in order to obtain its exit status. Making a thread detached is useful for some types of daemon threads whose exit status the application does not need to care about. By default, a new thread is created in a joinable state, unless attr was set to create the thread in a detached state (using pthread_attr_setdetachstate(3)).

      线程可以是 joined 也可以是 detached。如果一个线程是 joined,那么另一个线程可以调用 pthread_join(3)来等待线程终止并获取其退出状态。仅当已终止的 joined 进程 被 join 时,其最后一个资源才会释放回系统。当 detached 线程终止时,其资源会自动释放回系统:无法与线程 join 以获取其退出状态。detached 线程对某些类型的守护程序线程非常有用,这些守护程序线程的退出状态是应用程序不需要关心的。默认情况下,创建新线程是 join 状态,除非将 attr 设置为以分离状态创建线程(使用 pthread_attr_setdetachstate(3))。

参见 thread_create.c

11.5 线程终止

  • void pthread_exit(void *rval_ptr);
    • If the thread has any thread-specific data, then, after the clean-up handlers have been executed, the correasponding destructor functions are called, in an unspecified order.

      如果线程具有任何特定于线程的数据,则在执行清理处理程序之后,将以未指定的顺序调用相应的析构函数。

    • After the last thread in a process terminates, the process terminates as by calling exit(3) with an exit status of zero; thus, process-shared resources are released and functions registered using atexit(3) are called.

      在进程中的最后一个线程终止后,进程终止,如通过调用退出状态为零的 exit(3); 因此,释放进程共享资源并调用使用 atexit(3)注册的函数。

    • To allow other threads to continue execution, the main thread should terminate by calling pthread_exit() rather than exit(3).

      要允许其他线程继续执行,主线程应该通过调用 pthread_exit()而不是 exit(3)来终止。

      参见 main_thread_exit.c

    • The value pointed to by retval should not be located on the calling thread's stack, since the contents of that stack are undefined after the thread terminates.

      retval 指向的值不应该位于调用线程的堆栈上,因为在线程终止后该堆栈的内容是未定义的。

  • int pthread_join(pthread_t thread, void *retval);

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

  • int pthread_cancel(pthread_t tid);

    参见 main_thread_canceled.c

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

    线程也可以安排退出时需要调用的函数,类似进程的 atexit 函数。当线程执行以下动作时,清理函数 rtn 是由 pthread_cleanup_push 函数调度的,调用时只有一个参数 arg:

    • 调用 pthread_exit
    • 响应取消请求时
    • 用非零的 excute 参数调用 pthread_cleanup_pop
    • 或者是 return 返回。(ubuntu 验证)

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

    #define pthread_cleanup_push(routine,arg) \
    
    {
    
    struct _pthread_cleanup_buffer _buffer; \
    
    _pthread_cleanup_push (&_buffer, (routine), (arg));
    
    #define pthread_cleanup_pop(execute) \
    
    _pthread_cleanup_pop (&_buffer, (execute)); \
    
        }
    
  • int pthread_detach(pthread_t tid);
    • The detached attribute merely determines the behavior of the system when the thread terminates; it does not prevent the thread from being terminated if the process terminates using exit(3) (or equivalently, if the main thread returns).

      detached 属性仅确定线程终止时系统的行为; 如果进程使用 exit(3)终止(或等效地,如果主线程返回),它不会阻止线程终止。

    • Either pthread_join(3) or pthread_detach() should be called for each thread that an application creates, so that system resources for the thread can be released. (But note that the resources of any threads for which one of these actions has not been done will be freed when the process terminates.)

      应该为应用程序创建的每个线程调用 pthread_join(3)或 pthread_detach(),以便可以释放线程的系统资源。 (但请注意,当进程终止时,将释放尚未执行其中一个操作的任何线程的资源。)

11.6 线程同步

当变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与写这两个周期交叉时,这种不一致就会出现。(什么是存储器周期?)

以下代码模拟线程的竞争情形:参见 thread_race.c

11.6.1 互斥量(mutex)

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

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

使用动态分配的互斥量,在释放内存前需要调用 pthread_mutex_destory。

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destory(pthread_mutex_t *mutex);

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

int pthread_mutex_timelock(pthread_mutex_t *mutex, const struct timespec *tsptr);  // tsptr 是绝对时间

用互斥量解决前面的多线程竞争:参见 mutex.c

11.6.2 避免死锁

对已有互斥量再次加锁可能会导致死锁。

程序中使用多个互斥量,可以通过仔细控制互斥量加锁的顺序来避免死锁的发生。

可以用 pthread_mutex_trylock 接口避免死锁:如果该接口返回成功可以操作,否则,可以释放已经该占有的资源,做好清理工作,过一会再尝试。

11.6.4 读写锁

有三种状态:加读锁,加写锁,不加锁。与互斥量相比,读写锁具有更高的并行性。非常适合于对数据结构读的次数大于写的情况。

虽然个操作系统对读写锁实现不同,但是当读写锁处于读模式锁住的状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后读模式锁的请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

int pthread_rwlock_init(pthread_rwlock_t *rwlock, pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock, const struce timespec *tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock, const struce timespec *tsptr);

读写锁示例参见:rwlock.c

11.6.6 条件变量(condition)

条件变量是线程可用的另一种 同步机制 (不是互斥机制)。条件变量与互斥量一起使用时,允许线程 以无竞争的方式等待特定的条件发生

条件本身是由互斥量保护的 。线程在 改变条件状态 之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉这种改变,因为必须在锁定互斥量以后才能计算条件。

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);

int phtread_cond_wait(pthread_cond_t cond, pthread_mutex_t *mutex);
int phtread_cond_timedwait(pthread_cond_t cond, pthread_mutex_t *mutex, const struct timespec *tsptr);

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

示例代码参见 cond.c

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

等待信号端代码:

pthread_mutex_lock(&mutex); //必须先锁住条件,然后才能判断
while(condition is not match) { // 进行条件不满足判断,注意不要使用 if,从 wait 返回之后还需要进行条件判断
    pthread_cond_wait(&cond, &mutex);
 }
/* 在这里做一些修改条件的操作 */

pthread_mutex_unlock(&mutex);

产生信号端代码:

pthread_mutex_lock(&p_lock);
/* 这里是修改条件的代码,是条件成立 */
pthread_mutex_unlock(&p_lock);

pthread_cond_signal(&cond);

11.6.7 自旋锁(spin lock)

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

自旋锁通过作为底层原语实现其他的类型的锁。

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

但在用户层,自旋锁并不是非常有用,除非运行在不允许抢占的实时调度类中。运行在分时调度类中的用户层线程在两种情况下可以被取消调度:当它们的时间片到期时,或者具有更高调度优先级的线程就绪变成可运行时。在这些情况下,如果线程拥有自旋锁,它就会进入休眠状态,阻塞在锁上的其他线程自旋时间可能比预期的时间更长。

很多互斥量的实现非常高效,以至于应用程序采用互斥锁的性能与曾经采用过的自旋锁的性能基本是相同的。事实上,有些互斥量的实现在试图获取互斥量的时候会自旋一小段时间,只有在自旋计数到达某一阈值的时候才会休眠。这些因素,加上现代处理的进步,使得上下文切换变得原来越快,也似地自旋锁只有在特定的情况下有用。

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

注意:

  1. 对已经加锁的自旋锁加锁结果是未定义的,调用 pthread_spin_lock 会返回 EDEADLK 错误(或其他错误),或者调用可能会永久自旋。具体行为依赖于实际的实现。
  2. 试图对没有加锁的自旋锁解锁结果也是未定义的。
  3. 不要调用在持有自旋锁的情况下可能会进入休眠状态的函数。如果调用了这些函数,会浪费 CPU 资源,因为其他线程需要自旋锁需要等待的时间就延长了。

11.6.8 屏障(barrier)

屏障是用户协调多个进程并行工作的同步机制。屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。

简单来说:

  • cond 情景:条件达到了,大家一起干,干完大家就没事了(pthread_exit)。
  • barrier 情景:先大家干到统一进度,然后在各自干各自的。
int pthread_barrier_init(pthread_barrier_t *barrier , const pthread_barrierattr_t *attr, unsigned int count);
int pthread_barrier_destory(pthread_barrier_t *barrier);
int pthread_barrier_wait(pthread_barrier_t *barrier);

示例代码参见barrier.c

第十二章 线程控制

12.2 线程属性

属性值不能直接设置,须使用相关函数进行操作,初始化的函数为 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() 通知系统其所需的并发级别。

12.4 同步属性

12.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);
类型属性1

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

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

12.4.2 读写锁属性

只支持进程共享属性。

12.4.3 条件变量属性

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

12.4.4 屏障属性

只支持进程共享属性。

12.5 重入

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

如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步信号安全的。

flockfile 以线程安全的方式来管理 FILE 对象。

使用递归互斥量在异步信号中断的情况下阻止其他线程改变保护的数据结构。

12.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;
}

12.7 取消选项

12.8 线程和信号

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

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

12.9 线程和 fork

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

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

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

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

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

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

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

12.10 线程和 I/O

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

第十三章 守护进程

第十四章 高级 I/O

引言

非阻塞 I/O

记录锁

I/O 多路转接

异步 I/O

函数 readv 和 writev

函数 readn 和 writen

存储映射 I/O

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

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

第十五章 进程间通信

管道

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

#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 将一个描述符复制到另一个上,作为一种保护性的编程措施,都要现将两个描述符进行比较。

函数 popen 和 pclose

协同进程

FIFO

XSI IPC

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

XSI IPC 有下列问题:

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

消息队列

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

信号量

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

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

共享存储

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

POSIX 信号量

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

Footnotes:

Author: liushangliang

Email: phenix3443+github@gmail.com

Created: 2020-04-26 日 10:53