当前位置:Linux教程 - Linux - Solaris2.4 多线程编程指南4--操作系统编程

Solaris2.4 多线程编程指南4--操作系统编程


本文出自:BBS水木清华站 作者:Mccartney (coolcat) (2002-01-29 20:29:36)

4. 操作系统编程

本章讨论多线程编程如何和操作系统交互,操作系统作出什么改变来支持多线
程。
进程--为多线程而做的改动
警告(alarm), 计数器(interval timer), 配置(profiling)
全局跳转--setjmp(3C) 和longjmp(3C)
资源限制
LWP和调度类型
扩展传统信号
I/O 问题

4.1进程--为多线程而做的改变

4.1.1复制父线程
fork(2)
用fork(2)和fork1(2)函数,你可以选择复制所有的父线程到子线程,或者子
线程只有一个父线程???。
Fork()函数在子进程中复制地址空间和所有的线程(和LWP)。这很有用,例如,
如果子进程永远不调用exec(2)但是用父进程地址空间的拷贝。
为了说明,考虑一个父进程中的线程--不是调用fork()的那个--给一个互斥锁
加了锁。这个互斥锁被拷贝到子进程当中,但给互斥锁解锁的线程没有被拷贝。所
以子进程中的任何试图给互斥锁加锁的线程永久等待。为了避免这种情况,用fork()
复制进程中所有的线程。
注意,如果一个线程调用fork(),阻塞在一个可中断的系统调用的线程将返回
EINTR。
Fork1(2)
Fork1(2) 函数在子线程中复制完全的地址空间,但是仅仅复制调用fork1()的
线程。这在子进程在fork()之后立即调用exec()时有用。在这种情况下,子进程不
需要复制调用fork(2)函数的那个线程以外的线程。
在调用fork1()和exec()之间不要调用任何库函数--库函数也许会使用一个由
多个线程操作的锁。

*Fork(2)和fork1(2)的注意事项
对于fork()和fork1(),在调用之后使用全局声明时要小心。
例如,如果一个线程顺序地读一个文件而另外一个线程成功地调用了fork(),
每一个进程都有了一个读文件的线程。因为文件指针被两个线程共享,父进程得到
一些数据,而子进程得到另外一些。
对于fork()和fork1(),不要创建由父进程和子进程共同使用的锁。这仅发生在
给锁分配的内存是共享的情况下(用mmap(2)的MAP_SHARED声明过)。

Vfork(2)
Vfork(2)类似于fork1(),只有调用线程被拷贝到子进程当中去。
注意,子进程中的线程在调用exec(2)之前不要改变内存。要记住vfork()将父
进程的地址空间交给子进程。父进程在子进程调用exec()或退出后重新获得地址空
间。子进程不改变父进程的状态是非常重要的。
例如在vfork()和exec()之间创建一个新线程是危险的。

4.1.2执行文件和终止进程

exec(2)和exit(2)
exec(2)和exit(2)调用和单线程的进程没有什么区别,只是它们破坏所有线程
的地址空间。两个调用在执行资源(以及活动线程)被破坏前阻塞。
如果exec()重建一个进程,它创建一个LWP。进程从这个初始线程开始执行程序。
象平时一样,如果初始线程返回,它调用exit()来破坏整个进程。
如果所有线程退出,进程用0值退出。

4.2 Alarms(闹钟???), Interval Timers(定时器), and Profiling(配置)

每个LWP有一个唯一的实时的定时器和一个绑定在LWP上的线程的闹钟。定时器
和闹钟在到时间时向线程发送信号。
每个LWP有一个虚拟时间或一个配置定时器,绑定在该LWP上的线程可以使用它
们。如果虚拟定时器到时间,它向拥有定时器的LWP发送信号SIGVTALRM或SIGPROF,
发送哪一个视情况而定。
你可以用profil(2)给每一个LWP进行预配置,给每个LWP私有的缓冲区或者一个
LWP共享的缓冲区。配置数据按LWP用户时间的每一个时钟单位更新。在创建LWP时配
置状态被继承。

4.3非本地跳转--setjmp(3C)和longjmp(3C)

