Adavance Programming in the UNIX Environment
Last updated
Was this helpful?
Last updated
Was this helpful?
严格来讲,操作系统可定义为一种软件,控制计算机硬件资源,提供程序运行环境。通常将这种软件称为内核(kernel)
内核接口被称为系统调用(system call)
登录
/ect/password 中查看登录项。
zj:x:1000:1000:zj,,,:/home/zj:/bin/bash
依次是登录名:加密口令:数字用户ID:数字组ID:注释字段:起始目录:shell程序
文件和目录
文件描述符(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
信号
通常有三种处理方式
忽略信号
按系统默认方式处理。对于除数0,默认方式是终止进程
提供一个函数,信号发生时调用,这被称为捕捉该信号
时间值
类型 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
线程终止
三种返回方式
用return
返回
被同进程中的其它线程pthread_cancel
。返回值的指针只想的内存单元被设置为PTHREAD_CANCELED
线程调用pthread_exit
pthread_cleanup_push
和 pthread_cleanup_pop
用于清理线程,用的栈
调用 pthread_exit
时
响应取消请求时
用非零参数调用 pthread_cleanup_pop
时
pthread_detach
分离线程,分离后将无法使用pthread_join
线程同步
增量操作通常分解为以下3步
从内存单元读入寄存器
在寄存器中对变量做增量操作
新的值写回内存单元
互斥量
pthread_mutex_lock
、pthread_mutex_trylock
、pthread_mutex_unlock
避免死锁
仔细控制互斥量加锁的顺序
可以先释放占有的锁,过一段时间再试,使用pthread_mutex_trylock
多线程的软件设计设计需在代码复杂性和性能之间找到正确的平衡。如果锁的粒度太粗,就会出现很多线程阻塞等待相同的锁;如果锁的粒度太细,那么过多的锁开销会使系统性能受到影响,而且代码变得复杂
pthread_mutex_timedlock
该函数允许设置线程阻塞时间,超时时返回错误码ETIMEDOUT
读写锁
允许更高的并行性
一般实现:当有一个线程试图获取写锁时,读写锁通常会阻塞随后的读锁请求,以避免写锁饿死
非常适合于对数据结构读的次数远大于写的情况
使用之前必须初始化,释放底层内存之前必须销毁
pthread_rwlock_t
、pthread_rwlock_init
、pthread_rwlock_destory
pthread_rwlock_rdlock
、pthread_rwlock_wrlock
、pthread_rwlock_unlock
需检查pthread_rwlock_unlock
的返回值
带超时的读写锁
pthread_rwlock_timedrdlock
、pthread_rwlock_timedwrlock
条件变量
pthread_cond_t
作用:阻塞,直到某信号的发生
条件本身是由互斥量保护的:调用前手动获取锁,阻塞时会自动释放锁,醒来后自动获取锁
示例代码:注意,线程醒来,发现队列为空(被其它线程处理了),就继续等待;如果代码不能容忍这种竞争,就要在发信号的时候占有互斥量,即pthread_cond_signal( &qready )
写到pthread_mutex_unlock( &qlock )
之前,这样只会有一个线程获取到锁,醒来
```cpp
include
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;
};
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_t
、pthread_barrier_init
使用pthread_barrier_wait
来表明线程以完成工作,等待其它线程赶上来
计数满足条件时,所有线程都被唤醒;不满足则此线程睡眠
第一个调用pthread_barrier_wait
的线程获得的返回值是PTHREAD_BARRIER_SERIAL_THREAD
;其他线程看到的返回值是0
十二、线程控制
线程属性
pthread_attr_t
管理这些属性的函数遵循相同的模式
每个对象与它自己类型的属性对象相关联
有一个初始化函数,把属性设置为默认值
有一个销毁属性的函数
每个属性都有一个从属性对象中获取属性值的函数
每个属性都有一个设置属性值的函数
detachstate : 线程的分离状态属性,PTHREAD_CREATE_DETACHED
和PTHREAD_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是以线程安全的方式实现的,内部必须看起来像是调用了flockfile
和funlockfile
但把锁开放给应用也是非常有用的,允许程序对标准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返回,也可以激活信号处理程序,但这两种情况不会同时发生
闹钟定时器是进程资源,且所有线程共享相同的闹钟。所以进程中的多个线程不可能互不干扰(或互不合作)的使用闹钟定时器