Linux程序设计——技术技巧与项目实践---进程调度与通信编程--第7章_第1页
Linux程序设计——技术技巧与项目实践---进程调度与通信编程--第7章_第2页
Linux程序设计——技术技巧与项目实践---进程调度与通信编程--第7章_第3页
Linux程序设计——技术技巧与项目实践---进程调度与通信编程--第7章_第4页
Linux程序设计——技术技巧与项目实践---进程调度与通信编程--第7章_第5页
已阅读5页,还剩77页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、2022-1-182022-1-181第七章 进程调度与通信编程7.1 LINUX下的进程概述7.2 进程的系统调用7.3 进程间通信7.4 信号7.5 守护进程7.6 实战技巧 巧妙使用Tab键2022-1-182022-1-1827.1 Linux下的进程概述Linux作为多用户操作系统,支持多道程序设计、分时处理和软实时处作为多用户操作系统,支持多道程序设计、分时处理和软实时处理,带有微内核的一些特征,在程序设计时需引进进程机制。当前正理,带有微内核的一些特征,在程序设计时需引进进程机制。当前正在运行的程序称为进程,对进程的监视和控制是在运行的程序称为进程,对进程的监视和控制是linux

2、系统管理员的核系统管理员的核心任务。管理员可以终止或重启一个进程,也可以指定一个新的优先心任务。管理员可以终止或重启一个进程,也可以指定一个新的优先级。命令级。命令“ps”和和“top”用于查看当前进程列表。如何用这些命令管理用于查看当前进程列表。如何用这些命令管理linux系统中的进程请参见本书第三章中的系统中的进程请参见本书第三章中的“3.7 进程管理命令进程管理命令”一节。一节。监视监视linux的标准工具的标准工具ps(process status)返回正在运行程序的信息,)返回正在运行程序的信息,包括程序运行的用户名,包括程序运行的用户名,CPU运行份额和时间。如果要手工终止程序运行

3、份额和时间。如果要手工终止程序或确定哪个程序让系统变慢,这些信息很有用。监视和控制进程很有或确定哪个程序让系统变慢,这些信息很有用。监视和控制进程很有必要。必要。使用使用ps、top、kill和和renice命令可以看到进程的运行情况并对它们进行命令可以看到进程的运行情况并对它们进行控制。进程是操作系统调度单位,进程控制。进程是操作系统调度单位,进程PCB用一个名为用一个名为task_struct的结的结构体表示,定义在构体表示,定义在/includelinuxsehed.h中。每个中。每个task_struct结构占结构占1680字节,系统中最大进程数由系统物理内存大小决定。每当创建一字节,

4、系统中最大进程数由系统物理内存大小决定。每当创建一个新进程时,便在内存中申请一个空个新进程时,便在内存中申请一个空task_struct结构,填入需要的信息。结构,填入需要的信息。7.1.1 7.1.1 进程的概念进程的概念2022-1-182022-1-183 指向该结构的指针也被加入到指向该结构的指针也被加入到task数组中,所有进程控制数组中,所有进程控制块都存储在块都存储在task数组中。在系统初始化后期,建立了第数组中。在系统初始化后期,建立了第一个进程块一个进程块INIT_TASK。为了便于找到当前正在执行的。为了便于找到当前正在执行的进程,进程,linux中定义了一个中定义了一个

5、current_set指针数组,其中每个指针数组,其中每个成员指向某成员指向某CPU上正在运行的进程,上正在运行的进程, 该数组定义格式在该数组定义格式在kemelsched.c中,具体形式是:中,具体形式是: struct task_struct * init_tasksNR_CPUS = &init_task, ; 进程由程序、数据和进程控制块进程由程序、数据和进程控制块PCB(Process Control Block)组成。当系统创建一个进程时,建立了一个组成。当系统创建一个进程时,建立了一个PCB。当进程消亡时,撤消了当进程消亡时,撤消了PCB。在进程活动的整个生命周期。在进

6、程活动的整个生命周期内,系统通过内,系统通过PCB对进程进行管理和调度。对进程进行管理和调度。 Linux中的进程分普通进程和实时进程两种,实时进程具中的进程分普通进程和实时进程两种,实时进程具有一定的紧迫性,对外部事件要作出快速响应,实时进程有一定的紧迫性,对外部事件要作出快速响应,实时进程的优先级高于普通进程。的优先级高于普通进程。2022-1-182022-1-1847.1.2 Linux进程的PCB结构Linux进程的进程的PCB用用task_struct结构体来表示,结构体来表示,task_struct结构体比较复杂,结构体比较复杂,共有共有80多个数据成员。下面仅就其主要数据成员按

7、功能划分并作说明。多个数据成员。下面仅就其主要数据成员按功能划分并作说明。1)volatile long state表示进程的当前状态。进程运行时,它会根据具体情况改变状态。进程状态表示进程的当前状态。进程运行时,它会根据具体情况改变状态。进程状态共有共有TASK_RUNNING (可运行状态可运行状态)、TASK_INTERRUPTIBLE(可中断的等可中断的等待状态待状态)、TASK_UNIN、TERRUPTIBLE(不可中断的等待状态不可中断的等待状态)、TASK_ZOMBIE(僵死状态僵死状态)和和TASK_STOPPED(暂停状态暂停状态)等等5种状态。种状态。2)long prio