setjmp()和longjmp()的使用范围限制在一个线程里,在大多数情况下是合适的。
然而,只有setjmp()和longjmp()在同一个线程里,线程才能对一个信号执行longjmp()。

4.4资源限制

资源限制在整个进程内,每个线程都可以给进程增加资源。如果一个线程超过
了软资源限制,它将发出相应的信号。进程内可用的资源总量可以由getrusage(3B)
获得。

4.5 LWP和调度类型

Solaris 内核有3种进程调度类型。最高优先级的是实时(realtime RT)。其次
是系统(system)。系统调度类型不能在用户进程中使用。最低优先级的是分时
(timeshare TS),它也是缺省类型。
调度类型在LWP内维护。如果一个进程被创建,初始LWP继承父进程的调度类型和
优先级。如果有跟多的LWP被创建来运行非绑定线程,它们也继承这些调度类型和优先
级。进程中的所有非绑定线程有相同的调度类型和优先级。
每个调度类型按照调度类型的配置优先级,把LWP的优先级映射到一个全体的分配
优先级。???
绑定线程拥有和它们绑定的LWP相同的调度类型和优先级。进程中的每个绑定线程
有一个内核可以看到的调度类型和优先级。系统按照LWP来调度绑定线程。
调度类型用priocntl(2)来设置。前两个参数的指定决定了是只有调用的LWP还是
一个或多个进程所有的LWP都被影响。第三个参数是一个指令,它可以是以下值之一。
· PC_GETCID--获得指定类型的类型号和类型属性
· PC_GETCLINFO--获得指定类型的名称和类型属性
· PC_GETPARMS--获得类型标识和进程中,LWP,或者一组进程的因类型而异
的调度参数
· PC_SETPARMS--设置类型标识和进程中,LWP,或者一组进程的因类型而异
的调度参数
用priocntl()仅限于绑定线程。为了设置非绑定线程的优先级,使用thr_setprio(3T)。

4.5.1分时调度

分时调度将执行资源公平地分配给各进程。内核的其他部分可以在短时间内独占
处理器,而不会使用户感到响应时间延长。
Priocntl(2)调用设置一个或多个线程的nice(2)级别。Priocntl()影响进程中所有
的分时类型的LWP的nice级别。普通拥护的nice()级别从0到20,而超级用户的进程从
-20到20。值越小,级别越高。
分时LWP的分配优先级根据它的LWP的CPU使用率和它的nice()级别来确定。Nice()
级别指定了在进程内供分时调度器参考的相对优先级。LWP的nice()值越大,所得的执
行资源越少,但不会为0。一个执行的多的LWP会被赋予比执行的少的LWP更小的优先级。

4.5.2实时调度

实时类型可以被整个进程或进程内部的一个或多个线程来使用。这需要超级用户
权限。与分时类型的nice(2)级别不同,标识为实时的LWP可以被独立或联合地分配优先
级。一个priocntl(2)调用影响进程中所有实时的LWP的属性。
调度器总是分配最高优先级的实时LWP。如果一个高优先级的LWP可运行,它将打断
低优先级的LWP。一个有先行权(preempt)的LWP被放置在该等级队列的头上。一个实
时(RT)的LWP保持控制处理器,直到被其他线程中断时挂起,或者实时优先级改变。
RT类型的LWP对TS类型的进程有绝对的优先权。
一个新的LWP继承父线程或LWP的调度类型。一个RT类型的LWP继承其父亲的时间片,
不管它是有限还是无限的。一个有有限时间片的LWP持续运行直到结束,阻塞(例如等
待一个I/O事件),被更高优先级的实时进程中断,或者时间片用完。一个拥有无限时
间片的进程则不存在第四种情况(即时间片用完)。

