资源描述:
《Linux课件 进程线程编程》由会员上传分享,免费在线阅读,更多相关内容在行业资料-天天文库。
进程/线程编程1 本章主要内容:◆进程概念、进程调度、进程通信和同步。◆线程定义、多线程同步及示例。◆多线程的编程方法。2 一、进程3 1.1进程概念20世纪60年代,进程(process)一词首先在麻省理工学院的MULTICS和IBM的CTSS/360系统中被引入。对进程下个准确定义不容易,一般的我们认为进程是一个程序的一次执行过程。进程是申请系统资源的基本的单位,它具有的两个重要特性。4 (1)独立性进程是系统中独立存在的实体,它可以拥有自己独立的资源,比如文件和设备描述符等。在没有经过进程本身允许的情况下,其他进程不能访问到这些资源。这一点上和线程有很大的不同。线程是共享资源的程序实体,创建一个线程所花费的系统开销要比创建一个进程小得多。5 (2.)动态性进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。6 由于以上两个性质,又可以衍生出进程的第三个重要特性,即并发性。若干个进程可以在单处理机状态上并发执行。注意并发性(concurrency)和多处理机并行(parallel)是两个不同的概念。7 并行指在同一时刻内,有多条指令在多个处理机上同时执行;而并发指在同一时刻内可能只有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。8 1.2进程的结构作为申请系统资源的基本单位,进程必须有一个对应的物理内存空间。而对这样的一块空间,首先要用数据结构进行描述,才能进一步对之进行管理。9 进程运行的环境称为进程上下文(context)。Linux中进程的上下文由进程控制块PCB(processcontrolblock)、代码段(textsegment)、数据段(datasegment)以及用户堆栈(stack)组成。10 其中:代码段存放该进程的可执行代码;数据段存放进程中静态产生的数据结构;PCB包括进程的编号、状态、优先级以及正文段和数据段中数据分布的大概情况。11 Linux中的PCB块又称为task_struct结构,task_struct数据结构十分巨大、复杂(在2.4版本内核中,每个task_struct占1680个字节),但它可以按功能分为以下几类信息:1)State、2)SchedulingInformation、3)Identifiers、4)Inter-ProcessCommunication、5)Links、6)TimesandTimers、7)Filesystem、8)Virtualmemory、9)ProcessorSpecificContext。12 图5-1进程的数据结构—个称做进程表(processtable)的链表结构将系统中所有的PCB块联系起来,如图5-1所示。13 在Linux中,进程以进程号PID(processID)作为标识。系统每次访问一个进程时,内核根据PID在进程表中查找相应的进程PCB块(具体查找过程通过一个PID的hash表实现),再通过PCB块找到其对应的代码段与数据段,并进行操作。14 1.3进程状态进程是—个动态的实体,故而它是有生命的。从创建到消亡,是一个进程的整个生命周期。在这个周期中,进程可能会经历各种不同的状态。一般来说,所有进程都要经历以下3种状态。15 ◆就绪(ready)态:指进程已经获得所有所需的其他资源,并正在申请处理机资源,准备开始运行。这种情况下,称进程处于就绪态。16 ◆阻塞(blocked)态:指进程因为需要等待所需资源而放弃处理机,或者进程本不拥有处理机,且其他资源也没有满足,从而即使得到处理机资源也不能开始运行。这种情况下,称进程处于阻塞态。阻塞状态又称休眠状态或者等待状态。17 ◆运行态:进程得到了处理机,并不需要等待其他任何资源,正在执行的状态,称之为运行态。只有在运行态时,进程才可以使用所申请到的资源。18 在Linux系统中,将各种状态进行了重新组织,由此得出Linux进程的几个状态(如图5-2所示):19 图5-2Linux进程状态转换20 ◆RUNNING:正在运行,或者在就绪队列中等待运行的进程。也就是上面提到的运行态和就绪态进程的综合。一个进程处于RUNNING状态,并不代表它一定在被执行。21 由于在多任务系统中,各个就绪进程需要并发执行,所以在某个特定时刻,这些处于RUNNING状态的进程之中,只有一个能够得到处理机,而其他进程必须在一个就绪队列中等待。即使是在多处理机的系统中,Linux也只能同时让一个处理机执行任务。22 ◆UNINTERRUPTABLE:不可中断阻塞状态。处于这种状态的进程正在等待队列中,当资源有效时,可由操作系统进行唤醒,否则,将一直处于等待状态。23 ◆INTERRUPTABLE:可中断阻塞状态。与不可中断阻塞状态一样,处于这种状态的进程也在等待队列中,当资源有效时,可以由操作系统进行唤醒。与不可中断阻塞状态有所不同的是,处于此状态中的进程亦可被其他进程的信号唤醒。24 ◆STOPPED:挂起状态。进程被暂停,需要通过其他进程的信号才能被唤醒。导致这种状态的原因有两种。其一是受到了相关信号(SIGSTOP、SIGSTP、SIGTTIN或SIGTTOU)的反应;其二是受到父进程ptrace调用的控制,而暂时将处理机交给控制进程。25 ◆ZOMBIE:僵尸状态。表示进程结束但尚未消亡的一种状态。此时进程已经结束运行并释放大部分资源,但尚未释放进程控制块。26 二、进程的管理27 2.1进程调度调度程序(scheduler)用来实现进程状态之间的转换。在Linux中,调度程序由系统调用schedule()来完成。schedule()是一个怪异的函数,它与一般C语言函数不同,因为它的调用和返回不在同一个进程中。28 用户进程由fork()系统调用实现。fork()创建一个新的进程,继承父进程的现有资源,初始化进程时钟、信号、时间等数据。完成子进程初始化后,父进程将它挂到就绪队列,返回子进程的PID。29 进程创建时的状态为UNINTERRUPTIBLE,在fork()结束前被父进程唤醒后,变为RUNNING。处于RUNNING状态的进程被移到就绪队列中,在适当时候由schedule()按处理机调度算法选中,获得处理机。30 获得处理机而正在运行的进程若申请不到某个资源,则调用sleep()进行休眠,其PCB挂到相应的等待队列,状态变为UNINTERRUPTIBLE或者INTERRUPTIBLE。sleep()将调用schedule()函数把休眠进程释放的处理机分配给就绪队列中的某个进程.31 状态为INTERRUPTIBLE的休眠进程当它申请的资源有效时被唤醒,也可以由信号或定时中断唤醒。而状态为UNINTERRUPTIBLE的休眠进程只有当它申请的资源有效时被唤醒,不能被信号和定时中断唤醒。唤醒后,进程状态改为RUNNING,并进入就绪队列。32 进程执行系统调用exit()或收到外部的杀死进程信号SIG_KILL时,进程状态变为ZOMBIE,释放所申请资源。同时启动schedule()把处理机分配给就绪队列中其他进程。33 若进程通过系统调用设置了跟踪标志位,则在系统调用返回前,进入跟踪状态,进程状态变为STOPPED,处理机分配给就绪队列中其他进程。只有通过其他进程发送SIG_KILL信号或继续信号SIG_CONT,才能把STOPPED进程唤醒。重新进入就绪队列。34 对每一个进程,其PCB块中都可以记录一种调度策略。进程调度算法可采用先进先出算法(FIFO)或轮转法(round-robin),有实时(这里的“实时”,只是一种说法)。实际上,未经改造的Linux很难实现“实时”)和非实时两种形式。35 若采用Linux的轮转法,当时间片到时(10ms的整数倍),由时钟中断触发,引起新一轮调度,把当前进程挂到就绪队列队尾。在schedule()中有一个goodness()函数,可以用来保证实时的进程可以得到优先调用。然而这只是在调用上优先,事实上在内核态下,实时进程并不能对普通进程进行抢占。所以在标准Linux中的实时并不是真正意义上的实时36 2.2信号(1)Linux系统信号信号主要用于通知进程异步事件的发生。在Linux中可以识别29种不同的信号,这些信号中的大部分都有了预先定义好的意义,进程可以显式的用kill或killpg系统调用来向另一个进程发信号。37 进程可以通过提供信号处理函数来取代对于任意信号的缺省反应,这种缺省反应一般都是终止进程。信号发生时,内核中断当前的进程,进程执行处理函数来响应信号,结束后恢复正常的进程处理。信号有自己的名称和特定的编号,见表5-3所示。38 5-3Linux系统信号39 (2)信号的产生条件(a)用户在按下特定的键之后,将向该终端上的前台进程组发送信号,如Ctrl+C快捷键。硬件异常会产生信号:如被0除、无效内存引用等。(c)kill(2)系统调用可允许进程向其他进程或进程组发送任意信号。(d)kill(1)命令允许用户向进程发送任意信号。(e)软件设置的条件,如SIGALARM。40 信号发生时并不会立即得到处理,Linux在如下几种情况下才有可能处理进程的信号:每次进程从系统调用中退出时;内核在调度程序中选择执行该进程时。如果有任何一个未被阻塞的信号发出,内核就根据sigaction结构数组中的信息进行处理。41 (3)信号控制函数(a)捕捉信号此函数是信号处理程序或者信号捕捉函数。它可以决定系统对信号的响应。void(*signal(intsigno,void(*func)(int)))(int)42 其中signo是表1-1中的信号名;func的值是SIG_IGN或SIG_DFL或者是接到此信号后需要调用的函数地址。43 当func为SIG_IGN时则向内核表示忽略此信号;当func为SIG_DFL时表示接到此信号的动作是系统默认动作(一般是终止或忽略);当func为函数地址时为捕捉信号。出错则返回SIG_ERR,成功返回信号处理配置。44 (b)发送信号①kill和raiseintkill(pid_tpid,intsig);向其他进程发送信号intraise(intsig);向当前进程发送信号45 其中kill的参数pid>0是将信号发送到PID为pid的进程;pid=0将信号发送到与发送进程处于同一进程组的进程;pid<-1将信号发送到进程组ID等于-pid的所有进程;pid=-1POSIX未指定,Linux发送到进程表中所有的进程(除第一个进程之外)。46 sig=0时,不会发送任何信号,但仍然执行错误检查,因此可用来检查是否有向指定进程发送信号的许可。raise等价于kill(getpid(),sig)即发信号到当前进程。成功返回为0,出错为-1。47 ②alarmunsignedintalarm(unsignedintseconds);这个函数可以用来设置一个时间值(闹钟时间),当所设置的值被超过以后,产生SIGALRM信号,默认动作是终止进程。48 seconds指定了设置的时间值,过了这个秒数就会发出SIGALRM信号。每个进程只有一个闹钟时间。如果在本次调用alarm以前曾经设置过闹钟时间,而且没有超时,则本次函数的返回就是以前的余留值,并且以前的闹钟值被新的设定刷新。49 ③pauseintpause(void);可以使进程挂起,直到捕捉到一个信号。执行了一个信号处理函数后pause函数返回,错误返回-1,errno设为EINTR。50 ④sleepunsignedintsleep(unsignedintseconds);此函数挂起调用中的进程,直到过了预定时间或者是收到一个信号并从信号处理程序返回。51 可以由alarm函数实现。seconds表明设定的时间值,超过此值后恢复进程;当超时返回时,返回值是0,当由于捕捉到某个信号而提早返回时,返回值是seconds减去已经过去的秒数。52 例:程序执行2秒打印hello字符串#include#includevoidhandler(){printf(“hello ”);}53 main(){inti;signal(SIGALRM,handler);alarm(2);for(i=1;i<10;i++){printf(“sleep%d ”,i);sleep(1);}}54 执行结果:sleep1sleep2hellosleep355 (c)信号控制的高级方法#includeintsigaction(intsig,conststructsigaction*act,structsigaction*oact);功能:设置与信号sig相关联的动作。如果oact不是空指针,sigaction将把原先对该信号的动作写到它指定的位置。如果oact是空指针,就不作其他设置,否则在该参数中设置对指定信号的动作。成功返回0,失败返回-156 sigaction结构定义在signal.h中,作用是在接收到参数sig指定的信号后应该采取的行动。该结构至少应该包括以下几个成员:void(*)(int)sa_handler/*function,SIG_DFLorSIG_IGNsigset_tsa_mask/*signalstoblockinsa_handlerintsa_flags/*signalactionmodifiers其中,sa_handler是一个函数指针,它指向接收到信号sig时将被调用的信号处理函数,或是SIG_DFLorSIG_IGN;sa_mask字段指定一个信号集,在调用sa_handler所指向的信号处理函数之前,该信号集将加入到进程的信号屏蔽字(一组将被阻塞且不会传递给该进程的信号)中。57 #include#include#includevoidouch(intsig){printf(“OUCH!–Igotsignal%d ”,sig);}58 intmain(){structsigactionact;act.sa_handler=ouch;sigemptyset(&act.sa_mask);act.sa_flags=0;sigaction(SIGINT,&act,0);while(1){printf(“helloworld! ”);sleep(1);}}59 三、进程运行和控制60 Linux中除了0号进程是启动时由系统创建,其余进程都是由其他进程自行创建的。为了表示这种创建关系,用父进程指代缔造者,用子进程指代被创建出的新进程。如果进程A是进程B的间接父进程,则A称做B的祖先,B为A的后代。既然提到了父子关系,那么这两个进程之间自然是有着如同父子一样的继承性。61 在数据结构上,父进程PCB中的指针p_cptr指向最近创建的一个子进程的PCB块,而每个子进程PCB中的指针p_pptr都指向其父进程的PCB块。这一对指针构成了进程的父子关系,如图5-4所示。62 图5-4父子进程关系63 此外,除了最老的子进程外,每个子进程PCB块中的p_osptr指针都指向其父进程创建的上一个子进程PCB;反之,除了最新的子进程外,每个子进程PCB块中的p_ysptr都指向其父进程所创建的后一个子进程PCB。64 这样,同一个父亲的子进程们就按“年龄”顺序构成了一个双向链表,而父进程则可以通过其p_cptr指针,从最新创建的子进程开始,依次访问到其每一个子进程。65 系统启动时,自行创建了0号进程,其所运行的代码是init_task()函数。该进程的作用是作为一切其他进程的父进程,就像亚当夏娃是一切人类的祖先那样。0号进程不能自动生成,必须手工将其设置到进程表中去,才能启动进程管理机制。66 在启动进程管理机制以后,就可以由进程自行创建新的子进程。创建新进程的调用是fork()。fork一词在英文中是“分叉”的意思。同样,在Linux中,fork()调用也起了一个“分叉”的作用。当进程A调用fork()生成进程B时,fork()函数同时在A和B两个进程中返回。其中,父进程A里的fork()返回了子进程的PID,而子进程B里的fork()返回0。67 fork()函数究竟做了些什么呢?我们发现,经过fork()以后,父进程和子进程拥有相同内容的代码段、数据段和用户堆栈,就像父进程把自己克隆了一遍。在早期,在调用系统调用fork创建一个进程时,子进程完全复制父进程的资源,这样得到的子进程独立于父进程。但是通过fork创建子进程系统开销很大,需要将父进程的每种资源都复制一个副本,因此,fork是一个开销十分大的系统调用,这些开销并不是所有的情况下都是必须的,比如某进程fork出一个子进程后,其子进程仅仅是为了调用exec执行另一个执行文件,那么在fork过程中对于虚存空间的复制将是一个多余的过程;68 有些系统为了克服fork调用的缺点,创建了vfork调用。用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,子进程对虚拟地址空间任何数据的修改同样为父进程所见。但是用vfork创建子进程后,父进程会被阻塞直到子进程调用exec或exit。这样的好处是在子进程被创建后仅仅是为了调用exec执行另一个程序时,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的,通过vfork可以减少不必要的开销。69 目前,由于采用copy-on-write技术,在调用fork时,子进程只复制了自己的PCB块,而代码段、数据段、用户堆栈内存空间并没有复制一份,而是与父进程共享。只有当子进程在运行中出现写操作时,才会产生中断,并为子进程分配内存空间。这样就极大的提高了fork函数的效率。70 四、Linux进程控制编程71 4.1进程控制编程基础(1)进程创建Linux系统中用来创建新进程的方法是fork()函数,它与其他函数有很大的区别,该函数执行一次有两返回值。72 fork()的语法要点所需头文件#include#include函数原型pid_tfork(void)返回值0:子进程子进程ID(大于0的整数):父进程-1:出错fork使用示例73 (2)exec函数族一般在调用fork()函数以后,可以用exec函数族来启动另一个程序的执行。exec函数族可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完后,原调用进程的内容除了进程号外,其他的全部被新的进程替换了。74 exec函数族语法所需头文件#include函数原型intexecl(constchar*path,constchar*arg,...);intexecv(constchar*path,char*constargv[]);intexecle(constchar*path, constchar*arg,...,char*constenvp[]);intexecve(constchar*path,char*constargv[],char*constenvp[]);intexeclp(constchar*file,constchar*arg,...);intexecvp(constchar*file,char*constargv[]);返回值:如果出错则返回-1,否则不返回。75 示例:#include#include#includeintmain(){if(fork()==0){if(execlp("ps","ps","-ef",NULL)<0)perror("execlperror!");}}76 (3)exit和_exitexit和_exit函数都可以用来终止一个进程。区别是:_exit立即进入内核,exit则先执行一些清除处理(包括调用执行各终止处理程序,关闭所有标准I/O流等),然后进入内核。所需头文件:exit:#include_exit:#include函数原型:exit:voidexit(intstatus)_exit:void_exit(intstatus)77 示例:1)使用exit:#include#includeintmain(){printf("Usingexit... ");printf("Thisisthecontentinbuffer");exit(0);}78 该程序的输出为:Usingexit...Thisisthecontentinbuffer79 2)使用_exit:#include#includeintmain(){printf("Using_exit... ");printf("Thisisthecontentinbuffer");_exit(0);}80 该程序的输出为:Using_exit...81 (4)wait和waitpid函数#include#includepid_twait(int*status);wait()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一快返回。如果不在意结束状态值,则参数status可以设成NULL。子进程的结束状态值请参考waitpid()。如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno中。82 pid_twaitpid(pid_tpid,int*status,intoptions);waitpid()会暂时停止目前进程的执行,直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值。子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一快返回。如果不在意结束状态值,则参数status可以设成NULL。参数pid为欲等待的子进程识别码,其他数值意义如下:pid<-1等待进程组识别码为pid绝对值的任何子进程。pid=-1等待任何子进程,相当于wait()。pid=0等待进程组识别码与目前进程相同的任何子进程。pid>0等待任何子进程识别码为pid的子进程。83 waitpid使用示例本例中首先使用fork新建一个子进程,然后让子进程暂停5秒。接下来对父进程使用waitpid函数,并使用参数WNOHANG使该父进程不会阻塞。父进程每隔一秒循环判断一次,如果子进程没有退出,则显示相关消息,如果子进程退出了,则程序结束。程序源代码(waitpid.c)84 该程序的运行结果为:ThechildprocesshasnotexitedThechildprocesshasnotexitedThechildprocesshasnotexitedThechildprocesshasnotexitedThechildprocesshasnotexitedGetchild75如果把“pr=waitpid(pc,NULL,WNOHANG)”改为“pr=waitpid(pc,NULL,0)”或者“pr=wait(NULL)”,则运行结果为:Getchild7685 4.2Linux守护进程编程守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程通常是在系统引导时启动,在系统关闭时终止。linux系统有很多守护进程,大多数服务都是用守护进程实现的。比如,作业规划进程crond、打印进程lqd等。守护进程可以Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。86 进程组和会话期进程组每个进程除了有一进程ID之外,还属于一个进程组(在讨论信号时就会涉及进程组)进程组是一个或多个进程的集合。每个进程有一个唯一的进程组ID。进程组ID类似于进程ID——它是一个正整数,并可存放在pid_t数据类型中。可以调用getpgrp()查看当前进程的进程组ID。每个进程组有一个组长进程。组长进程的标识是其进程组ID等于其进程ID,进程组组长可以创建一个进程组,创建该组中的进程,然后终止,只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生命期。某个进程组中的最后一个进程可以终止,也可以参加另一进程组。87 会话期会话期(session)是一个或多个进程组的集合。一个会话期可以有一个单独的控制终端(controlling terminal),这一般是我们在其上登录的终端设备(终端登录)或伪终端设备(网络登录),但这个控制终端并不是必需的。建立与控制终端连接的会话期首进程,被称之为控制进程(contronllingprocess)。一个会话期中的几个进程组可被分为一个前台进程组(foreground process group)以及一个或几个后台进程组(background process group)如果一个会话期有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。无论何时键入中断键(常常是delete或ctrl-c)或退出键(通常是ctrl-),就会造成将中断信号或退出信号送至前台进程组的所有进程。88 守护进程的编写规则守护进程比较复杂,但是只要掌握其编写的一般流程,我们也可以很方便的编写自己的守护进程。编写守护进程一般的步骤如下所述:1)创建子进程,父进程退出为避免挂起控制终端,要将daemon放入后台执行,其方法是,在进程中调用fork使父进程终止,让daemon在子进程中后台执行。具体就是调用f o r k,然后使父进程e x i t。pid=fork();if(pid>0){exit(0);}89 2)脱离控制终端,登录会话和进程组这是创建守护进程中最重要的一步,需要使用setsid这个函数。setsid函数的语法所需头文件:#include#include函数原型:pid_tsetsid(void)返回值:成功:该进程组ID错误:-1setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时与控制终端脱离。90 3.禁止进程重新打开控制终端进过第二步,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:pid=fork();if(pid>0){exit(0);}91 4)改变当前目录为根目录从父进程继承过来的当前工作目录可能在一个可卸载的文件系统中。因为守护进程通常在系统再引导之前是一直存在的,所以如果精灵进程的当前工作目录在一个可卸载文件系统中,那么该文件系统就不能被拆卸,这样可能会出现某些不便。所以一般在守护经常中把当前工作目录更改为根目录,这样就可以避免上述问题。改变工作目录可以使用chdir函数。#includeintchdir(constchar*path)返回值:成功:0;不成功:-1;92 5)重设文件权限掩码文件权限掩码是只屏蔽掉文件权限中对应位。由继承得来的文件方式创建屏蔽字可能会拒绝设置某些许可权。例如,若精灵进程要创建一个组可读、写的文件,而继承的文件方式创建屏蔽字,屏蔽了这两种许可权,则所要求的组可读、写就不能起作用。因此我们一般把文件权限掩码设置为0,这样就可以读写所有文件。设置文件权限掩码的函数是umask;#include#includemode_tumask(mode_tmask)返回值:这个函数总是执行成功并返回执行前的文件权限掩码。93 6)关闭打开的文件描述符进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在地文件系统无法卸下以及无法预料的错误。一般来说,必要的是关闭0、1、2三个文件描述符,即标准输入、标准输出、标准错误。因为我们一般希望守护进程自己有一套信息输出、输入的体系,而不是把所有的东西都发送到终端屏幕上。调用fclose();94 守护进程编程示例本例首先创建一个守护进程,然后让守护进程每隔60s在/tmp/dameon.log中写入一句话;守护进程示例源代码(daemon.c)95 守护进程的出错处理守护进程不属于任何终端,所以当需要输出某些信息时,它无法像一般程序那样将信息直接输出到标准输出和标准错误输出中。我们很大时候也不希望每个守护进程将它自己的出错消息写到一个单独的文件中。因为对于系统管理人员而言,要记住哪一个守护进程写到哪一个记录文件中,并定期的检查这些文件,他一定会为此感到头疼的。所以,我们需要有一个集中的守护进程出错记录机制。目前很多系统都引入了syslog记录进程来实现这一目的。Syslog是Linux系统中的系统日志管理服务,通常由守护进程syslogd来维护。此守护进程在启动时读一个配置文件。一般来说,其文件名为/etc/syslog.conf,该文件决定了不同种类的消息应送向何处。例如,紧急消息可被送向系统管理员(若已登录),并在控制台上显示,而警告消息则可记录到一个文件中。该机制提供了syslog函数,其调用格式如下96 #include voidopenlog(char*ident,intoption ,intfacility);voidsyslog(intpriority,char*format,……)voidcloselog();调用openlog是可选择的。如果不调用openlog,则在第一次调用syslog时,自动调用openlog。调用closelog也是可选择的,它只是关闭被用于与syslog守护进程通信的描述符。调用openlog使我们可以指定一个ident,以后, 此ident将被加至每则记录消息中。ident一般是程序的名称(例如 ,cron,inetd等)。97 option有4种可能:LOG_CONS若日志消息不能通过Unix域数据报发送至syslog,则将该消息写至控制台。LOG_NDELAY1立即打开Unix域数据报套接口至syslog守护进程,而不要等到记录第一消息。通常,在记录第一条消息之前,该套接口不打开。LOG_PERROR除将日志消息发送给syslog外,还将它至标准出错。此选项仅由4.3BSDReno及以后版本支持。LOG_PID每条消息都包含进程ID。此选项可供对每个请求都fork一个子进程的守护进程使用。98 facilityfacility参数用来指定何种程式在记录讯息,这可让设定档来设定何种讯息如何处理。LOG_AUTH:安全/授权讯息(别用这个,请改用LOG_AUTHPRIV)LOG_AUTHPRIV:安全/授权讯息LOG_CRON:时间守护进程专用(cron及at)LOG_DAEMON:其它系统守护进程LOG_KERN:核心讯息LOG_LOCAL0到LOG_LOCAL7:保留LOG_LPR:lineprinter次系统LOG_MAIL:mail次系统LOG_NEWS:USENETnews次系统LOG_SYSLOG:syslogd内部所产生的讯息LOG_USER(default):一般使用者等级讯息LOG_UUCP:UUCP次系统99 在syslog(intpriority,char*format,……)中:priotity决定讯息的重要性.以下的等级重要性逐次递减:LOG_EMERG:系统无法使用LOG_ALERT:必须要立即采取反应行动LOG_CRIT:重要状况发生LOG_ERR:错误状况发生LOG_WARNING:警告状况发生LOG_NOTICE:一般状况,但也是重要状况LOG_INFO:资讯讯息LOG_DEBUG:除错讯format以字符川指针的形式表示的输出格式。100 syslog使用示例本例中对上例程序使用syslog服务进行重写,其源代码为syslog_dema.c101 五、进程通讯102 5.1进程间通信用户态进程间处于并发状态。为了协调进程的运行,需要实现进程之间通信的机制。103 Linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“systemVIPC”,通信进程局限在单个计算机内;后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。Linux则把两者继承了下来,如图示:104 105 在Linux中,常见的进程间通信方法包括:1).管道机制。该机制最适用于解决生产者――消费者问题。管道是一种在进程之间单向流动数据的结构,具有固定的读端fd[0]和写端fd[1]。管道只能用在具有亲缘关系的进程之间通信。管道可以看成特殊文件,我们可以用readwrite等普通函数对其进行操作,但是它只存在于内存中。106 管道的创建和关闭创建函数:intpipe(intfd[2])关闭:关闭管道只需要使用close函数关闭其打开的文件描述符即可。管道读写需要注意的问题管道在写入时不会保证写入操作的原子性。虽然通过pipe()调用会产生两个描述符(写管道和读管道),但是在写之前必须关闭读管道,反之亦然。107 管道使用示例:本例中,首先创建管道,之后创建一个子进程,然后关闭父进程的读描述符和子进程的写描述符,建立他们之间的管道通信。源代码(pipe_rw.c)108 2).先进先出(FIFO)机制。管道机制的最大缺点是不能由多个进程共享,除非此管道为这些进程共同的祖先所创建。为了解决这个问题,Linux中引入了FIFO机制(又称为namedpipe,有名管道)。109 FIFO为“firstin,firstout”的简写,指一个在磁盘上的文件,它可以被所有进程所共享。但是FIFO与一般文件不同,它还使用了内核中的缓冲区,所以在效率上要比一般共享文件快得多。110 mkfifo函数(创建有名管道)所需头文件:#include#include函数原型:intmkfifo(constchar*pathname,mode_tmode);mkfifo()会依参数pathname建立特殊的FIFO文件,该文件必须不存在,而参数mode为该文件的权限。返回值:成功0;出错-1,错误原因保存在errno中111 管道创建成功后就可以使用open、read、write等函数进行操作。需要注意的一点是FIFO的读写是可以阻塞的(在open函数中可设定非阻塞标志O_NONBLOCK)。如果FIFO是阻塞打开,对于读进程,若FIFO中没有数据可读,则读进程会被阻塞;对于写进程,则写进程会被阻塞直到有读进程读出数据。112 使用示例本例中包含两个程序,一个用于读管道(fifo_write.c),一个用户写管道(fifo_read.c),两个程序都是使用非阻塞方式读写管道。113 3.IPC机制。IPC是“interprocesscommunication”的缩写形式。它包含了一系列系统调用,允许用户态进程通过信号量进行同步,向其他进程发消息,并且可以与其他进程共享一块内存空间.IPC首先是在一个叫做“ColumbusUnix”的系统中实现的,其后在现代Unix类操作系统中广为流行。114 如上文所述,IPC资源包括信号量,消息队列和共享内存几种。115 (1)消息队列消息队列是由内核创建并维护的一个数据结构,它是有标识的。任何具有足够权限的进程都可以向消息队列中放置一个消息,同样,任何具有足够权限的进程都可以从中读取一个消息。这样,不同的进程通过访问相同的消息队列便可实现进程间通信。116 (2)共享内存共享内存区是这几种进程间通信方式中最快的一种。它的特点除了速度快外,而且可传递的信息量大。它是通过将一段内存区映射到一个进程的地址空间来实现。117 因此,这种进程间通信就不再涉及到内核(即进程不是通过执行任何进入内核的系统调用来传递数据的。这样,内核必须建立允许各个进程共享该内存区的内存映射关系,然后一直管理该内存区)。但同时,也要有效地保证它能同步、有序且没有死锁。118 用共享内存实现过程如下:①服务器取得访问该共享内存区的权限。②服务器从输入文件读取数据到该共享内存区。③服务器读入数据完毕时,通知用户进程。④用户从该共享内存区读出这些数据并输出。119 (3)信号量信号量并不是一种IPC机制,它是用于提供不同进程间或一给定进程的不同线程间同步的一种手段。120 信号量主要包括以下几种类型:①二值信号量:其值为0或1。资源被锁住而不可用时,值为0;资源可用时,值为1。②计数信号量:其值在0和某个限制值之间。它的值一般是可用的资源数。③计数信号量集:由一个或多个信号量构成的一个集合,其中每一个都是计数信号量。每个集合的信号量数都有一个限制值,一般在25个以上。121 以计数信号量为例,在有信号量机制的程序中,一个进程为了获得共享资源,需要执行以下操作:122 测试控制该资源的信号量,若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示它使用了一个资源;若此信号量的值为0,则进入睡眠状态,直至信号量的值大于0为止;若进入1状态,则进程被唤醒。进程不再使用该资源时,信号量的值就加1。如果这时有进程处在睡眠状态,则唤醒它。123 一个IPC资源将永久地驻留在内存中,除非进程显式地释放它。并且,IPC资源不仅能被其创建进程的后代所共享,任何进程都可以共享它。124 六、线程概念及分类125 6.1线程特点多线程的概念是首先在UNIX中引入的,Linux也继承这一点。发展到现在多线程已经成为Linux编程中不可或缺的一种机制。126 线程和进程相比有以下优点:1.“节俭”的多任务操作方式在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种“昂贵”的多任务工作方式。而运行一个进程中的多个线程,它们彼此之间使用相同的地址空间、共享大部分数据、启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。127 2.线程间方便的通信机制对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,它们不仅共享全局变量,而且共享进程指令、大多数数据、打开的文件(如描述字)、信号处理程序和信号处置、当前工作目录、用户ID和组ID。所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。128 3.提高应用程序响应这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作置于一个新的线程,这样可以避免这种尴尬的情况。129 4.使多CPU系统更加有效操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。130 5.改善程序结构一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。131 6.数据共享问题有的变量不能同时被两个线程所修改,有的子程序中声明为静态的数据更有可能给多线程程序带来灾难性的打击,带来了同步问题,这些正是编写多线程程序时最需要注意的地方。132 6.2线程的基本概念线程是包含在进程中的一种实体.它有自己的运行线索,可以完成一定的任务,可与其他线程共享进程中的共享变量及部分环境、相互之间协同来完成进程所要完成的任务。133 线程能够比进程有更高的性能,这是由于同一个进程中的线程都有共性:多个线程将共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。134 进程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括:1.线程ID每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。135 2.寄存器组的值由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。136 3.线程的堆栈堆栈是保证线程独立运行所必须的。线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影响。137 4.错误返回码由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。138 5.线程的信号屏蔽码由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。139 6.线程的优先级由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。140 6.3线程的分类从调度的角度,线程可以分为用户线程和内核线程两类:141 1.用户线程用户线程是通过线程库实现的。它们可以在没有内核参与下创建、释放和管理。线程库提供了同步和调度的方法。这样进程可以使用大量的线程而不消耗内核资源,而且省去大量的系统开销。用户线程的实现是可能的,因为用户线程的上下文可以在没有内核干预的情况下保存和恢复。每个用户线程都可以有自己的用户堆栈,一块用来保存用户级寄存器上下文以及如信号屏蔽等状态信息的内存区。库通过保存当前线程的堆栈和寄存器内容载入新调度线程的那些内容来实现用户线程之间的调度和上下文切换。142 2.内核线程它的创建和撤消是由内核的内部需求来决定的,用来负责执行一个指定的函数,一个内核线程不需要和一个用户进程联系起来。它共享内核的正文段核全局数据,具有自己的内核堆栈。它能够单独的被调度并且使用标准的内核同步机制,可以被单独的分配到一个处理器上运行。内核线程的调度由于不需要经过态的转换并进行地址空间的重新映射,因此在内核线程间做上下文切换比在进程间做上下文切换快得多。143 七、线程编程基础本节将结合实例说明多线程编程中所使用到的一些函数和基本方法。144 7.1线程的基本操作函数以下先讲述4个基本线程函数,在调用它们前均要包括pthread.h头文件。然后再给出用它们编写的一个程序例子。145 1.创建线程函数intpthread_create(pthread_t*tid,constpthread_attr_t*attr,void*(*func)(void*),void*arg);第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性第三个参数是线程运行函数的起始地址最后一个参数是运行函数的参数。146 这里,我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL147 前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。148 2.等待线程的结束函数intpthread_join(pthread_ttid,void**status);第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。149 3.取自己线程ID函数:pthread_tpthread_self(void);线程都有一个ID以在给定的进程内标识自己。线程ID由pthread_creat返回,150 4.终止线程函数一个线程的结束有两种途径,一种是函数结束了,调用它的线程也就结束了;另一种方式是通过函数pthread_exit来实现。它的函数原型为:voidpthread_exit(void*status);151 唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。152 7.2简单的多线程编程Linux系统下的多线程遵循POSIX线程接口,称为pthread。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a。顺便说一下,Linux下pthread的实现是通过系统调用clone()来实现的。153 clone()是Linux所特有的系统调用,它的使用方式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关文档说明。下面展示一个最简单的多线程程序。154 #include#includevoidthread(void){inti;for(i=0;i<3;i++)printf("Thisisapthread. ");}155 intmain(void){pthread_tid;inti,ret;ret=pthread_create(&id,NULL,(void*)thread,NULL);if(ret!=0){printf("Createpthreaderror! ");exit();}156 for(i=0;i<3;i++)printf("Thisisthemainprocess. ");pthread_join(id,NULL);return(0);}157 运行程序,得到如下结果:Thisisthemainprocess。Thisisapthread。Thisisthemainprocess。Thisisthemainprocess.Thisisapthread.Thisisapthread。158 再次运行,可能得到如下结果:Thisisapthread.Thisisthemainprocess.Thisisapthread.Thisisthemainprocess.Thisisapthread.Thisisthemainprocess.前后两次结果不一样,这是两个线程争夺CPU资源的最终结果。159 7.3修改线程属性属性结构为pthread_attr_t,它在头文件/usr/include/bits/pthreadtypes.h中定义。属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。160 /*Attributesforthreads. */typedefstruct__pthread_attr_s{int__detachstate;//是否分离int__schedpolicy;//调度策略struct__sched_param__schedparam;//线程的运行优先级int__inheritsched;//显式指定还是继承调度策略和调度参数int__scope;//表示线程间竞争CPU的范围,也就是说线程优先级的有效范围size_t__guardsize;int__stackaddr_set;void *__stackaddr;size_t__stacksize;}pthread_attr_t;161 __detachstate,表示新线程是否与进程中其他线程脱离分离,如果置位则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。该参数涉及函数:intpthread_attr_setdetachstate(pthread_attr_t*attr,intdetachstate);intpthread_attr_getdetachstate(constpthread_attr_t*attr,int*detachstate);__detachstate可取值:PTHREAD_CREATE_JOINABLE,PTHREAD_CREATE_DETACH。162 __schedpolicy,表示新线程的调度策略,主要包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过pthread_setschedparam()来改变。该参数涉及函数:intpthread_attr_setschedpolicy(pthread_attr_t*attr,intpolicy);intpthread_attr_getschedpolicy(constpthread_attr_t*attr,int*policy);163 __schedparam,一个structsched_param结构,其中有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。该参数涉及函数:intpthread_attr_setschedparam(pthread_attr_t*attr, conststructsched_param*param);intpthread_attr_getschedparam(constpthread_attr_t*attr,structsched_param*param);164 __inheritsched,有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED。intpthread_attr_setinheritsched(pthread_attr_t*attr,intinherit);intpthread_attr_getinheritsched(constpthread_attr_t*attr,int*inherit);165 __scope,表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。该参数涉及函数:intpthread_attr_setscope(pthread_attr_t*attr,intscope);intpthread_attr_getscope(constpthread_attr_t*attr,int*scope);166 pthread_attr_t结构中还有一些值,但不使用pthread_create()来设置。另外还有pthread_attr_init(pthread_attr_t*attr),该函数用来初始化线程属性;pthread_attr_destroy(pthread_attr_t*attr),该函数使线程属性无效。167 属性设置示例在本示例中(pthread.c)线程1设为分离属性,线程2使用默认属性(非分离)。该程序的运行结果如下:Thisisapthread1.Thisisapthread2.Thisisapthread2.Thisisapthread2.168 7.4线程访问控制由于线程共享进程的资源和地址空间,因此在对这些资源进行操作时,必须考虑到线程间资源访问的唯一性。在程序(threadrace.c)中,主线程和新线程都将全局变量myglobal加一20次。但是程序本身产生了某些意想不到的结果,最后输出的myglobal为20,而不是我们所预期的40;169 在POSIX中线程同步的方法主要有互斥锁和信号量。一、互斥锁mutex是一种简单的加锁方法来控制对共享资源的存取。它只有两种状态:上锁和解锁。在同一时刻只能有一个线程掌握某个已经上锁的互斥锁,拥有上锁状态的线程能够对共享资源进行操作。若其他的线程希望上锁一个已经上锁了的互斥锁,则该线程会被挂起,直到上锁的线程释放互斥锁为止。170 1.创建和销毁有两种方法创建互斥锁,静态方式和动态方式。POSIX定义了一个宏PTHREAD_MUTEX_INITIALIZER来静态初始化互斥锁,方法如下:pthread_mutex_tmutex=PTHREAD_MUTEX_INITIALIZER;在LinuxThreads实现中,pthread_mutex_t是一个结构,而PTHREAD_MUTEX_INITIALIZER则是一个结构常量。动态方式是采用pthread_mutex_init()函数来初始化互斥锁,API定义如下:intpthread_mutex_init(pthread_mutex_t*mutex,constpthread_mutexattr_t*mutexattr)其中mutexattr用于指定互斥锁属性(见下),如果为NULL则使用缺省属性。pthread_mutex_destroy()用于注销一个互斥锁,API定义如下:intpthread_mutex_destroy(pthread_mutex_t*mutex)销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占用任何资源,因此LinuxThreads中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。171 2.Linuxpthread互斥属性有三种PTHREAD_MUTEX_INITIALIZER快速互斥PTHREAD_RECURSIVE_MUTEX_INITEALIZER_NP递归互斥PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP检错互斥172 3.锁操作锁操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。intpthread_mutex_lock(pthread_mutex_t*mutex)intpthread_mutex_unlock(pthread_mutex_t*mutex)intpthread_mutex_trylock(pthread_mutex_t*mutex)pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。173 mutex使用示例在前面的示例(threadrace.c)中,由于没有控制主线程和新线程对共享资源的访问,我们没有得到预期的结果。现在我们使用互斥锁机制来修改该程序,修改后的代码如(threadracemutex.c)所示,这样我们就可以得到预期的结果。174 二、信号量(信号灯)信号量本质上是一个非负的整数计数器,它可以用来控制对公共资源的访问,如果信号量的值大于0,则表示资源可用,否则表示资源不可用。信号量可以用于进程或者线程之间的同步和互斥两种情况。如果用于互斥,一般只需要设置一个信号量sem,操作流程如图5-6所示。如果用于同步,一般需要设置多个信号量,并安排不同的初始值来实现他们之间的顺序执行,操作流程如图5-7所示。175 开始初始化sem为1p操作v操作p操作v操作线程一执行线程2执行结束开始初始化sem1为1sem2为0p操作sem1v操作sem1p操作sem2v操作sem2线程一执行线程2执行结束图5-6信号量互斥操作图5-7信号量同步操作176 1.创建和注销intsem_init(sem_t*sem,intpshared,unsignedintvalue)这是创建信号灯的API,其中value为信号灯的初值,pshared表示是否为多进程共享而不仅仅是用于一个进程。LinuxThreads没有实现多进程共享信号灯,因此所有非0值的pshared输入都将使sem_init()返回-1,且置errno为ENOSYS。intsem_destroy(sem_t*sem)被注销的信号灯sem要求已没有线程在等待该信号灯,否则返回-1,且置errno为EBUSY。除此之外,LinuxThreads的信号灯注销函数不做其他动作。177 2.点灯和灭灯intsem_post(sem_t*sem)点灯操作将信号灯值原子地加1,表示增加一个可访问的资源。intsem_wait(sem_t*sem)intsem_trywait(sem_t*sem)sem_wait()为等待灯亮操作,等待灯亮(信号灯值大于0),然后将信号灯原子地减1,并返回。sem_trywait()为sem_wait()的非阻塞版,如果信号灯计数大于0,则原子地减1并返回0,否则立即返回-1,errno置为EAGAIN。3.获取灯值intsem_getvalue(sem_t*sem,int*sval)读取sem中的灯计数,存于*sval中,并返回0。178 信号量使用示例1)信号量互斥使用示例把上例中的互斥锁机制改为信号量机制。源代码threadracesem.c2)信号量同步使用示例本例中通过使用两个信号量来实现两个线程间的同步,通过观察其运行结果,可以确定该程序实现了先运行线程二,再运行线程一。源代码sem_syn.c179 八、本章小节本章主要介绍了进程概念、进程调度、进程通信和同步。另外介绍了线程定义、多线程同步及示例。在实际应用中,多线程的编程方法也是得到了广泛的使用,希望大家能用多线程技术写出高效实用的好程序来。180 练习题1.简述进程和线程的异同点。2.简述进程的通信方法。3.简述线程同步方法。4.上机试验,编制生产者-消费者程序。181