8、rity进程优先级,进程优先级,priority的值给出了进程每次获取的值给出了进程每次获取CPU后后,可使用的时间片长度可使用的时间片长度(单位单位jiffies)。3)unsigned long rt_priorityrt_ priority的值给出了实时进程的优先级,的值给出了实时进程的优先级,rt_priority+1000给出进程每次获给出进程每次获取取CPU后,可使用的时间片长度后,可使用的时间片长度(单位是单位是jiffies)。4)long counter在轮转法调度时在轮转法调度时counter表示当前进程还可运行多久。在进程开始时被赋为表示当前进程还可运行多久。在进程开始时

9、被赋为priority的值,以后每隔一个时钟中断递减的值,以后每隔一个时钟中断递减1,减到,减到0时引起新一轮调度。时引起新一轮调度。1.调度数据成员2022-1-182022-1-1855)unsigned long policy表示该进程的进程调度策略。调度策略有表示该进程的进程调度策略。调度策略有: SCHED_OTHER 0,非实时进程,用基于优先权的轮转法。,非实时进程,用基于优先权的轮转法。 SCHED_ FIFO 1,实时进程,用先进先出算法。,实时进程,用先进先出算法。 SCHED_RR 2,实时进程,用基于优先权的轮转法。,实时进程,用基于优先权的轮转法。1)struct t

10、ask_struct *next_task,*prev_taskLinux中所有进程组成了双向链表,中所有进程组成了双向链表,next_task和和prev_task是链表前后向指针。是链表前后向指针。2)struct task_struct *p_opptr,*p_pptr, struct task_struct *p_cptr, p_ysptr,*p_osptr以上分别指向该进程的原始父进程、父进程、子进程和新老兄弟进程指针。以上分别指向该进程的原始父进程、父进程、子进程和新老兄弟进程指针。3)struct task_struct *pidhash_next, struct task_st

11、ruct *pidhash_pprev链入进程链入进程hash表前后指针。系统进程除链入双向链表外,还加到表前后指针。系统进程除链入双向链表外,还加到hash表中。表中。3. 进程标识进程标识1)uid_t uid与与gid_t gid。uid和和gid分别是运行进程的用户标识和用户组标识。分别是运行进程的用户标识和用户组标识。2)pid_t pid,pid_t pgrp。pid和和pgrp分别是运行进程的进程分别是运行进程的进程ID和进程组和进程组ID。2. 进程队列指针2022-1-182022-1-1864. 时间数据成员1)long per_cpu_utimeNR_CPUS per_c

12、pu_stimeNR_CPUSper_cpu_utime是用户态进程运行时间,是用户态进程运行时间,per_cpu_stime内核态进程运行时间。内核态进程运行时间。2)unsigned long start_time进程创建时间。进程创建时间。1)struct fs_struct *fs,fs保存进程本身与虚拟文件系统保存进程本身与虚拟文件系统VFS的关系信息。的关系信息。struct fs_structAtomic_t count;rwlock_t lock;int umask;struct dentry *root,*pwd,*altroot;struct vfsmount *rootm

13、nt,*pwdmnt,*altrootmnt;root、rootmnt是根目录的是根目录的dentry和其和其mount点的点的vfsmount。pwd、pwdmnt是当前工作目录的是当前工作目录的dentry和其和其mount点的点的vfsmount。altroot、altrootmnt是保存根节点被替换之后原来根目标的是保存根节点被替换之后原来根目标的dentry和其和其mount点的点的vfsmount。2)struct files_struct*files,files包含了进程当前所打开的文件。包含了进程当前所打开的文件。3)int link_count文件链的数目。文件链的数目。5.

14、 文件系统数据成员2022-1-182022-1-1876. 内存数据成员 1)struct mm_struct *mm 在在linux中,采用按需分页的策略解决进程的内存需求。中,采用按需分页的策略解决进程的内存需求。task_ struct的数据成员的数据成员mm指向关于存储管理的指向关于存储管理的mm_struct结构。结构。 2)struct mm_struct*active_mm Active_ mm指向活动地址空间。指向活动地址空间。 3)mm_segment_t addr_limit表示线程空间地址。表示线程空间地址。 用户线程空间地址:用户线程空间地址:0_0 xBFFFFFF

15、F。 内核线程空间地址:内核线程空间地址:0_0 xFFFFFFFF。 4)spinlock_t alloc_lock 用于申请空间时用的自旋锁。自旋锁的主要功能是临界区用于申请空间时用的自旋锁。自旋锁的主要功能是临界区保护。保护。2022-1-182022-1-1887. 页面管理 1)int swappable:1 进程占用的页面是否可换出。进程占用的页面是否可换出。swappable为为1表示可换出。表示可换出。 2)unsigned long min_fit,maj_flt 该进程累计该进程累计minor缺页次数和缺页次数和major缺页次数。缺页次数。 3)unsigned long