4.5.3 LWP调度和线程绑定
· 线程库自动调节缓冲池中LWP的数量来运行非绑定线程。其目标是:
避免程序因为缺少没有阻塞的LWP而阻塞。
例如,如果可运行的非绑定线程比LWP多而所有的活动线程在内核中处于无限等待
的阻塞状态,进程不能继续,知道一个等待的线程返回。
· 有效利用LWP
例如,如果线程库给每个线程创建一个LWP,许多LWP通常处于闲置状态,而操作
系统反而被没用的LWP耗尽资源。
要记住,LWP是按时间片运行的,而不是线程。这意味着如果只有一个LWP,则进程
内部没有时间片--现成运行直到阻塞(通过线程间同步),被中断,或者运行结束。
可以用thr_setprio(3T)来为线程分配优先级:只有在没有高优先级的非绑定线程
可用时,LWP才会被分配给低优先级的线程。当然,绑定线程不会参与这种竞争,因为
它们有自己的LWP。
把线程绑定到LWP上可以精确地控制调度。???但这种控制在很多非绑定线程竞
争一个LWP是不可能的。
实时线程可以对外部事件有更快的反应。考虑一个用于鼠标控制的线程,它必须对
鼠标事件及时作出反应。通过绑定一个线程到LWP上,保证了在需要时会有LWP可用。通
过将LWP设定为实时调度类型,可以保证LWP对LWP事件作出快速响应。

4.5.4信号等待(SIGWAITING)--给等待线程创建LWP
线程库通常保证在缓冲池内有足够的LWP保证程序运行。如果进程中所有的LWP处
于无限等待的阻塞状态(例如在读中断或网络时阻塞),操作系统给进程发送一个新的
信号,SIGWAITING。这个信号由线程库来控制。如果进程中有一个等待运行的线程,一
个新的LWP被创建并被赋予适当的线程使之执行。
SIGWAITING机制在一个或多个线程处于计算绑定并且有新线程可以执行的情况下。
一个计算绑定线程可以阻止在缺少LWP的情况下有多个可运行的线程启动运行。这可以
通过调用thr_setconcurrency(3T)或者在调用thr_create(3T)时使用THR_NEW_LWP标志。

4.5.5确定LWP的已空闲时间

如果活动线程的数量减少,LWP池中的一些LWP将不再被需要。如果LWP的数量比活动
的线程多,线程库破坏那些没有用的LWP。线程库确定LWP的空闲的时间--如果线程在
足够长的时间内没有被使用(现在的设置是5分钟),它们将被删除。

4.6扩展传统的信号

为了适应多线程,UNIX的信号模型以一种相当自然的方式被扩展。信号的分布是用
传统机制建立在进程内部的(signal(2),sigaction(2), 等等)。
如果一个信号控制器被标志为SIG_DFL或者SIG_IGN,在收到信号后所采取的行动
(exit, core dump, stop, continue, or ignore)在整个接收进程中有效,将影响到
进程中的所有线程。关于信号的基本信息请参见signal(5)。
每个线程有它自己的信号掩模。如果线程使用的内存或其他状态也在被信号控制
器使用,线程会关于一些信号阻塞。进程中所有的线程共享由sigaction(2)和其变量
建立的信号控制器,???象通常那样。
进程中的一个线程不能给另一个进程中的线程发送信号。一个由
kill(2)和sigsend(2)送出的信号是在进程内部有效的,将被进程中的任何一个接收态
的线程接收并处理。
非绑定线程不能使用交互的信号栈。一个绑定线程可以使用交互信号栈,因为其
状态是和执行资源连接的。一个交互信号栈必须通过sigaction(2) ,以及
sigaltstack(2)来声明并使能。
一个应用程序给每个进程一个信号控制器,在它的基础上,每个线程都有线程信
号控制器。一种办法是给在一张表中给每个线程控制器建立一个索引,由进程信号控制
器来通过这张表实现线程控制器。这里没有零线程。
信号被分为两类:陷阱(traps)和意外(exceptions,同步信号)和中断
(interrupts,异步信号)。
在传统的UNIX中,如果一个信号处于挂起状态(即等待接收),发生的其他同样的
信号将没有效果--挂起信号由一位来表示,而不是一个计数器。
就象在单线程的进程里那样,如果一个线程在关于系统调用阻塞时收到一个信号,
线程将提前返回,或者带有一个EINTR错误代码,或者带有比请求少的字节数(如果阻
塞在I/O状态)。
对于多线程编程有特殊意义的是作用在cond_wait(3T)上的信号的效果。这个调用
通常在其他线程调用cond_signal(3T)和cond_broadcast(3T),但是,如果等待线程收
到一个UNIX信号,将返回一个EINTR错误代码。更多的信息参见""对于条件变量的等待中断""。

