嵌入式Linux编程入门与开发实例-第9章.ppt

上传人:max****ui 文档编号:12724571 上传时间:2020-05-19 格式:PPT 页数:114 大小:1.07MB
返回 下载 相关 举报
嵌入式Linux编程入门与开发实例-第9章.ppt_第1页
第1页 / 共114页
嵌入式Linux编程入门与开发实例-第9章.ppt_第2页
第2页 / 共114页
嵌入式Linux编程入门与开发实例-第9章.ppt_第3页
第3页 / 共114页
点击查看更多>>
资源描述
第9章进程控制,操作系统的主要任务是管理计算机的软硬件资源,进程(process)在操作系统中执行特定的任务。操作系统借助进程来管理计算机的资源。而程序是存储在磁盘上包含可执行机器指令和数据的静态实体。进程或者任务是处于活动状态的计算机程序。理解和掌握进程,对于Linux下的编程来说是非常重要的。本章主要介绍进程的概念,进程的结构和进程的内存映像,以及进程的创建和退出、进程间的通信等操作。,第9章进程控制,9.1Linux进程,9.1.1Linux进程概述9.1.2Linux进程调度9.1.3进程的内存映像,9.1.1Linux进程概述,在Linux中,每个进程在创建时都会被分配一个数据结构,称为进程控制块(ProcessControlBlock,简称PCB)。PCB中包含了很多重要的信息,供系统调度和进程本身执行使用,其中最重要的莫过于进程ID(processID)了,进程ID也被称作进程标识符,是一个非负的整数,在Linux操作系统中唯一地标志一个进程。一个或多个进程可以合起来构成一个进程组(processgroup),一个或多个进程组可以合起来构成一个会话(session)。这样就有了对进程进行批量操作的能力,比如通过向某个进程组发送信号来实现向该组中的每个进程发送信号。,每个进程除了进程ID外还有一些其它标识信息,它们可以通过相应的函数获得。这些函数的声明在unistd.h头文件中,表10-1是这些函数的说明。用户ID和组ID的相关概念如下所示:实际用户ID(uid):标识运行该进程的用户。有效用户ID(euid):标识以什么用户身份来运行进程。实际组ID(gid):它是实际用户所属的组的组ID。有效组ID(egid):有效用户所属的组的组ID。,表9-1获取进程各种标识符的函数表,【例9-1】获取进程ID。,设计步骤1在Vim中创建一个新工程文件,命名为“example9_1.c”。2在“example9_1.c”中创建代码如下所示。,#include#include#includeintmain()printf(TheprocessIDis%dn,getpid();/*本进程*/printf(TheparentprocessIDis%dn,getppid();/*父进程*/printf(Theprocesspriorityis:%dn,getpriority(PRIO_PROCESS,getpid();return0;,3用GCC编译运行程序结果如图9-1所示。,图9-1获取进程ID例9-1的运行结果,1.Linux进程的组成Linux进程是由三部分组成:代码段、数据段和堆栈段。如图9-2所示。,图9-2Linux进程组成,(1)代码段代码段存放程序的可执行代码。(2)数据段数据段存放程序的全局变量、常量、静态变量。(3)堆栈段堆栈段中的堆用于存放动态分配的内存变量,栈用于函数的调用,它存放着函数的参数、函数内部定义的局部变量。,2.Linux进程的状态一个进程在其生存期内,可处于一组不同的状态下,称为进程状态。进程状态保存在进程任务结构的state字段中。当进程正在等待系统中的资源而处于等待状态时,则称其处于睡眠等待状态。在Linux系统中,睡眠等待状态被分为可中断的和不可中断的等待状态。,可运行状态(TASK_RUNNING)当进程正在被CPU执行,或已经准备就绪随时可由调度程序执行,则称该进程为处于运行状态(running)。进程可以在内核态运行,也可以在用户态运行。当系统资源已经可用时,进程就被唤醒而进入准备运行状态,该状态称为就绪态。这些状态在内核中表示方法相同,都被称为处于TASK_RUNNING状态。,可中断睡眠状态(TASK_INTERRUPTIBLE)当进程处于可中断等待状态时,系统不会调度该进行执行。当系统产生一个中断或者释放了进程正在等待的资源,或者进程收到一个信号,都可以唤醒进程转换到就绪状态(运行状态)。,不可中断睡眠状态(TASK_UNINTERRUPTIBLE)与可中断睡眠状态类似。但处于该状态的进程只有被使用wake_up()函数明确唤醒时才能转换到可运行的就绪状态。暂停状态(TASK_STOPPED)当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。处于该状态的进程将被作为进程终止来处理。僵死状态(TASK_ZOMBIE),图9-3进程状态转换示意图,查看进程当前的状态可以使用ps命令。使用该命令可以确定有哪些进程正在运行和运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等。ps命令格式如下:,ps选项,下面对命令选项进行说明-e显示所有进程;-f全格式;-h不显示标题;-l长格式;-w宽输出;-a显示终端上的所有进程,包括其他用户的进程;-r只显示正在运行的进程;-x显示没有控制终端的进程;-u以用户为主的格式来显示程序状况。,图9-4显示了ps命令执行的部分结果。,图9-4使用ps命令显示的结果,上述ps命令显示的数据共分为4个字段,它们的说明如下:PID:进程标识(ProcessID),系统就是凭这个编号来识别及处理此进程的。TTY:Teletypewriter,登录的终端机编号。TIME:此进程所消耗的CPU时间。CMD:正在执行的命令或进程名称。,9.1.2Linux进程调度,1、调度方式2、三种调度策略3、进程调度信息4、Linux进程描述符task_struct结构5、进程调度程序,1.调度方式Linux中的每个进程都分配有一个相对独立的虚拟地址空间。该虚存空间分为两部分:用户空间包含了进程本身的代码和数据;内核空间包含了操作系统的代码和数据。Linux采用“有条件的可剥夺”调度方式。对于普通进程,当其时间片结束时,调度程序挑选出下一个处于TASK_RUNNING状态的进程作为当前进程(自愿调度)。对于实时进程,若其优先级足够高,则会从当前的运行进程中抢占CPU成为新的当前进程(强制调度)。发生强制调度时,若进程在用户空间中运行,就会直接被剥夺CPU;若进程在内核空间中运行,即使迫切需要其放弃CPU,也仍要等到从它系统空间返回的前夕才被剥夺CPU。,2.三种调度策略(1)SCHED_OTHER:这是普通的用户进程,进程的缺省类型,采用该策略时,系统为处于TASK_RUNNING状态的每个进程分配一个时间片。当时间片用完时,进程调度程序再选择下一个优先级相对较高的进程,并授予CPU使用权。(2)SCHED_FIFO:这是一种实时进程,采用该策略时,各实时进程按其进入可运行队列的顺序依次获得CPU。除了因等待某个事件主动放弃CPU,或者出现优先级更高的进程而剥夺其CPU之外,该进程将一直占用CPU运行。(3)SCHED_RR:这也是一种实时进程,采用该策略时,各实时进程按时间片轮流使用CPU。当一个运行进程的时间片用完后,进程调度程序停止其运行并将其置于可运行队列的末尾。,3.进程调度信息调度程序利用这部分信息决定系统中哪个进程最应该运行,并结合进程的状态信息保证系统运转的公平和高效。这一部分信息通常包括进程的类别(普通进程还是实时进程)、进程的优先级等等。,表9-2进程调度信息,4.Linux进程描述符task_struct结构为了管理进程,操作系统必须对每个进程所做的事情进行清楚地描述,为此,操作系统使用数据结构来代表处理不同的实体,这个数据结构就是通常所说的进程描述符或进程控制块,在linux系统中,这就是task_struct结构,在includelinuxsched.h文件中定义。每个进程都会被分配一个task_struct结构,它包含了这个进程的所有信息,在任何时候操作系统都能跟踪这个结构的信息,这个结构是linux内核汇总最重要的数据结构,这个结构的部分源代码及其注释如下。,structtask_structvolatilelongstate;/*state=-1不能运行,state=0运行,state0停止*/unsignedlongflags;/*进程标志*/intsigpending;/*进程上是否有待处理的信号*/mm_segment_taddr_limit;/*进程地址空间,0-0 xBFFFFFFFforuser-thead,0-0 xFFFFFFFFforkernel-thread*/structexec_domain*exec*domain;volatilelongneed_resched;/*调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度*/unsignedlongptrace;intlock_depth;/*锁深度*/longcounter;/*进程可运行的时间片*/,longnice;/*进程的基本时间片*/unsignedlongpolicy;/*进程的调度策略有三种,实时进程:SCHED_FIFO,SCHED_RR;分时进程:SCHED_OTHER*/structmm_struct*mm;/*进程内存管理信息*/intprocessor;/*若进程不在任何CPU上运行,cpus_runnable的值是0,否则是1。*/unsignedlongcpus_runnable,cpus_allowed;structlist_headrun_list;/*指向运行队列的指针*/unsignedlongsleep_time;/*进程的睡眠时间*/structtask_struct*next_task,*prev_task;/*用于将系统中所有的进程连成一个双向循环链表,其根是init_task*/structmm_struct*active_mm;structlist_headlocal_pages;/*指向本地页面*/unsignedintallocation_order,nr_local_pages;/*任务状态*/structlinux_binfmt*binfmt;/*进程所运行的可执行文件的格式*/.;,图9-5task_struct的数据结构,内核函数goodness()用来衡量一个处于可运行状态的进程值得运行的程度,该函数给每个处于可运行状态的进程赋予一个权值(weight),函数主体如下。,Staticinlinegoodness(structtask_struct*pintthis_cpu,structmm_struct*this_mm)intweight;/*权值*/wight=-1;if(p-policy/*返回权值为进程的counter值*/,/*如果当前进程的counter为0,则表示当前进程的时间片已用完,直接返回*/if(!weight)gotoout;#IfdefCONFIG_SMPif(p-processor=this_cpu)weight+=PROC_CHANGE_PENALTY;#Endif/*对进程权值进行微调,如果进程的内存空间使用当前正在运行的进程的内存空间,则权值额外加1*/if(p-mm=this_mm|!p-mm),Weight+=1;/*将权值加上20与进程优先级nice的差。普通进程的权值主要由counter值和nice值组成*/weight+=20-p-nice;gotoout;/*对实时进程进行处理,返回权值为rt_priority+1000,确保优先级高于普通进程*/weight=1000+p-rt_priority;out:returnweight;/*返回权值*/;,5.进程调度程序Linux的进程调度由调度函数schedule()完成,schedule()函数首先扫描任务数组。通过比较每个就绪态(TASK_RUNNING)任务的运行时间递减计数counter的值来确定当前哪个进程运行的时间最少。哪一个的值大,就表示运行时间还不长,于是就选中该进程,并使用任务切换宏函数切换到该进程运行。,9.1.3进程的内存映像,进程的内存映像,指的是内核在内存中如何存放可执行程序文件。这里的可执行程序文件和内存映像是有区别的,其区别体现在以下方面:(1)可执行程序是位于硬盘上的,而内存映像位于内存上。(2)可执行程序没有堆栈,因为只有当程序被加载到内存上的时候才会分配相应的堆栈。(3)可执行程序是静态的,因为它还没运行,但是内存映像是动态的,数据是随着运行过程改变的。,Linux下的内存映像布局一般有如下几个段(从低地址到高地址):代码段:即二进制机器代码,代码段是只读的,可以被多个进程共享。数据段:存储已初始化的变量,包括全局变量和初始化了的静态变量。未初始化数据段:存储未被初始化的静态变量,也就是BSS段。堆:用于存放动态分配的变量。栈:用于函数调用,保存函数返回值,参数等等。,图9-6程序映像的布局,9.2进程控制,9.2.1创建进程9.2.2创建守护进程9.2.3进程退出9.2.4改变进程的优先级9.2.5执行新程序9.2.6等待进程结束,Linux系统中一个进程可以在内核态(kernelmode)或用户态(usermode)下执行,并且分别使用各自独立的内核态堆栈和用户态堆栈。用户堆栈用于进程在用户态下临时保存调用函数的参数、局部变量等数据;内核堆栈则含有内核程序执行函数调用时的信息。,在Linux中主要提供了fork()、vfork()的进程创建方法,介绍如下:1、fork()函数,9.2.1创建进程,#include#includepid_tfork(void);,fork()函数有两个返回值,即调用一次返回两次。成功调用fork()函数后,当前进程实际已经分裂为两个进程,一个是原来的父进程,另一个是刚刚创建的子进程。,【例9-2】创建进程。,设计步骤1在Vim中创建一个新工程文件,命名为“example9_2.c”。2在“example9_2.c”中创建代码如下所示。,#include#include#includemain()inti;if(fork()=0)/*子进程程序*/printf(Thisischildprocessn);else/*父进程程序*/printf(Thisisfatherprocessn);,3用GCC编译并运行结果如图9-7所示。,图9-7用fork()函数创建进程,2、vfork()函数,#include#includepid_tvfork(void);,正确返回:在父进程中返回子进程的进程号,在子进程中返回0;错误返回:-1。,Linux系统启动时往往需要启动很多的系统服务程序,这些系统服务程序往往是运行在后台的,不受用户登录注销的影响,它们一直在运行着,这些服务程序被称为守护进程(daemon)。由于守护进程运行在后台中,不可能向终端输出相关的运行信息,因此,日志系统是守护进程用于记录信息的重要手段。Linux的大多数服务器就是用守护进程的方式实现的。,9.2.2创建守护进程,图9-8用“psaxj”命令查看系统中的进程。参数a表示不仅列当前用户的进程,也列出所有其他用户的进程,参数x表示不仅列有控制终端的进程,也列出所有无控制终端的进程,参数j表示列出与作业控制相关的信息。,图9-8查看系统中进程的结果,要编程实现一个守护进程必须遵守如下的步骤。1调用fork()函数创建子进程后,使父进程立即退出。这样,产生的子进程将变成孤儿进程,同时,所产生的新进程将变为在后台运行。2调用setsid()函数,使得新创建的进程脱离控制终端,同时创建新的进程组,并成为该进程组的首进程。由于守护进程没有控制终端,而使用fork()函数创建的子进程继承了父进程的控制终端、会话和进程组,因此,必须创建新的会话,以脱离父进程的影响。Linux系统提供了setsid()函数用于创建新的会话。,setsid()函数原型如下所示,#includepid_tsetsid(void);,函数调用成功调用进程的会话ID,失败返回-1。,3使用fork()函数产生的子进程将继承父进程的当前工作目录。当进程没有结束时,其工作目录是不能被卸载的。为了防止这种问题发生,守护进程一般会将其工作目录更改到根目录下(/目录)。4关闭文件描述符,并重定向标准输入、输出和错误输出。新产生的进程从父进程继承了某些打开的文件描述符,如果不使用这些文件描述符,则需要关闭它们。守护进程是运行在系统后台的,不应该在终端有任何的输出信息。可以使用dup()函数将标准输入、输出和错误输出重定向到/dev/null设备上。5将文件创建时使用的屏蔽字设置为0,很多情况下,守护进程会创建一些临时文件。出于安全性的考虑,往往不希望这些文件被别的用户查看。可以使用umask(0)将屏蔽字清零。,【例9-3】创建守护进程。,设计步骤1在Vim中创建一个新工程文件,命名为“example9_3.c”和“example9_4.c”。2在“example9_3.c”和“example9_4.c”中创建代码如下所示。,/*example9_3.c代码如下*/#include#include#include#include#include#include#includevoidinit_daemon(void)intpid;inti;,/*处理SIGCHLD信号。处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。*/if(signal(SIGCHLD,SIG_IGN)=SIG_ERR)printf(Cantsignalininit_daemon.n);exit(1);if(pid=fork()exit(0);/是父进程,结束父进程elseif(pid0)exit(1);/fork失败,退出,/是第一子进程,后台继续执行setsid();/第一子进程成为新的会话组长和进程组长/并与控制终端分离if(pid=fork()exit(0);/是第一子进程,结束第一子进程elseif(pid0)printf(forkfail.n);exit(1);/fork失败,退出/是第二子进程,继续/第二子进程不再是会话组长for(i=0;i=0)t=time(0);fprintf(fp,Imhereat%sn,asctime(localtime(,3用GCC编译运行程序结果如图9-9所示。,图9-9例9-3的运行结果,1、正常退出(1)在main()函数中执行return。(2)调用exit()函数。exit()函数原型:voidexit(intstate);(3)调用_exit函数。(4)调用on_exit()函数2、异常退出(1)调用abort()函数。abort()函数原型:voidabort(void);(2)进程收到某个信号,而该信号使程序终止。,9.2.3进程退出,【例9-4】进程退出。,设计步骤1在Vim中创建一个新工程文件,命名为“example9_5.c”。2在“example9_5.c”中创建代码如下所示。,#include#includemain()printf(Iamprocess!Nowexit.n);exit(0);,3用GCC编译运行程序结果如图9-10所示。,图9-10使用_exit()函数退出进程,可以通过设置进程的优先级来保证进程的优先运行。相关的函数有setpriority()、getpriority()和nice()。setpriority()函数的原型为:#include#includeintsetpriority(intwhich,intwho,intprio);,9.2.4改变进程的优先级,setpriority()函数可用来设置进程、进程组和用户的进程执行优先权。参数which有三种数值,参数who则依which值有不同定义,whichwho代表的意义:PRIO_PROCESSwho为进程识别码。PRIO_PGRPwho为进程的组识别码。PRIO_USERwho为用户识别码。,参数prio介于-20至20之间。代表进程执行优先权,数值越低代表有较高的优先次序,执行会较频繁。此优先权默认是0,而只有超级用户(root)允许降低此值。执行成功则返回0,如果有错误发生返回值则为-1,错误原因存于errno。可能产生的错误有:ESRCH:参数which或who可能有错,而找不到符合的进程。EINVAL:参数which值错误。EPERM:权限不够,无法完成设置。EACCES:该调用可能降低进程的优先级。,getpriority()函数原型为:,#include#includeintgetpriority(intwhich,intwho);,该函数返回一组进程的优先级。参数which和问候组合确定返回哪一组进程的优先级。which的可能取值以及who的意义如下。PRIO_PROCESS:一个特定的进程,此时who的取值为进程ID。PRIO_PGRP:一个进程组的所有进程,此时who的取值为进程组ID。PRIO_USER:一个用户拥有的所有进程,此时参数who取值为实际用户组ID。,getpriority()函数如果调用成功返回指定进程的优先级,如果出错将返回-1,并设置errno的值。Errno的可能取值如下:ESRCH:参数which或who可能有错,而找不到符合的进程。EINVAL:参数which值错误。,nice()函数原型为:,#includeintnice(intincrement);,使用fork()或vfork()函数创建子程序后,子程序通常会调用exec()函数来执行另外一个程序。当进程调用exec()函数时,该进程完全由新程序代替,因为并不创建新的进程,所以前后的进程ID并未改变。Linux下exec()函数族有以下6种不同的调用形式,格式如下:#includeintexecve(constchar*path,char*constargv,char*constenvp);intexecv(constchar*path,char*constenvp);intexecle(constchar*path,constchar*arg,.);intexecl(constchar*path,constchar*arg,.);intexevp(constchar*file,Har*constargv);intexeclp(constchar*file,constchar*arg,.);,9.2.5执行新程序,当子进程先于父进程退出时,如果父进程没有调用wait()和waitpid()函数,子进程就会进入僵死状态。如果父进程调用了wait()或waitpid()函数,就不会使子进程变为僵死进程,这两个函数的声明如下:#include#includepid_twait(int*status);pid_twaitpid(pid_tpid,int*status,intoptions);,9.2.6等待进程结束,表9-3检查wait()和waitpid()所返回的终止状态的宏,表9-4waitpid()函数参数pid不同取值的对应状态,【例9-5】进程等待。,设计步骤1在Vim中创建一个新工程文件,命名为“example9_6.c”。2在“example9_6.c”中创建代码如下所示。,#include#include#includeexterninterrno;/引入errno外部变量intmain(intargc,char*argv)pid_tpid_one,pid_wait;intstatus;if(pid_one=fork()=-1)/调用fork函数perror(fork);/如果出错,打印错误信息,if(pid_one=0)printf(mypidis%dn,getpid();sleep(1);exit(EXIT_SUCCESS);/正确退出elsepid_wait=wait(,3用GCC编译运行程序结果如图9-11所示。,图9-11等待进程的运行结果,9.3进程间通信,Linux下进程间通信的几个主要方法如下:(1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。管道是一种半双工的通信方式,数据只能单方向流动。(2)有名管道(namedpipe):也是一种半双工的通信方式。有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。有名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。,(3)消息队列(Messagequeue):消息队列是消息的链接表,包括Posix消息队列systemV消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号传递信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。(4)信号量(semaphore):信号量是一个计数器,主要用于同一进程中各线程之间的信息交互和同步。信号量常常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。,(5)共享内存(sharedmemory):使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及通信。(6)信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal()外,还支持语义符合Posix.1标准的信号函数sigaction()(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction()函数重新实现了signal()函数)。(7)套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和SystemV的变种都支持套接字。,9.3.1管道9.3.2有名管道9.3.3消息队列9.3.4信号量9.3.5共享内存,管道是Linux最早使用的进程通信机制之一,管道只能实现具有亲缘关系的进程(如父进程与子进程)间的通信,而有名管道克服了这一缺点。管道是单向的,数据只能从一端写入,从另一端读取。如果要进行全双工通信,需要建立两个管道。管道还有其他一些不足,如管道没有名字,管道的缓冲区大小是受限制的,管道所传送的是无格式的字节流。要求管道的输入方和输出方事先约定好数据的格式。,9.3.1管道,常见的管道相关函数有如下几种。1.popen()函数原型为:#includeFILE*popen(constchar*command,constchar*type);该函数会调用fork()产生子进程,然后从子进程中调用/bin/sh-c来执行参数command的指令。参数type可使用“r”代表读取,“w”代表写入。依照此type值,popen()会建立管道连到子进程的标准,输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。,2.pipe()函数原型为:#includeintpipe(intfiledes2);该函数会创建管道,并将文件描述词由参数filedes数组返回。filedes0为管道的读取端,filedes1为管道的写入端。如果从管道写端读数据或者向管道写端读数据都将导致出错。函数执行成功返回0,否则返回-1。,3.pclose()函数原型为:#includeintpclose(FILE*stream);该函数用来关闭由popen所建立的管道及文件指针,参数stream为先前由popen所返回的文件指针。如果执行成功则返回子进程的结束状态,否则返回-1。,管道的不足之处是没有名字,只能用于具有亲缘关系的进程间通信,有名管道(namedpipe或FIFO)可以在互不相关的两个进程间实现彼此通信。有名管道提供一个路径名与之关联,有名管道是一个设备文件。有名管道FIFO严格按照先进先出的规则,对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾,不支持lseek等文件定位操作。有名管道的创建在Shell方式下可以使用mkfifo()函数和mknod()函数。创建成功后就可以使用open()、read()、write()这些函数了。,9.3.2有名管道,【例9-5】进程通信。,设计步骤1在Vim中创建一个新工程文件,命名为“pipeS.c”。2在“pipeS.c”中创建代码如下所示。,#include#include#include#include#include#includemain()intpipe_fd2;pid_tpid;intlen=4096*2;charr_buflen;charw_buflen;char*p_wbuf;,intr_num;intw_num;intcmd;memset(r_buf,0,sizeof(r_buf);memset(w_buf,0,sizeof(r_buf);p_wbuf=w_buf;if(pipe(pipe_fd)0)/父进程close(pipe_fd0);/关闭读,父进程写,fd0用于读取管道,fd1用于写入管道。/sleep(5);while(1)/w_num=write(pipe_fd1,w_buf,sizeof(w_buf);w_num=write(pipe_fd1,w_buf,111);if(w_num!=-1)printf(WriterNum=%dn,w_num);elseprintf(nerrorn);sleep(1);/验证读阻塞close(pipe_fd1);/关闭写printf(nParentprocessquit.n);,代码实例创建了父子进程,父进程写管道,子进程读管道。子进程读一次管道就休眠1秒,父进程一次写操作后将阻塞,直到子进程取走数据。父进程的写一次管道后休眠1秒,子进程一次读操作后将阻塞,直到父进程再次写数据。如果管道内的实际数据比请求读的要少,读不阻塞。程序用GCC编译成可执行文件pipieS后,在终端上运行./pipeS,程序结果如图9-12所示。,图9-12进程通信结果,消息队列是一个存放在内核中的消息链表,每个消息队列由消息队列标识符标识。消息队列是存放在内核中的,只有在内核重启(操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正删除。操作消息队列时用到的数据结构主要有msgbuf、msqid_ds和ipc_perm。,9.3.3消息队列,msgbuf的定义如下:,#includestructmsgbuflongmtype;/*typeofmessage*/charmtext1;/*messagetext*/;,在结构中共有两个元素:mtype指消息的类型,它由一个整数来代表,并且,它只能是整数。mtext是消息数据本身。,msqid_ds定义如下:,structmsqid_dsstruct_ipc_permmsg_perm;struct_msg*msg_first;struct_msg*msg_last;_kernel_ttime_tmsg_stime;_kernel_ttime_tmsg_rtime;_kernel_ttime_tmsg_ctime;unsignedlongmsg_lcbytes;unsignedlongmsg_lqbytes;unsignedshortmsg_cbytes;unsignedshortmsg_qnum;unsignedshortmsg_qbytes;_kernel_ipc_pid_tmsg_lspid;_kernel_ipc_pid_tmsg_lrpid;,各字段含义如下:msg_perm:是一个ipc_perm的结构,保存了消息队列的存取权限、队列的用户ID、组ID等信息。msg_first:指向队列中的第一个消息。msg_last:指向队列中的最后一个消息。msg_stime:发送到队列中的最后一条消息的时间。msg_rtime:从队列中读取的最后一条消息的时间。msg_ctime:是队列最后一次改动的时间。msg_cbytes:是队列中所有消息的总长度。msg_qnum:是当前队列中的消息的数目。msg_qbytes:队列中的最大的字节数。msg_lspid:发送最后一条消息的进程ID。msg_lrpid:读取队列中最后一个消息的进程的ID。,ipc_perm定义如下:,structipc_perm_kernel_key_tkey;/*创建消息队列时用到的键值key*/_kernel_uid_tuid;/*消息队列的用户ID*/_kernel_gid_tgid;/*消息队列的组ID*/_kernel_uid_tcuid;/*创建消息队列的进程用户ID*/_kernel_gid_tcgid;/*创建消息队列的进程组ID*/_kernel_mode_tmode;/*/unsigned_shortseq;/*/;,与消息队列处理相关的函数主要有以下几种。1.msgget()msgget()函数原型为:#includeintmsgget(key_tkey,intflags);函数中参数key用来转换成一个标识符,每一个IPC对象与一个key相对应。参数flags用来决定消息队列的存储权限。,2.msgrcv()函数msgrcv()可以从队列中读取消息,函数原型如下:#includessize_tmsgrcv(intmsqid,void*ptr,size_tnbytes,longtype,intflag);函数中参数msqid为指定要读的队列,参数ptr为要接收数据的缓冲区,nbytes为要接收数据的长度,当队列中满足条件的消息长度大于nbytes的值时,则会参照行为参数flag的值决定如何操作:当flag中设置了MSG_NOERROR位时,则将消息截短到nbytes指定的长度后返回。如没有MSG_NOERROR位,则函数出错返回,并设置错误变量errno。,表9-5type值详解,3.msgsnd()由于消息队列的特殊性,系统为这个数据类型提供了两个接口(msgsnd()函数,msgrcv()函数),分别及读消息队列。将一个新的消息写入队列,函数msgsnd()的作用是写消息队列,函数原型如下:#includeintmsgsnd(intmsqid,constvoid*prt,size_tnbytes,intflags);对于写入队列的每一个消息,都含有三个值,正长整型的类型字段、数据长度字段和实际数据字节。新的消息总是放在队列的尾部,函数中参数msqid指定要操作的队列,ptr指针指向一个msgbuf的结构,这是一个模板的消息结构。,4.msgct1()函数msgctl()可以在队列上做多种操作,函数原型如下:#includeintmsgctl(intmsqid,intcmd,structmsqid_ds*buf);参数msqid为指定的要操作的队列,cmd参数指定所要进行的操作,其中有些操作需要buf参数。,表9-6cmd参数详解,信号量的原理是一种数据操作锁的概念,它本身不具备数据交换的功能,而是通过控制其他的通信资源(文件,外部设备等)来实现进程间通信。当请求一个使用信号量来表示的资源时,进程需要先读取信号量的值,以判断相应的资源是否可用。当信号量的值大于0时,表明有资源可以请求。等于0时,说明现在无可用资源,所以进程会进入睡眠状态直至有可用资源时。当进程不再使用一个信号量控制的共享资源时,此信号量的值增1,对信号量的值进行增减操作均为原子操作,这是由于信号量主要的作用是维护资源的互斥或多进程的同步访问。而在信号量的创建以及初始化时,不能保证操作均为原子。,9.3.4信号量,1.信号量的创建同共享内存一样,系统中同样需要为信号量集定制一系列专有的操作函数。系统命令ipcs可查看当前的系统IPC的状态,在命令后使用-s参数。使用函数semget()可以创建或者获得一个信号量集ID,函数原型如下:#includeintsemget(key_tkey,intnsems,intflag);该函数用来取得参数key所关联的信号标识码,每一个IPC对象与一个key相对应。如果参数key为IPC_PRIVATE,则会建立新的信号队列。如果key不为IPC_PRIVATE,也不是已经建立的信号队列IPCkey,系统会视参数flag是否有IPC_CREAT位来决定IPCkey为参数key的信号队列。参数nsems是一个大于等于0的值,用于指明该信号量集中可用资源数(在创建一个信号量时)。当打开一个已存在的信号量集时该参数值为0。函数执行成功,则返回信号量集的标识符(一个大于等于0的整数),失败,则返回1。,函数semop()用以操作一个信号量集,函数原型如下:,#includeintsemop(intsemid,structsembufsemoparray,size_tnops);函数中参数semid是要处理的信号队列识别代码,参数nops标明了参数semoparray所指向数组中的元素个数。参数semoparray是一个structsembuf结构类型的数组指针,结构sembuf来说明所要执行的操作,其定义如下:structsembufunsignedshortsem_num;/*要处理的信号识别码,0代表第一个*/shortsem_op;/*要执行的操作*/shortsem_flg;/*操作标志*/,表9-7sem_op值详解,2.信号量集的操作信号量有自己的专属操作函数semctl(),函数原型如下:#includeintsemctl(intsemid,intsemnum,intcmd,unionsemunarg);函数中参数semid是要处理的信号队列识别码,semnum指定semid的信号集中的某一个信号灯,其类似于在信号量集资源数组中的下标,用来对指定资源进行操作。参数cmd定义函数所要进行的操作。,表9-8cmd值详解,共享内存是最便捷、速度最快的进程通信方式,它将同一块物理内存分别映射到A、B两个进程的逻辑空间。共享内存是存在于内核级别的一种资源,在shell中可以使用ipcs命令来查看当前系统IPC中的状态,在文件系统中/proc目录下有对其描述的相应文件。在系统内核为一个进程分配内存地址时,通过分页机制可以让一个进程的物理地址不连续,同时也可以让一段内存同时分配给不同的进程。由于多个进程共享同一块内存空间,因此需要其他同步进制协同工作。图9-13描述了多进程如何使用共享内存通信。,9.3.5共享内存,图9-13共享内存示意图,对于每一个共享存储段,内核会为其维护一个shmid_ds类型的结构体(shmid_ds结构体定义在头文件中)。,shmid_ds结构体定义如下:,structshmid_dsstructipc_permshm_perm;/size_tshm_segsz;pid_tshm_lpid;pid_tshm_cpid;shmatt_tshm_nattch;time_tshm_atime;time_tshm_dtime;time_tshm_ctime;.;,共享内存的创建函数shmget()可以创建或打开一块共享内存区。函数原型如下:#includeintshmget(key_tkey,size_tsize,intflag);该函数用来取得参数key所关联的共享内存识别代码。size参数为要请求的内存长度(以字节为单位)。,表9-9shmid_ds的初始化,2.共享内存的操作由于共享内存这一特殊的资源类型,使它不同于普通的文件,因此,系统需要为其提供专有的操作函数,而这无疑增加了程序员开发的难度(需要记忆额外的专有函数)。使用函数shmctl()可以对共享内存段进行多种操作,其函数原型如下:#includeintshmctl(intshm_id,intcmd,structshmid_ds*buf);函数中参数sh_mid为所要操作的共享内存段的标识符,structshmid_ds型指针参数buf的作用与参数cmd的值相关,参数cmd指明了所要进行的操作。,表9-10shmctl函数中参数cmd详解,9.4思考与练习,概念题(1)父进程和子进程属性有何异同?(2)多进程中,父子进程的运行顺序是怎样的?(3)fork()函数和vfork()函数在创建进程时有什么区别?(4)进程间通信的方式有哪些,各有什么特点?(5)对管道和有名管道的操作过程有什么不同?操作题(1)编写一个程序完成以下工作:在一个进程里创建两个子进程,两兄弟进程通过管道进行信息传递。(2)编写一个多进程的程序,要求父进程给每个子进程传递不同的参数。,
展开阅读全文
相关资源
相关搜索

当前位置:首页 > 图纸专区 > 课件教案


copyright@ 2023-2025  zhuangpeitu.com 装配图网版权所有   联系电话:18123376007

备案号:ICP2024067431-1 川公网安备51140202000466号


本站为文档C2C交易模式,即用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。装配图网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知装配图网,我们立即给予删除!