16、 nswap 该进程累计换出页面数。该进程累计换出页面数。 4)unsigned long swap_cnt 下一次循环最多可换出的页数。下一次循环最多可换出的页数。2022-1-182022-1-1898. 支持对称多处理机方式的数据成员 1)int has_cpu,processor processor指向进程正在使用的指向进程正在使用的CPU,has_cpu表示进程是否表示进程是否占有占有CPU。 2)struct thread_struct thread 记录该进程的记录该进程的CPU状态信息。状态信息。 1)unsigned short used_math 是否使用是否使用CPU。

17、2)char comm6 进程正在运行的可执行文件的文件名。进程正在运行的可执行文件的文件名。 3)volatile long need_resched 重新调度标志。当需要重新调度标志。当需要linux调度时该变量置位。调度时该变量置位。9. 其他数据成员2022-1-182022-1-18107.1.3 Linux进程的组织方式在在linux中,每个进程都有自己的中,每个进程都有自己的task_struct结构。系统拥有进程数取结构。系统拥有进程数取决于物理内存大小,进程数可能成千上万。决于物理内存大小,进程数可能成千上万。Linux采用如下几种组织采用如下几种组织方式来管理进程。方式来管

18、理进程。1. 哈希表哈希表哈希表是进行快速查找的一种有效的组织方式。哈希表是进行快速查找的一种有效的组织方式。Linux在进程中引入在进程中引入的哈希表叫做的哈希表叫做pidhash。在。在includelinuxsched.h中定义如下:中定义如下:struct task_struct *pidhashPIDHASH_SZ;PIDHASH _SZ在在includelinuxsched.h中定义,值为中定义,值为1024。系统根。系统根据进程的进程号求得据进程的进程号求得hash值,加到值,加到hash表中:表中:#define pid_hashfn(x)(x)8)(x)(PIDHASH_SZ

19、_1)其中,其中,PIDHASH_SZ_1是表中元素的个数,表中的元素是指向是表中元素的个数,表中的元素是指向task_ struct结构的指针。结构的指针。pid_ hashfn为哈希函数,将进程的为哈希函数,将进程的pid转换为表的转换为表的索引,通过该函数,可以将进程的索引,通过该函数,可以将进程的pid均匀地散列在它们的域中。如均匀地散列在它们的域中。如果知道进程号,可以通过果知道进程号,可以通过hash表很快地找到该进程,查找函数如下:表很快地找到该进程,查找函数如下:static inline struct task_ struct*find_task_by_pid(int pid

20、) struct task_stmct *p,*htable=&pidhashpid_hashfn(pid);for( p = *htable;p&ppid! =pid;p=p-pidhash_next);return p;2022-1-182022-1-18112.双向循环链表 哈希表的主要作用是根据进程的哈希表的主要作用是根据进程的pid可以快速地找到对应可以快速地找到对应的进程,但它没有反映进程创建的顺序,也无法反映进程的进程,但它没有反映进程创建的顺序,也无法反映进程之间的亲属关系,因此引入双向循环链表。每个进程之间的亲属关系,因此引入双向循环链表。每个进程task_s

21、truct结构中的结构中的prev_task和和next_task成员用来实现这成员用来实现这种链表。链表的头和尾都是种链表。链表的头和尾都是init_task(即即0号进程号进程)。通过宏。通过宏for_each_task可以方便地搜索所有进程;可以方便地搜索所有进程; #define for_each_task(p) for(p=&init_task(P=P-next_task)!=&init_task;)2022-1-182022-1-18123. 运行队列 当内核要寻找一个新的进程在当内核要寻找一个新的进程在CPU上运行时,一般只考虑上运行时,一般只考虑那些处于可运行状

22、态的进程,因为查找整个进程链表效率那些处于可运行状态的进程,因为查找整个进程链表效率是很低的,所以引入了可运行状态进程的双向循环链表,是很低的,所以引入了可运行状态进程的双向循环链表,也叫运行队列。运行队列容纳了系统中所有可以运行的进也叫运行队列。运行队列容纳了系统中所有可以运行的进程,它是一个双向循环队列,该队列通过程,它是一个双向循环队列,该队列通过task_struct结构结构中的两个指针链表维护。中的两个指针链表维护。 队列的标志有两个:一个是队列的标志有两个:一个是“空进程空进程”,一个是队列的长,一个是队列的长度。空进程是一个比较特殊的进程,只有系统中没有进程度。空进程是一个比较特

23、殊的进程,只有系统中没有进程可运行时它才会被执行,可运行时它才会被执行,linux将它看作运行队列的头,当将它看作运行队列的头,当调度程序遍历运行队列时,是从调度程序遍历运行队列时,是从idle_task开始到开始到idle_task结束。队列长度表示系统中处于可运行状态的进程数目,结束。队列长度表示系统中处于可运行状态的进程数目,用全局变量用全局变量nr_running表示,在表示,在kernelfork.c中定义如中定义如下:下: int nr_running = 1;若;若nr_running为为0,表示队列中只有,表示队列中只有空进程。空进程。2022-1-182022-1-18137