4.6.1同步信号

陷阱(例如SIGILL, SIGFPE, SIGSEGV)发生在线程自身的操作之后,例如除零
错误或者显式地发信号给自身。一个陷阱仅仅被导致它的线程类控制。进程中的几个
线程可以同时产生和控制同类陷阱。
扩展信号到独立线程的主张对于同步信号来说是容易的--信号被导致问题的线程
来处理。然而,如果一个线程没有处理这个问题,例如通过sigaction(2)建立一个信号
控制器,整个进程将终止。
因为一个同步信号通常意味着整个进程的严重错误,而不只是一个线程,终止进程
通常是一个明智的做法。

4.6.2异步信号

中断(例如SIGINT和SIGIO)是与任何线程异步的,它来自于进程外部的一些操作。
它们也许是显式地送到其他线程的信号,或者是例如Control-c的外部操作,处理异步
信号不处理同步信号要复杂的多。
一个中断被任何线程来处理,如果线程的信号掩模允许的话。如果有多个线程可以
接收中断,只有一个被选中。
如果并发的多个同样的信号被送到一个进程,每一个将被不同的线程处理,如果
线程的信号掩模允许的话。如果所有的线程都屏蔽该信号,则这些信号挂起,直到有信
号解除屏蔽来处理它们。

4.6.3连续语义(Continuation Semantics)

连续语义(Continuation Semantics)是处理信号的传统方法。其思想是当一个
信号控制器返回,控制恢复到中断前的状态。这非常适用于单线程进程的异步信号,如
同在示例4-1中的那样。在某些程序设计语言里(例如PL/1),这也被用于意外
(exception)处理机制。

Code Example 4-1 连续语义
Unsigned int nestcocunt;
Unsigned int A(int i, int j) {
Nestcount++;
If(i==0)
Return (j+1);
Else if (j==0)
Return (A(I-1,1));
Else
Return (A(I-1,A(I, j-1)));
}
void sig(int i){
printf(""nestcount=%d "",nestcount);
}
main(){
sigset(SIGINT, sig);
A(4,4);
}

4.6.4对于信号的新操作

对于多线程编程的几个新的信号操作被加入操作系统。
Thr_sigsetmask(3T)
Thr_sigsetmask(3T)针对线程而sigprocmask(2)针对进程--它设置(线程)的
信号掩模。如果一个新线程被创建,它的初始信号掩模从父线程那里继承。
在多线程编程中避免使用sigprocmask(),因为它设置LWP的信号掩模,被这个
操作影响的线程可以在一段时间后改变。???
不象sigprocmask(),thr_sigsetmask()是一种代价相对低廉的调用,因为它不
产生系统调用。
Thr_kill(3T)
Thr_kill是kill(2)的线程版本--它发送信号给特定的线程。
当然,这与发送信号给进程不同。如果一个信号被发送给进程,信号可以被进
程中的任何线程所控制。一个由thr_kill()发出的信号只能被指定的线程处理。
注意,你只能用thr_kill()给当前进程里的线程发信号。这是因为线程标识符
是本地的--不可能给其他进程内的线程命名。
Sigwait(2)
Sigwait(2)导致调用线程阻塞直到收到set参数指定的所有信号。线程在等待时,
被set标识的信号应当被解除屏蔽,但最初的信号掩模在调用返回时将恢复。
用sigwait()来从异步信号当中把线程分开。你可以创建一个线程来监听异步信
号,而其它线程被创建来关于指定的异步信号阻塞。
如果信号被发送,sigwait()清除挂起的信号,返回一个数。许多线程可以同时
调用sigwait(),但每个信号被收到后只有相关的一个线程返回。
通过sigwait()你可以同时处理异步信号--一个线程通过简单的sigwait()调用
来处理信号,在信号一旦被受到就返回。如果保证所有的线程(包括调用sigwait()
的线程)屏蔽这样的信号,你可以保证这样的信号被你指定的线程安全地处理。
通常,用sigwait()创建一个或多个线程来等待信号。因为sigwait()可以接收
被屏蔽的信号,应当保证其它线程对这样的信号不感兴趣,以免信号被偶然地发送给
这样的线程。如果信号到达,一个线程从sigwait()返回,处理该信号,等待其它的
信号。处理信号的线程不限于使用异步安全函数,可以和其它线程以通常的方式同
步(异步安全函数类型被定义为""安全等级的MT界面MT Interface Safety Levels)。
---------------------------------------
注意-sigwait()不能用于同步信号
---------------------------------------
sigtimedwait(2)
sigtimedwait(2)类似于sigwait(2),不过如果在指定时间内没有收到信号,
它出错并返回。

