目录

Adavance Programming in the UNIX Environment

书中代码

一、UNIX 基础知识

UNIX 体系结构

  • 严格来讲,操作系统可定义为一种软件,控制计算机硬件资源,提供程序运行环境。通常将这种软件称为内核(kernel)

  • 内核接口被称为系统调用(system call)

登录

  • /ect/password 中查看登录项。

    zj:x:1000:1000:zj,,,:/home/zj:/bin/bash
    依次是登录名:加密口令:数字用户ID:数字组ID:注释字段:起始目录:shell程序

文件和目录

  • 'ls'命令的简单实现

  • 文件描述符(file descriptor)通常是一个小的非负整数,内核用以标识一个特定进程正在访问的文件

  • 每一个新程序运行时,shell都会为其打开 standard input, standard output, standard error

  • 函数 open、read、write、lseek 以及 close 提供了不带缓冲的I/O

  • 标准 I/O 函数为不带缓冲的 I/O 函数提供了一个带缓冲的接口

程序和进程

  • 程序是存储在磁盘上某目录下的可执行文件

  • 程序的执行实例被成为进程(process)

  • UNIX 系统保证每个系统都有一个唯一的进程ID。总是一个非负整数

  • 有三个进程控制函数: fork、exec 和 waitpid

  • 一个进程内的所有线程共享同一地址空间、文件描述符、栈以及与进程相关的属性

  • 每个线程也拥有各自的栈

出错处理

  • Linux 中,出错常量在 errno(3) 手册页中列出

  • errno 需注意,仅当函数返回值指明出错时,才去校验 errno;任何函数都不会将 errno 值设为0

  • char* strerror(int errnum)void perror(const char* msg) 用于输出出错消息

  • 对于非致命性错误,可使用指数补偿法,延长一段时间

用户标识

  • 用户ID为0的是root或superuser

  • 组文件将用户映射到不同的组ID。组文件通常是 /etc/group

信号

  • 通常有三种处理方式

    1. 忽略信号

    2. 按系统默认方式处理。对于除数0,默认方式是终止进程

    3. 提供一个函数,信号发生时调用,这被称为捕捉该信号

时间值

  • 类型 time_t 用于保存日历时间,从1970.1.1.0:0:0开始至今的秒数

  • 类型 clock_t 保存进程时间,度量 CPU 资源

  • 时钟时间:进程运行的时间总量

  • 用户 CPU 时间:执行用户指令所用的时间量

  • 系统 CPU 时间:执行内核程序所经历的时间

系统调用和库函数

  • 各种版本的UNIX系统都提供良好定义、数量有限、直接进入内核的入口点,这些入口点称为系统调用

  • 从应用角度,可将系统调用视为C函数

  • UNIX 系统调用中处理空间分配的是sbrk。它按指定字节数增加或减少进程地址空间。如何管理该地址空间取决于进程。

  • 有很多软件包h使用sbrk系统调用来实现自己的存储空间分配算法

  • 系统调用通常提供最小接口,而库函数通常提供比较复杂的功能

二、UNIX 标准及实现

十一、线程

  • 每个线程都包含有表示执行环境所必需的信息,其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据

  • POSIX 线程的功能测试宏是_POSIX_THREADS,位于<unistd.h>

  • 线程 ID 只有在它所属的进程上下文中才有意义

  • 互斥量、读写锁、条件变量、自旋锁、屏障

线程创建

  • 新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽量,但是该线程的挂起信号集会被清除

  • pthread_t pthread_self()获得本线程的id

线程终止

  • 三种返回方式

    1. return返回

    2. 被同进程中的其它线程pthread_cancel。返回值的指针只想的内存单元被设置为PTHREAD_CANCELED

    3. 线程调用pthread_exit

  • pthread_cleanup_pushpthread_cleanup_pop 用于清理线程,用的栈

    1. 调用 pthread_exit

    2. 响应取消请求时

    3. 用非零参数调用 pthread_cleanup_pop

  • pthread_detach 分离线程,分离后将无法使用pthread_join

线程同步

  • 增量操作通常分解为以下3步

    1. 从内存单元读入寄存器

    2. 在寄存器中对变量做增量操作

    3. 新的值写回内存单元

互斥量

  • pthread_mutex_lockpthread_mutex_trylockpthread_mutex_unlock

避免死锁

  • 仔细控制互斥量加锁的顺序

  • 可以先释放占有的锁,过一段时间再试,使用pthread_mutex_trylock

  • 多线程的软件设计设计需在代码复杂性和性能之间找到正确的平衡。如果锁的粒度太粗,就会出现很多线程阻塞等待相同的锁;如果锁的粒度太细,那么过多的锁开销会使系统性能受到影响,而且代码变得复杂