24、.1.4 Linux进程异常检测 系统调用是操作系统中位于用户空间的用户程序和位于内系统调用是操作系统中位于用户空间的用户程序和位于内核空间的操作系统内核程序之间的功能接口,在核空间的操作系统内核程序之间的功能接口,在linuxUNIX系统中,用户和内核的交互就是通过系统调用完成系统中,用户和内核的交互就是通过系统调用完成的。而针对的。而针对linuxUNIX系统的大多数攻击也是通过非法系统的大多数攻击也是通过非法执行系统调用来进行的。一个进程执行时发出的系统调用执行系统调用来进行的。一个进程执行时发出的系统调用序列一定程度上反映出进程的行为特征,程序受到入侵后序列一定程度上反映出进程的行为特

25、征,程序受到入侵后在其执行时产生的系统调用序列将会有一定的反映。目前在其执行时产生的系统调用序列将会有一定的反映。目前针对进程进行的入侵检测都是对进程执行时产生的系统调针对进程进行的入侵检测都是对进程执行时产生的系统调用进行监控,通过不同的分析方法检测是否受到入侵,从用进行监控,通过不同的分析方法检测是否受到入侵,从而达到保护主机的目的。而达到保护主机的目的。2022-1-182022-1-18147.2 进程的系统调用7.2.1 getpid的用法的用法 在在linux的的2.4.4版内核中,版内核中,getpid是第是第20号系统调用,其在号系统调用,其在linux函数库函数库中的原型是:

26、中的原型是: #include /* 提供类型提供类型pid_t的定义的定义 */#include /* 提供函数的定义提供函数的定义 */pid_t getpid(void);函数函数getpid作用简单,返回当前进程的进程作用简单,返回当前进程的进程ID,请看例子:,请看例子:/* getpid_test.c */#include int main( ) printf(The current process ID is %dn,getpid( );2022-1-182022-1-1815图7.1 getpid( )的调用 这个程序里没有包含头文件这个程序里没有包含头文件sys/types.

27、h,因为在程序中没,因为在程序中没有用到有用到pid_t类型,也就是进程类型,也就是进程ID类型。在类型。在i386架构上,架构上,pid_t类型和类型和int类型完全兼容,可以用整形数处理类型完全兼容,可以用整形数处理pid_t类类型的数据,比如,用型的数据,比如,用%d把它打印出来。编译并运行程把它打印出来。编译并运行程序序getpid_test.c的结果如图的结果如图7.1所示。所示。 读者程序的运行结果与这个数字不一样,也是很正常的。读者程序的运行结果与这个数字不一样,也是很正常的。 再运行一遍结果也会不一样。尽管程序相同,每次运行时再运行一遍结果也会不一样。尽管程序相同,每次运行时分

28、配的进程标识符都有所不同。分配的进程标识符都有所不同。 2022-1-182022-1-18167.2.2 fork的用法与多进程解惑 在在linux内核中,系统调用内核中,系统调用fork在在linux函数库中的原型是:函数库中的原型是: #include /* 提供类型提供类型pid_t的定义的定义 */ #include /* 提供函数的定义提供函数的定义 */ pid_t fork(void); 只看只看fork的名字,很难猜出它是做什么的。的名字,很难猜出它是做什么的。fork系统调用系统调用的作用是克隆一个进程。当一个进程调用它,完成后就出的作用是克隆一个进程。当一个进程调用它,完

29、成后就出现两个一模一样的进程,由此就得到了一个新进程。在现两个一模一样的进程,由此就得到了一个新进程。在linux中,创建新进程的方法只有中,创建新进程的方法只有fork一个。其他库函数如一个。其他库函数如system( ),看起来似乎也能创建新进程,但实际上也在内,看起来似乎也能创建新进程,但实际上也在内部调用了部调用了fork。包括在命令行下运行应用程序,新的进程。包括在命令行下运行应用程序,新的进程也是由也是由shell调用调用fork创建而来的。创建而来的。fork有一些很有意思的有一些很有意思的特征,也由难以理解之处。特征,也由难以理解之处。2022-1-182022-1-1817/

30、* fork_test.c */#include #inlcude int main( )pid_t pid; /*此时仅有一个进程此时仅有一个进程*/pid=fork( ); /*此时已经有两个进程在同时运行此时已经有两个进程在同时运行*/if(pid0) printf(error in fork!); else if(pid=0) printf(I am the child process, my process ID is %dn,getpid( ); else printf(I am the parent process, my process ID is %dn,getpid( );