4.6.5面向线程的信号(thread-directed signals)

UNIX信号机制扩展了一个叫做""线程引导信号""的概念。它们就象普通的异步信
号一样,只不过他们被送到指定线程,而不是进程。
在单独的线程内等待信号比安装一个信号控制器安全和容易。
处理异步信号的更好的办法是同时处理它们。通过调用sigwait(2),一个线程
可以等待一个信号发生。
Code Example 4-2 异步信号和sigwait(2)
Main(){
Sigset_t set;
Void runA(void);

Sigemptyset(&set);
Sigaddset(&set, SIGINT);
Thr_sigsetmask(SIG_BLOCK, &set, NULL);
Thr_create(NULL, 0, runA, NULL, THR_DETACHED, NULL);
While(1){
Sigwait(&set);
Printf(""nestcount=%d "",nestcount);
}
}
void runA(){
A(4,4);
Exit(0);
}
这个例子改变了示例4-1:主函数屏蔽了SIGINT信号,创建了一个子线程来调
用前例中的函数A,然后用sigwait来处理SIGINT信号。
注意信号在计算线程中被屏蔽,因为计算线程继承了主线程的信号掩模。除非
用sigwait()阻塞,主线程不会接收SIGINT。
而且,注意在使用sigwait()中,系统调用不会被中断。

4.6.6完成语义(Completion Semantics)

处理信号的另外一种办法是用完成语义。完成语义使用在信号表明有极严重的
错误发生,以至于当前的代码块没有理由继续运行下去。该代码将被停止执行,取
而代之的是信号控制器。换句话说,信号控制器完成代码块。
在示例4-3中,有问题的块是if语句的then部分。调用setjmp(3C)在jbuf中保
存寄存器当前的状态并返回零--这样执行了块。

Code Example 4-3 完成语义
Sigjmp_buf jbuf;
Void mult_divide(void) {
Int a,b,c,d;
Void problem();
Sigset(SIGFPE, problem);
While(1) {
If (sigsetjmp(&jbuf) ==0) {
Printf(""three numbers, please: "");
Scanf(""%d %d %d"", &a,&b,&c);
D=a*b/c;
Printf(""%d*%d/%d=%d "",a,b,c,d);
}
}
}
void problem(int sig){
printf(""couldn''t deal with them,try again "");
siglongjmp(&jbuf,1);
}
如果SIGFPE(一个浮点意外)发生,信号控制器被唤醒。
信号控制器调用siglongjmp(3C),这个函数保存寄存器状态到jbuf,导致程序
从sigsetjmp()再次返回(保存的寄存器包含程序计数器和堆栈指针)。
然而,这一次,sigsetjmp(3C)返回siglongjmp()的第二个参数,是1。注意块
被跳过,在while循环的下一次重复才会执行。
注意,你可以在多线程编程中用sigsetjmp(3C)和siglongjmp(3C),但是要小心,
线程永远不会用另一个线程的sigsetjmp()的结果来做siglongjmp()。而且,
sigsetjmp()和siglongjmp()保存和恢复信号掩模,但sigjmp(3C)和longjmp(3C)
不会这样做。如果你使用信号控制器时最好使用sigsetjmp()和siglongjmp()。
完成语义经常用来处理意外。具体的,Ada语言使用这种模型。
--------------------------------------
注意-sigwait(2)永远不应用来同步信号。
--------------------------------------

4.6.7信号控制器和异步安全