pthread_mutex_timedlock

  • 该函数允许设置线程阻塞时间,超时时返回错误码ETIMEDOUT

读写锁

  • 允许更高的并行性

  • 一般实现:当有一个线程试图获取写锁时,读写锁通常会阻塞随后的读锁请求,以避免写锁饿死

  • 非常适合于对数据结构读的次数远大于写的情况

  • 使用之前必须初始化,释放底层内存之前必须销毁

  • pthread_rwlock_tpthread_rwlock_initpthread_rwlock_destory

  • pthread_rwlock_rdlockpthread_rwlock_wrlockpthread_rwlock_unlock

  • 需检查pthread_rwlock_unlock的返回值

带超时的读写锁

  • pthread_rwlock_timedrdlockpthread_rwlock_timedwrlock

条件变量

  • pthread_cond_t

  • 作用:阻塞,直到某信号的发生

  • 条件本身是由互斥量保护的:调用前手动获取锁,阻塞时会自动释放锁,醒来后自动获取锁

  • 示例代码:注意,线程醒来,发现队列为空(被其它线程处理了),就继续等待;如果代码不能容忍这种竞争,就要在发信号的时候占有互斥量,即pthread_cond_signal( &qready )写到pthread_mutex_unlock( &qlock )之前,这样只会有一个线程获取到锁,醒来

#include <pthread.h>
struct msg
{
    struct msg* m_next;
};

struct msg *workq;

pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;

void process_msg(void)
{
    struct msg* mp;

    for(;;)
    {
        pthread_mutex_lock( &qplco );
        while( workq == NULL )
        pthread_cond_wait( &qready, &qlock );
        mp = workq;
        workq = mp->m_next;
        pthread_mutex_unlock( *qlock );
    }
};

void enqueue_msg( struct msg *mp )
{
    pthread_mutex_lock( &qlock );
    mp->next = workq;
    workq = mp;
    pthread_mutex_unlock( &qlock );
    pthread_cond_signal( &qready );
}

自旋锁

  • 忙等待的一种锁;pthread_spinlock_t

  • 适用于:锁被持有的时间短,而且线程不希望在重新调度上花费太多的成本

  • 自旋锁通常作为底层原语用于实现其他类型的锁,根据它们所基于的系统体系结构,可以通过使用测试并设置指令有效的实现。当然这里说的有效也还是会造成CPU的浪费: 当线程自旋等待锁变为可用时,CPU不能做其他的事情。这也是自旋锁只能够被持有一小段时间的原因

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

  • 用户层,自旋锁并不是非常有用,除非运行在不允许抢占的实时调度类中

  • 不要在持有自旋锁的情况下调用可能会陷入休眠状态的函数,其它线程获取锁时浪费CPU资源

屏障(barrier)

  • 屏障允许每个线程等待,直到所有合作线程都到达某一点

  • pthread_join就是一种屏障,允许一个线程等待,直到另一个线程退出

  • pthread_barrier_tpthread_barrier_init

  • 使用pthread_barrier_wait来表明线程以完成工作,等待其它线程赶上来

  • 计数满足条件时,所有线程都被唤醒;不满足则此线程睡眠

  • 第一个调用pthread_barrier_wait的线程获得的返回值是PTHREAD_BARRIER_SERIAL_THREAD;其他线程看到的返回值是0

十二、线程控制

线程属性

  • pthread_attr_t

  • 管理这些属性的函数遵循相同的模式

    1. 每个对象与它自己类型的属性对象相关联

    2. 有一个初始化函数,把属性设置为默认值

    3. 有一个销毁属性的函数

    4. 每个属性都有一个从属性对象中获取属性值的函数

    5. 每个属性都有一个设置属性值的函数

  • detachstate : 线程的分离状态属性,PTHREAD_CREATE_DETACHEDPTHREAD_CREATE_JOINABLE

  • guardsize : 线程栈末尾的警戒缓冲区大小(字节数)

  • stackaddr : 线程栈的最低地址,最低内存地址,并不一定是栈的开始位置,处理器架构可能是从高地址向低地址增长

  • stacksize : 线程栈的最小长度(字节数);进程的虚地址空间大小固定,且所有线程共享,所以线程太多时可能需要减少默认的线程栈大小;也有可能需要很大的栈来完成很深的递归或很多自动变量

同步属性

  • pthread_mutexattr_t

进程共享属性

  • 默认不在进程间共享