31、return 0; 编译并运行的结果如图编译并运行的结果如图7.2所示。所示。 $gcc fork_test.c -o fork_test2022-1-182022-1-1818分析这个程序的流程,也许很难理解,用以前的分析这个程序的流程,也许很难理解,用以前的C理念来思考就会想理念来思考就会想不通:为什么不通:为什么if和和else语句能够同时执行?是不是很奇怪呢?语句能够同时执行?是不是很奇怪呢?实际上,并不是分支语句出了问题,而是实际上,并不是分支语句出了问题,而是fork调用的奇特之处起作用调用的奇特之处起作用了。可以说,了。可以说,fork函数是函数库里独一无二的。因为只有这一个函数

32、函数是函数库里独一无二的。因为只有这一个函数调用会返回两个值,其它的所有函数好像都没有这个特异功能。调用会返回两个值,其它的所有函数好像都没有这个特异功能。在语句在语句pid=fork( )之前,只有一个进程在执行这段代码,但在这条语之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程同时在执行,而且这两个进程的代码完全相句之后,就变成两个进程同时在执行,而且这两个进程的代码完全相同,它们要执行的代码都是随后的所有语句,相当于变成了两个子程同,它们要执行的代码都是随后的所有语句,相当于变成了两个子程序进入内存同时执行。但由于只有一个处理机,所以在真正执行时,序进入内存同时执行。但

33、由于只有一个处理机,所以在真正执行时,就看谁的动作快,就谁先执行。就看谁的动作快,就谁先执行。这两个进程中,原先就存在的那个进程称为父进程,新出现的那个称这两个进程中,原先就存在的那个进程称为父进程,新出现的那个称为子进程。父子进程的区别除了进程标志符为子进程。父子进程的区别除了进程标志符process ID不同外,变量不同外,变量pid的值也不相同,的值也不相同,pid存放的是存放的是fork调用的返回值。调用的返回值。fork调用的奇妙调用的奇妙之处就是它被调用一次,却返回两次,它可能有三种不同的返回值:之处就是它被调用一次,却返回两次,它可能有三种不同的返回值:2022-1-182022

34、-1-1819在父进程中,在父进程中,fork返回新创建子进程的进程返回新创建子进程的进程ID;在子进程中,;在子进程中,fork返返回回0值;如果出现错误,值;如果出现错误,fork返回返回-1值。值。fork出错可能有两种原因:出错可能有两种原因: (1)当前进程数已达系统规定上限时,)当前进程数已达系统规定上限时,errno值被设为值被设为EAGAIN。(2)系统内存不足,这时)系统内存不足,这时errno的值被设置为的值被设置为ENOMEM。fork系统调用出错的可能性很小,如果出错,一般都是第一种错误。系统调用出错的可能性很小,如果出错,一般都是第一种错误。如果出现第二种错误说明系统

35、已经没有可分配的内存,正处于崩溃边如果出现第二种错误说明系统已经没有可分配的内存,正处于崩溃边缘,这种情况是很罕见的。缘,这种情况是很罕见的。至此就完全能够看懂剩下的代码和程序的流程了。如果至此就完全能够看懂剩下的代码和程序的流程了。如果pid小于小于0,说,说明出错了;明出错了;pid=0,说明,说明fork返回了返回了0,当前进程是子进程,执行的是,当前进程是子进程,执行的是printf(I am the child!),else部分子进程是不会执行的。或者部分子进程是不会执行的。或者fork返返回了子进程的回了子进程的id值,执行的是值,执行的是else语句语句printf(I am t

36、he parent!)。为了说明进程的创建情况,这里请读者看看下面的为了说明进程的创建情况,这里请读者看看下面的fork_test2.c程序运程序运行结束后,行结束后, How、are、you各输出多少个?输出次序是否唯一?为什各输出多少个?输出次序是否唯一?为什么?么?2022-1-182022-1-1820#include #include #include void main( void ) fork( ); printf(Hown); fork( ); printf(aren); fork( ); printf(youn);这个程序的执行结果表明这个程序的执行结果表明How输出了输出了

37、2个,个,are是是4个,而个,而you则是则是8个。个。输出的次序也是不确定的,要想顺序输出,只要在每个输出的次序也是不确定的,要想顺序输出,只要在每个printf( )后面后面调用调用sleep(5)就可以保证完全按次序输出。就可以保证完全按次序输出。2022-1-182022-1-18217.2.3 exit和_exit在在linux的的2.4.4版内核中,版内核中,exit是第是第1号调用,在号调用,在linux函数库中的原型是:函数库中的原型是:#include void exit(int status);从从exit名字可以看出,这个系统调用是用来终止进程的。无论在程序中的什名字可

38、以看出,这个系统调用是用来终止进程的。无论在程序中的什么位置,只要执行到么位置,只要执行到exit,进程就会停止所有操作,清除包括,进程就会停止所有操作,清除包括PCB在内的各在内的各种数据结构,并终止本进程的运行。请看下面的种数据结构,并终止本进程的运行。请看下面的exit_test1.c程序:程序:/* exit_test1.c */#includeint main( ) printf(this process will exit!n); exit(0); printf(never be displayed!n);编译后运行:编译后运行: $gcc exit_test1.c -o exit