有一个类似与线程安全的概念:异步安全。异步安全操作被保证不会和被中断
的操作相混。
如果信号控制器与正被中断的操作冲突,就会有异步安全的问题。例如,假设
有一个程序正在printf调用的当中,一个信号发生,它的控制器也要调用printf():
两个printf()的输出会交织在一起。为了避免这种结果,如果是printf被中断,控
制器就不应当调用printf。
这个问题使用同步原语无法解决,因为试图的同步操作会立即导致死锁。
例如,假设printf()用互斥锁来保护它自己。现在假设一个线程正在调用
printf(),第一个printf就得在互斥锁上等待,但是线程突然被信号中断了。如果
控制器(被在printf的里面中断的线程调用)也调用printf(),在互斥锁上阻塞的
线程回再次尝试得到printf的使用权,这就导致了死锁。
为了避免控制器和操作之间的干涉,或者保证这种情况永远不会发生(例如在
可能出问题的时刻封掉所有信号),或者在信号控制器中仅仅使用异步安全操作。
因为在用户级操作设置线程的掩模相对代价较小,你可以方便地设计代码使得
它符合异步安全的范畴。

4.6.8关于条件变量的中断等待

如果在线程等待条件变量的时候获得一个信号,过去的做法是(假设进程没有
终止)被中断的调用返回EINTR。
理想的新条件是当cond_wait(3T)和cond_timedwait(3T)返回,将重新获得互斥
锁。
Solaris多线程是这样做的:如果一个线程在cond_wait或cond_timedwait()函
数上阻塞,而且获得一个没有被屏蔽信号,(信号)控制器将被启用,cond_wait()
或cond_timedwait()返回EINTR,并且互斥锁加锁。???
这意味着互斥锁将被信号控制器获得,因为控制器必须清理环境。
请看示例4-4
Code Example 4-4 条件变量和等待中断
Int sig_catcher() {
Sigset_t set;
Void hdlr();

Mutex_lock(&mut);

Sigemptyset(&set);
Sigaddset(&set,SIGING);
Thr_sigsetmask(SIG_UNBLOCK,&set,0);

If(cond_wait(&cond,&mut) == EINTR){
/* signal occurred and lock is held */
cleanup();
mutex_unlock(&mut);
return(0);
}
normal_processing();
mutex_unlock(&mut);
return(1);
}
void hdlr() {
/* lock is held in the handler */
………
}
假设SIGINT信号在sig_catcher()的入口处被阻塞,而且hdlr()已被建立(通
过sigaction()调用)成为SIGINT的控制器。
如果线程阻塞在cond_wait()的时候,一个没有被屏蔽的信号被送给线程,线
程首先获得互斥锁,然后调用hdlr(),然后从cond_wait()返回EINTR。
注意,在sigaction()中指定SA_RESTART标志是没有效果的--cond_wait(3T)
不是系统调用,不会被自动重新启动。如果线程在cond_wait()阻塞时,调用总是
返回EINTR。

4.7 I/O事项

多线程的一个优势是它的I/O性能。传统的UNIX API在这一领域没有给程序员
足够的辅助--你或者使用文件系统的辅助,或者跳过整个文件系统。
这部分将介绍怎样在多线程利用I/O并发和多缓冲区来获得更多的灵活性。这
个部分也探讨了同步I/O(多线程)和异步I/O(可以是也可以不是多线程)的异同。

4.7.1 I/O作为远程过程调用

在传统的UNIX模型里,I/O表现为同步的,就象你在通过一个远程过程调用
(RPC)来操纵外设。一旦调用返回,I/O完成(或至少看上去已完成--例如一个写
请求,也许仅仅是在操作系统内做数据移动)。
这个模型的优势在于容易理解,因为程序员对过程调用是很熟悉的。
一个代替的办法(在传统的UNIX里没有的)是异步模式,I/O请求仅仅启动一
个操作。程序要自己来发现操作是否完成。
这个办法不象同步方法那样简单,但它的优势在于允许并发的I/O处理和传统
的单线程进程处理。

4.7.2驯服的异步(Tamed Asynchrony)

你可以通过在多线程编程里使用同步I/O来获得异步I/O的大多数好处。在异
步I/O中,你发出一个请求,过一会儿再去检查请求是否已经完成,你可以用分离
的线程来同步操作I/O。然后由主线程(也许是thr_join(3T))检查操作是否完成。

4.7.3异步I/O