健壮属性

  • 默认是持有互斥量的进程在终止时不需要采取特别的动作

  • 可设置为PTHREAD_MUTEX_ROBUST,进程终止后,其它进程的线程调用pthread_mutex_lock时会返回EOWNERDEAD;这时候需要调用pthread_mutex_consistent来恢复该锁的一致性,再解锁;否则该互斥量将不再可用

类型属性

  • PTHREAD_MUTEX_NORMAL : 无错误检查和死锁检测

  • PTHREAD_MUTEX_ERRORCHECK : 错误检查

  • PTHREAD_MUTEX_RECURSIVE : 允许同一线程内进行多次加锁,内部维护了每个线程的加锁计数

  • PTHREAD_MUTEX_DEFAULT : 取决于操作系统将其映射为何种类型

  • 各类型的错误处理

互斥量类型未解锁时重新加锁不占用时解锁已解锁时解锁
NORMAL死锁未定义未定义
ERRORCHECK返回错误返回错误返回错误
RECURSIVE允许返回错误返回错误
DEFAULT未定义未定义未定义
  • 递归锁可能很难处理,应该只在没有其它可行方案时才使用

如果在最早定义数据结构时,预留了足够的可填充字段,允许把某些填充字段替换成互斥量,这种方法也是可行的。不过遗憾的是,大多数程序员并不善于预测未来,所以这并不是普遍可行的实践。

读写锁属性

  • 只有一个进程共享属性 : 与互斥量的线程共享属性相同

  • 不同平台的实现也可以定义其它属性,但不据具移植性

条件变量属性

  • 进程共享属性

  • 时钟属性,控制超时函数pthread_cond_timewait的时钟类型

  • 奇怪的是,Singel UNIX Speciafication 并没有为其它有超时等待函数的属性对象定义时钟属性

屏障属性

  • 只有进程共享属性

重入

  • 很多函数并不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区中

  • 线程安全并不代表异步信号安全

  • 标准I/O是以线程安全的方式实现的,内部必须看起来像是调用了flockfilefunlockfile

  • 但把锁开放给应用也是非常有用的,允许程序对标准I/O函数的调用组合成原子序列

  • 也提供了不加锁版本的标准I/O以提升性能,但必须保证此操作是被锁保护的

  • pthread_once函数用于保证函数只会被调用一次

线程特定数据 thread-specific data

  • 除了使用寄存器以外,一个线程没有办法阻止另一个线程访问它的数据,线程特定数据也不例外

  • 但管理线程特定数据的函数可以提高线程间的数据独立性,使线程不太容易访问其他线程的线程特定数据

  • int pthread_key_create(pthread_key_t *keyp, void(*destructor)(void*))

  • void* pthread_getspecific(pthread_key_t key)int pthread_setspecific(pthread_key_t key, const void *value)

取消选项

  • int pthread_setcancelstate(int state, int *oldstate)

  • 把当前可曲线状态设置为state,并把原理的状态存在oldstate里,这两步是原子操作

  • pthread_cancel 调用并不等待线程终止,默认是等到线程到达某个取消点

  • POSIX 在很多函数里定义了取消点,也可自行通过pthread_testcancel添加取消点

线程和信号

  • 把线程引入编程范型,使信号的处理变得更加复杂

  • 每个线程都有自己的信号屏蔽字,但是信号处理是进程中所有线程共享的。这意味着单个线程可以阻止某些信号,但当某个线程修改了与某个给定信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改编

  • 进程中的信号是递送到单个线程的。如果一个信号与硬件故障相关,那么该信号一般会被发送到引起该时间的线程中去,而其他的信号则被发送到任意一个线程

  • 线程可以通过调用int sigwait(const sigset_t *restrict set, int *restrict signop)来等待一个或多个信号的出现

  • 线程在调用sigwait之前,必须阻塞那些它正在等待的信号,sigwait会原子地去去取消信号集的阻塞状态

  • 使用sigwait 的好处在于,允许把异步产生的信号用同步的方式处理

  • 为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中,然后安排专用线程处理信号。这些专用线程可以进行函数调用,不用担心在信号处理程序中调用哪些函数是安全你的,因为这些函数调用来自正常的线程上下文,而非会中断线程正常执行的传统信号处理程序

  • 多个线程在sigwait等待同一个信号时,只有一个线程会返回

  • 入股一个信号被捕获(例如进程通过sigaction建立了一个信号处理程序),而且一个线程正在sigwait此信号,那么将由操作系统来决定以何种方式递送信号。操作系统实现可以让sigwait返回,也可以激活信号处理程序,但这两种情况不会同时发生

  • 闹钟定时器是进程资源,且所有线程共享相同的闹钟。所以进程中的多个线程不可能互不干扰(或互不合作)的使用闹钟定时器