39、_test1$./exit_test1this process will exit!2022-1-182022-1-1822由此看来,程序并没有打印后面的由此看来,程序并没有打印后面的“never be displayed! ”,因为在执,因为在执行到行到exit(0)时,进程就已经终止了。时,进程就已经终止了。exit系统调用带有一个整型参数系统调用带有一个整型参数status,可以利用这个参数传递进程结,可以利用这个参数传递进程结束时的状态。如果返回值是束时的状态。如果返回值是0则表示正常结束,其它值则表示出现了则表示正常结束,其它值则表示出现了错误,进程非正常结束。在实际编程时,可以用错

40、误,进程非正常结束。在实际编程时,可以用wait系统调用接收子系统调用接收子进程的返回值,从而针对不同的情况进行不同处理。进程的返回值,从而针对不同的情况进行不同处理。作为系统调用,作为系统调用,_exit和和exit是一对孪生兄弟,它们非常相似。从是一对孪生兄弟,它们非常相似。从C语语言的角度看,言的角度看,_exit和和exit没有任何区别。实际上,二者的区别体现在没有任何区别。实际上,二者的区别体现在函数库中的定义不同。函数库中的定义不同。exit( )函数定义在函数定义在stdlib.h中,而中,而_exit( )定义在定义在unistd.h中。中。_exit( )函数的作用简单,直接

41、使进程停止运行,清除使函数的作用简单,直接使进程停止运行,清除使用的内存空间,销毁其在内核中的各种数据结构;用的内存空间,销毁其在内核中的各种数据结构;exit( )函数则在这函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序,所以可以些基础上作了一些包装,在执行退出之前加了若干道工序,所以可以认为认为exit已不是纯粹的系统调用了。已不是纯粹的系统调用了。exit( )函数与函数与_exit( )函数的区别在于函数的区别在于exit( )函数在调用函数在调用exit系统调用前系统调用前要检查文件打开情况,把文件缓冲区内容写回文件,就是图中的要检查文件打开情况,把文件缓冲区内容写回文

42、件,就是图中的“清清理理I/O缓冲缓冲”项。项。2022-1-182022-1-1823在在linux的标准函数库中,有一套称作的标准函数库中,有一套称作“高级高级I/O”的函数,主的函数,主要有要有printf( )、fopen( )、fread( )、fwrite( ),它们也被称,它们也被称作作“缓冲缓冲I/O即即buffered I/O”,特征是对应每一个打开的,特征是对应每一个打开的文件在内存中都有一片缓冲区,每次读文件时,会多读出文件在内存中都有一片缓冲区,每次读文件时,会多读出若干条记录,这样下次读文件时就可以直接从内存的缓冲若干条记录,这样下次读文件时就可以直接从内存的缓冲区中

43、读取,每次写文件时,也仅仅写入内存的缓冲区,等区中读取,每次写文件时,也仅仅写入内存的缓冲区,等满足了一定的条件,达到一定数量或遇到特定字符,如换满足了一定的条件,达到一定数量或遇到特定字符,如换行符和文件结束符行符和文件结束符EOF,再将缓冲区中的内容一次性写入,再将缓冲区中的内容一次性写入文件,大大提高了文件的读写速度,但为编程带来了一些文件,大大提高了文件的读写速度,但为编程带来了一些麻烦。如果有一些数据,认为已经写入了文件,但实际上麻烦。如果有一些数据,认为已经写入了文件,但实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,因为没有满足特定的条件,它们还只是保存在缓冲区内,这时若

44、用这时若用_exit( )函数直接将进程关闭,缓冲区中的数据就函数直接将进程关闭,缓冲区中的数据就会丢失,反之,如果想保证数据的完整性,就一定要使用会丢失,反之,如果想保证数据的完整性,就一定要使用exit( )函数。函数。 请看例程请看例程exit2.c:2022-1-182022-1-1824 /* exit1.c */#includemain( ) printf(output beginn); printf(content in buffer); exit(0); 编译并运行:编译并运行: $gcc exit1.c -o exit1 $./exit1 output begin conte

45、nt in buffer2022-1-182022-1-1825再看下面的例子再看下面的例子_exit2.c:/* _exit2.c */#include main( )printf(output beginn );printf(content in buffer );_exit(0);编译并运行:编译并运行:$gcc _exit2.c -o _exit2$./_exit2output begin2022-1-182022-1-1826可以看出二者的区别在于缓冲区中的数据是否丢失。可以看出二者的区别在于缓冲区中的数据是否丢失。printf在遇到换行符时从缓冲区中读取记录,而上面的在遇到换行符时

46、从缓冲区中读取记录,而上面的_exit(0)未读取第二个未读取第二个printf的内容就退出了。如果在第二个的内容就退出了。如果在第二个printf中都加上中都加上“n”则缓冲区中的数据就能够先读出再退出而则缓冲区中的数据就能够先读出再退出而不会丢失了。在不会丢失了。在linux中,标准输入和标准输出都是作为文中,标准输入和标准输出都是作为文件处理的,虽然是一类特殊文件,但从程序员的角度看,件处理的,虽然是一类特殊文件,但从程序员的角度看,它们和硬盘上存储数据的普通文件并没有任何区别。与所它们和硬盘上存储数据的普通文件并没有任何区别。与所有其它文件一样,它们在打开后也有自己的缓冲区。有其它文件