在大多数情况下没有必要使用异步I/O,因为它的效果可以通过线程来实现,
每个线程使用同步I/O。然而,在少数情况下,线程不能完全实现实现异步I/O的功
能。
最直接的例子是用流的方法写磁带。这种技术在有持续的数据流写向磁带,磁
带驱动器高速运转时防止磁带驱动器停止。
为了作到这点,在磁带驱动程序响应一个标志上一个写操作已经完成的中断时,
内核里的磁带驱动器必须发出一个写请求队列。
线程不能保证异步写被排队,因为线程本身执行的顺序就是不确定的。例如试
图给磁带的写操作排队是不可能的。

*异步I/O操作
#include
int aioread(int filedes, char *bufp, int bufs, off_t offset,
int whence, aio_result_t *resultp);
int aiowrite(int filedes, const char *bufp, int bufs,
off_t offset, int whence, aio_result_t *resultp);
aio_result_t *aiowait(const struct timeval *timeout);
int aiocancel(aio_result_t *resultp);
aioread(3)和aiowrite(3)在形式上与pread(2)和pwrite(2),不同的是最后一
个参数。调用aioread()和aiowrite()导致初始化(或排队)一个I/O操作。
调用不会阻塞,调用的状态将返回到由resultp指向的结构。其类型为
aio_result_t,包含有:
int aio_return;
int aio_errno;
如果一个调用立即失败,错误码被返回到aio_errno。否则,这个域包含
AIO_INPROGRESS,意味着操作被成功排队。
你可以通过调用aiowait(3)来等待一个特定的异步I/O操作结束。它返回一个
指向aio_result_t数据结构的指针,该结构由最初的aioread(3)或者aiowrite(3)
提供。如果这些函数被调用,Aio_result包含类似与read(2)和write(2)相似返回
值,aio_errno包含错误代码,如果有的话。
Aiowait()使用一个timeout参数,该参数指定了调用者可以等多久。通常情况
下,一个NULL指针表示调用者希望等待的时间不确定,如果指针指向的数据结构包
含零值,表明调用者不希望等待。
你可以启动一个异步I/O操作,做一些工作,然后调用aiowait()来等待结束的
请求。或者你可以在操作结束后,用SIGIO来异步地通知。
最后,一个挂起的异步I/O操作可以通过调用aiocancel()来取消。这个过程在
调用时使用存放结果的地址做参数。这个结果区域标识了要取消哪一个操作。

4.7.4共享的I/O和新的I/O系统调用

如果多个线程同时使用同一个文件描述符来进行I/O操作,你会发现传统的
UNIX I/O接口不安全。在非串行的I/O(即并发)发生时会有问题。它使用
lseek(2)系统调用来为后续的read(2)和write(2)函数设置文件偏移量。如果两个或
更多的线程使用lseek(2)来移动同一个文件描述符,就会发生冲突。
为了避免冲突,使用新的pread(2)和pwrite(2)系统调用。
#include
#include
ssize_t pread(int fildes,void *buf,size_t nbyte,off_t offset);
ssize_t pwrite(int filedes,void *buf,size_t nbyte,off_t offset);
这些调用效果类似于read(2)和write(2),不同之处在于多了一个参数,文件
偏移量。用这个参数,你可以用不着用lseek(2)指定偏移量,多线程可以对同一个
文件描述符进行安全的操作。

4.7.5 Getc(3S)和putc(3S)的替代函数

一个问题会发生在标准I/O的情况下。程序员可以很快地习惯于getc(3S)和
putc(3S)这样的函数--它们是用宏来实现的。因为如此,他们可以在程序的循环内
部使用,用不着考虑效率。
然而,如果改用线程安全的版本后,代价会突然变的昂贵--它们需要(至少)
两个内部子程序调用,来给一个互斥锁加锁和解锁。为了解决这个问题,提供了这
些函数的替代版本--getc_unlocked(3S)和putc_unlocked(3S)。
这些函数不给互斥锁加锁,因此速度象非线程安全版本的getc(3S)和putc(3S)
一样快。然而如果按照线程安全的方法来使用的话,必须用flockfile(3S)和
funlockfile(3S)显式地给互斥锁加锁和解锁来保护标准的I/O流。这两个调用放在
循环外面,而getc_unlocked()或者putc_unlocked()放在循环内部。