47、一样,它们在打开后也有自己的缓冲区。 2022-1-182022-1-1827 7.3 进程间通信进程间通信 一般来说,一般来说,linux下的进程包含几个关键要素,分别是:下的进程包含几个关键要素,分别是:(1) 有一段可执行程序;有一段可执行程序; (2) 有专用的系统堆栈空间;有专用的系统堆栈空间; (3)内核内核中有进程控制块描述进程占用的资源,这样,进程才能接中有进程控制块描述进程占用的资源,这样,进程才能接受内核的调度;受内核的调度; (4)具有独立的存储空间;具有独立的存储空间;(5)进程和线程进程和线程有时候并不完全区分,往往根据上下文理解其含义。有时候并不完全区分,往往根据上

48、下文理解其含义。2022-1-182022-1-18287.3.1 管道通信管道分为管道分为pipe(无名管道)和(无名管道)和FIFO(命名管道),它们都是通过内核缓冲区(命名管道),它们都是通过内核缓冲区按先进先出的方式进行数据传输。从管道的一端顺序地写入数据,而从另一按先进先出的方式进行数据传输。从管道的一端顺序地写入数据,而从另一端顺序地读出数据。读写位置都是自动增加的,数据读一次之后就释放。在端顺序地读出数据。读写位置都是自动增加的,数据读一次之后就释放。在缓冲区写满时,由相应规则控制读写进程进入等待队列,当空缓冲区有写入缓冲区写满时,由相应规则控制读写进程进入等待队列,当空缓冲区有

49、写入数据或满的缓冲区有数据读出时,就唤醒等待队列中的写进程继续读写。数据或满的缓冲区有数据读出时,就唤醒等待队列中的写进程继续读写。管道两端可分别用描述字管道两端可分别用描述字fd0及及fd1来描述,要注意的是管道两端任务划分来描述,要注意的是管道两端任务划分是固定的,即某一端为只读,由描述字是固定的,即某一端为只读,由描述字fd0表示,称为读端;另一端只能用表示,称为读端;另一端只能用于写,由描述字于写,由描述字fd1表示,称为管道写端。如果试图从写端读数据或向读端表示,称为管道写端。如果试图从写端读数据或向读端写数据都会导致错误发生。一般文件的写数据都会导致错误发生。一般文件的I/O函数都

50、可以用于管道读写与关闭操函数都可以用于管道读写与关闭操作,如作,如close、read、write等等。等等。1. 从管道中读取数据从管道中读取数据如果管道的写端不存在,则认为已经读到了数据末尾,读函数返回的读出字如果管道的写端不存在,则认为已经读到了数据末尾,读函数返回的读出字节数为节数为0;当管道写端存在时,如果请求的字节数大于;当管道写端存在时,如果请求的字节数大于PIPE_BUF,则返回管,则返回管道中现有的数据字节数。如果请求的字节数不大于道中现有的数据字节数。如果请求的字节数不大于PIPE_BUF,则返回管道,则返回管道中现有数据字节数,此时管道中数据量小于请求的数据量;或者返回请

51、求字中现有数据字节数,此时管道中数据量小于请求的数据量;或者返回请求字节数,此时管道中的数据量不小于请求的数据量。节数,此时管道中的数据量不小于请求的数据量。2022-1-182022-1-18292. 向管道中写入数据 向管道中写数据时,向管道中写数据时,linux不保证写入的原子性,管道缓冲不保证写入的原子性,管道缓冲区一旦有空闲区域,写进程就会试图向管道中写数据。如区一旦有空闲区域,写进程就会试图向管道中写数据。如果读进程没有读出管道缓冲区中的数据,那么写操作将一果读进程没有读出管道缓冲区中的数据,那么写操作将一直阻塞。管道的主要局限性体现在它的性质上:直阻塞。管道的主要局限性体现在它的

52、性质上:(1)只支持只支持单向数据流;单向数据流;(2)只能用于有亲缘关系的进程之间;只能用于有亲缘关系的进程之间;(3)无无命名字;命名字;(4)管道缓冲区有限(管道只存在于内存,在管道管道缓冲区有限(管道只存在于内存,在管道创建时为缓冲区分配一页的内存空间);创建时为缓冲区分配一页的内存空间);(5)管道传送的是管道传送的是无格式字节流,要求管道读和写双方必须事先约定好数据无格式字节流,要求管道读和写双方必须事先约定好数据格式,如多少字为一个消息或命令或记录等等。格式,如多少字为一个消息或命令或记录等等。2022-1-182022-1-18307.3.2 管道技术 管道是连接一个进程的输出

53、和另一个进程输入的单向通道。管道是连接一个进程的输出和另一个进程输入的单向通道。在在linux系统的各种进程通讯方法中,是最古老且应用最广系统的各种进程通讯方法中,是最古老且应用最广泛的一种。泛的一种。 当进程创建一个管道的时候,系统内核同时为该进程设立了当进程创建一个管道的时候,系统内核同时为该进程设立了一对文件句柄,一个用来从该管道获取数据,另一个则用来一对文件句柄,一个用来从该管道获取数据,另一个则用来作为管道的输入。作为管道的输入。 进程通过句柄进程通过句柄fd0向管道写入数据,同时通过向管道写入数据,同时通过fd1从管道从管道读出数据。在读出数据。在linux内核里,管道用一个内核里

54、,管道用一个inode节点表示,只节点表示,只存在于系统内核中。存在于系统内核中。 图图7.3 fork( )调用后的管道调用后的管道 当然,建立一个只能跟自己通讯的管道是没有意义的,建立当然,建立一个只能跟自己通讯的管道是没有意义的,建立管道的目标是通过管道实现进程间的通讯。在主进程中利用管道的目标是通过管道实现进程间的通讯。在主进程中利用fork( )函数创建一个自身的子进程,然后子进程继承了父进函数创建一个自身的子进程,然后子进程继承了父进程打开的文件句柄。利用继承的句柄,就可以实现父子进程程打开的文件句柄。利用继承的句柄,就可以实现父子进程间的通信了。见图间的通信了。见图7.3所示。所

55、示。2022-1-182022-1-1831 图图7.4 父子进程之间单向管道父子进程之间单向管道 现在,父子进程同时拥有一个管道的读写句柄,但是管道现在,父子进程同时拥有一个管道的读写句柄,但是管道必须是单向的,所以必须决定数据的流动方向,然后在每必须是单向的,所以必须决定数据的流动方向,然后在每个进程中关闭不需要的句柄。假如需要管道从子进程传送个进程中关闭不需要的句柄。假如需要管道从子进程传送数据到父进程,那么就应关闭子进程的读句柄和父进程的数据到父进程,那么就应关闭子进程的读句柄和父进程的写句柄,在图写句柄,在图7.4中建立了一个可用的单向管道。中建立了一个可用的单向管道。父父进进程程子

56、子进进程程句柄句柄fd1句柄句柄fd0父父进进程程子子进进程程句柄句柄fd1句柄句柄fd02022-1-182022-1-18327.3.3 无名管道编程 在在C程序中使用系统函数程序中使用系统函数pipe( )建立管道。建立管道。pipe( )函数的函数的原型是:原型是:#includeint pipe(int fd2) 其中的整形数组其中的整形数组fd存放新建管道句柄,存放新建管道句柄,fd0存放从管道中存放从管道中读出数据句柄,读出数据句柄,fd1存放向管道写入数据的句柄。函数调存放向管道写入数据的句柄。函数调用成功返回用成功返回0;调用失败返回;调用失败返回-1。2022-1-1820

57、22-1-1833【例】【例】7.1 编写程序使用管道实现数据的传输。编写程序使用管道实现数据的传输。#include#include#includemain( ) int fd2,nbytes; pid_t childpid; char string=Hello pipe!0; char readbuffer80; pipe(fd); /创建创建一个无名管道一个无名管道if(childpid=fork( )=-1)perror(fork);exit(1);2022-1-182022-1-1834if(childpid=0) close(fd0);/子进程关闭管道的读句柄子进程关闭管道的读句柄

58、write(fd1,string,strlen(string); /子进程通过管道写入数据子进程通过管道写入数据 Hello!;exit(0);elseclose(fd1);/父进程关闭管道的写父进程关闭管道的写句柄;句柄; nbytes=read(fd0,readbuffer,sizeof(readbuffer);/父进程通过管道读出数据父进程通过管道读出数据Hello pipe!printf(Received string:%sn,readbuffer);return(0);2022-1-182022-1-1835 程序运行结果如图7.5所示2022-1-182022-1-1836假设要让

59、管道中的数据从子进程流到父进程,则父进程就要假设要让管道中的数据从子进程流到父进程,则父进程就要关闭写管道句柄关闭写管道句柄fd1,而子进程要关闭读管道进程,而子进程要关闭读管道进程fd0。管道建立后就可以象操作普通文件一样对其进行操作。若管道建立后就可以象操作普通文件一样对其进行操作。若父进程先于子进程运行,执行到读语句时,由于管道没有父进程先于子进程运行,执行到读语句时,由于管道没有数据,父进程被阻塞。数据,父进程被阻塞。【例】【例】7.2 修改【例】修改【例】7.1程序,使父进程收到消息时,通过程序,使父进程收到消息时,通过管道给子进程返回一个消息。程序清单见下页。管道给子进程返回一个消

60、息。程序清单见下页。2022-1-182022-1-1837#include#include#includemain( ) int fd2, fd12;pid_t childpid;char string=Hello!;char readbuffer80;char readbuffer180;pipe(fd);pipe(fd1); if (childpid=fork( )=-1)perror(fork);exit(1); if (childpid=0) close(fd0);close(fd11); write(fd1,string,strlen(string);/1 read(fd10,readbuffer1,sizeof(r

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论