LINUX环境编程-进程编程.ppt
LINUX应用编程 进程篇,2009年12月22日,2,LINUX应用编程-进程篇,HTTP协议HTML语言CGI编程,3,进程编程 进程与程序,程序是一个包含可执行代码的文件,它放在磁盘等介质上。当程序被操作系统装载到内存并分配给它一定资源后,此时可称为进程。为方便操作系统管理,每个进程都会有一个唯一的非负整数编号。程序是一个静态概念,进程是一个动态概念。,4,进程编程 内存空间,Linux的虚拟地址空间也为04G,Linux将整个4G线性地址空间分为用户空间和内核空间两部分,最高的1G字节(从虚拟地址0 xC0000000到0 xFFFFFFFF),供内核使用,称为“内核空间”。而将较低的3G字节(从虚拟地址0 x00000000到0 xBFFFFFFF),供各个进程使用,称为“用户空间)。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。当进程陷入内核时,内核代表进程运行。,5,进程编程 进程描述,进程描述符:当进程产生时有Linux操作系统分配。内存:用来存放进程要执行的代码和使用的数据。文件描述符:进程运行时打开的文件。认证信息:用户和组ID进程执行环境:各种环境变量资源安排:CPU时间进程状态,6,进程编程 进程状态,用户状态:进程在用户状态下运行的状态。内核状态:进程在内核状态下运行的状态。内存中就绪:进程没有执行,但处于就绪状态,只要内核调度它,就可以执行。内存中睡眠:进程正在睡眠并且进程存储在内存中,没有被交换到SWAP设备。就绪且换出:进程处于就绪状态,但是必须把它换入内存,内核才能再次调度它进行运行。睡眠且换出:进程正在睡眠,且被换出内存。被抢先:进程从内核状态返回用户状态时,内核抢先于它,做了上下文切换,调度了另一个进程。原先这个进程就处于被抢先状态。创建状态:进程刚被创建。该进程存在,但既不是就绪状态,也不是睡眠状态。这个状态是除了进程0以外的所有进程的最初状态。僵死状态(zombie):进程调用exit结束,进程不再存在,但在进程表项中仍有纪录,该纪录可由父进程收集。,7,进程编程 进程状态及转换,图如下:,8,进程调度原理图,9,进程编程 进程的布局,栈用来存放局部变量和函数的返回地址。地址从高到低生长。堆是一块连续的内存,有低地址向高地址生长。需要程序在运行时动态申请和释放。数据段存放了程序运行时的各种数据。代码段存放了可执行指令,一般为只读。,10,进程编程 进程的环境变量,环境变量和命令行参数都放在进程的高地址。环境变量可用 environ来引用。以name=string的形式存放。,#include#include#include extern char*environ;int main(int argc,char*argv)int i=0;for(i=0;environi;i+)printf(%sn,environi);return 0;,11,进程编程 EXIT系统调用,系统调用exit的功能是终止发出调用的进程。它的声明格式如下:#include void exit(int status);#include void _exit(int status);系统调用_exit立即终止发出调用的进程。所有属于该进程的文件描述符都关闭。该进程的所有子进程由进程1(进程init)接收,并对该进程的父进程发 出一个SIGCHLD(子进程僵死)的信号。参数status作为退出的状态值返回父进程,该值可以通过系统调用wait来收集。返回状态码status 只有最低一个字节有效。如果进程是一个控制终端进程,则SIGHUP信号将被送往该控制终端的前台进程。系统调用_exit从不返回任何值给发出调用的进 程;也不刷新I/O缓冲,如果要自动完成刷新,可以用函数调用exit。exit为glibc库函数,它会先运行注册函数,也有可能会进行文件流的关闭操作,之后再调用_exit系统调用。,12,进程编程 RETURN退出进程,return的返回值就是进程终止时的返回状态。如果return没有返回任何值,进程的返回状态是什么?在 gcc加std=c99编译选项下,进程返回状态又是什么?C99:是ISO组织在99年新制定的c标准,使C更可靠。分别用 gcc main.c 和gcc main.c std=c99编译,观察返回值。,13,进程编程 进程终止处理函数,Atexit()函数用于注册一个进程正常终止时要调用的函数,一个进程最多可注册32个终止处理函数。#include int atexit(void(*function)(void);成功返回0,失败返回非0。参数为函数指针,此函数会在进程调用exit时调用。在main函数返回出,分别用return、exit(0)_exit(0),看其中的区别。_exit函数是不会调用注册函数的。void exit_handler(void)printf(Im in exit handlern);int main(int argc,char*argv)if(0!=atexit(exit_handler)printf(register exit function handler failed!n);return-1;printf(before exitn);exit(0);/_exit(0);return 0;,14,进程编程 getenv、setenv函数,getenv用来获取指定环境变量。#include char*getenv(const char*name);参数name为环境变量的名称,如 PATH,SHELL 等。如果环境变量存在那么返回该环境变量的值,否则为NULLsetenv用来设置环境变量,会把value拷贝到环境变量所在的内存区域。#include int setenv(const char*name,const char*value,int overwrite);参数name为环境变量的名称,value为该环境变量的值。如果环境变量已经存在,overwrite非0时,改变该环境变量的值。overwrite为0时,什么都不做直接返回。成功返回0,失败返回-1(一般为放置环境变量的内存空间不够)。环境变量都是以 name=string的形式存放。,15,进程编程 putenv、clearenv函数,putenv 无论环境变量是否存在,都会使设置值生效。#include int putenv(char*string);参数string的格式为 name=string成功返回0,失败返回非0putenv()函数并不拷贝环境变量字符串到进程环境表,只是存放环境变量数值的指针,而setenv()函数则完全拷贝环境变量字符串到进程环境表。分别用 gcc putenv.c 和gcc putenv.c DLOCAL g编译,产看运行结果。把putenv.c中的putenv换成setenv看结果。clearenv清除所有的环境变量,并且把environ设置为NULL。unsetenv清除名字为name的环境变量。#include int clearenv(void);int unsetenv(const char*name);,16,进程编程 跨函数跳转,goto只能在函数中跳转,如果要求从一个函数跳到另外一个函数?用setjmp和longjmp。跨函数的跳转主要用在调用函数层次较深时,为了节约函数的返回时间。如右假设我们从a函数调用到了z函数。,a函数,b函数,r函数,z函数,17,进程编程 setjmp、longjmp函数,setjmp用来设置返回点,保存当前的寄存器值。#include int setjmp(jmp_buf env);void longjmp(jmp_buf env,int val);参数jmp_buf env用来保存当前寄存器值。longjmp会根据env跳转到setjmp处。一个setjmp可以对应n个longjmp,可以用setjmp的返回值来区分。如果成功setjmp返回0,如果是从longjmp返回的,那么返回值有longjmp的第二个参数决定。所以longjmp的第二个参数不可以为0,否则无法判断setjmp是如何返回的。longjmp的第一个参数是setjmp返回的,第二个参数是给setjmp的返回值。当longjmp返回后,setjmp所在函数中的自动变量恢复到调用setjmp时的值。如果变量是保存在内存中的,那么它的值仍然是调用longjmp的时候的值。当你希望变量值在setjmp和longjmp时仍然保持其值,但必须用volatile说明该变量,并且需打开-O优化选项。gcc setjmp.c 和 gcc setjmp.c O观察运行结果。,18,进程编程 getpid、getppid函数,getpid得到自己的进程ID,getppid得到自己父进程ID#include#include pid_t getpid(void);pid_t getppid(void);返回值为进程ID,进程ID一般为一非负整数值。,19,进程编程 fork:进程的创建,我们可以在shell里敲入命令的方式来创建进程,也可以在在程序中通过调用fork系统调用来生成新的进程。新产生的进程我们称他为子进程。init进程的进程ID为1,是一个特殊的用户进程。它会收集孤儿进程(父进程已退出的子进程),并结束它们。fork会产生一个新的进程。#include pid_t fork(void);返回值为-1时,创建子进程失败。返回0时,子进程开始执行。返回 0时,父进程开始执行。父子进程都是从fork之后开始执行。到底是父进程还是子进程先开始执行,看操作系统的调度算法。当你的程序执行到fork语句时:操作系统会复制一个与父进程完全相同的子进程。新进程和原有进程共享代码空间,可执行程序是同一个程序。Linux操作系统会为新进程产生一个ID和进程控制块。当子进程或者父进程不进行写操作时,父子进程共享一分数据,当有写操作时,数据会分离(称为写时复制Copy-On-Write),互不干涉。子进程/父进程对数据所做的任何修改,都不会影响另一方。,20,进程编程 写时复制,1.父子进程均无写操作时:,2.父进程或者子进程有些操作时,子进程或父进程将新产生一份进程的数据拷贝,然后再修改。,21,进程编程 子进程的继承,知道子进程自父进程继承什么或未继承什么将有助于我们。下面这个名单会因为不同Unix的实现而发生变化,所以或许准确性有了水份。请注意子进程得到的是这些东西的 拷贝,不是它们本身。由子进程自父进程继承到:进程的资格(真实(real)/有效(effective)/已保存(saved)用户号(UIDs)和组号(GIDs)环境(environment)堆栈内存打开文件的描述符(注意对应的文件的位置由父子进程共享,这会引起含糊情况)执行时关闭(close-on-exec)标志(close-on-exec标志可通过fcntl()对文件描 述符设置,POSIX.1要求所有目录流都必须在exec函数调用时关闭。更详细说明,参见UNIX环境高级编程 W.R.Stevens,1993,尤晋元等译(以下简称高级编程),3.13节和8.9节)信号(signal)控制设定nice值(nice值由nice函数设定,该值表示进程的优先级,数值越小,优先级越高),22,进程编程 子进程的继承,进程调度类别(scheduler class)(进程调度类别指进程在系统中被调度时所属的类别,不同类别有不同优先级,根据进程调度类别和nice值,进程调度程序可计算出每个进程的全局优先级(Global process prority),优先级高的进程优先执行)进程组号对话期ID(Session ID)(指:进程所属的对话期(session)ID,一个对话期包括一个或多个进程组,更详细说明参见高级编程 9.5节)当前工作目录根目录(译者注:根目录不一定是“/”,它可由chroot函数改变)文件方式创建屏蔽字(file mode creation mask(umask)(指:创建新文件的缺省屏蔽字)资源限制控制终端,23,进程编程-子进程独有的,子进程所独有:进程号:不同的父进程号(译者注:即子进程的父进程号与父进程的父进程号不同,父进程号可由getppid函数得到)自己的文件描述符和目录流的拷贝(译者注:目录流由opendir函数创建,因其为顺序读取,顾称“目录流”)子进程不继承父进程的进程,正文(text),数据和其它锁定内存(memory locks)(锁定内存指被锁定的虚拟内存页,锁定后,不允许内核将其在必要时换出(page out),详细说明参见The GNU C Library Reference Manual 2.2版,节)在tms结构中的系统时间(tms结构可由times函数获得,它保存四个数据用于记录进程使用中央处理器(CPU:Central Processing Unit)的时间,包括:用户时间,系统时间,用户各子进程合计时间,系统各子进程合计时间)资源使用(resource utilizations)设定为0阻塞信号集初始化为空集不继承由timer_create函数创建的计时器不继承异步输入和输出,24,进程编程 继承的测试题,glibc库的I/O有三个类型的缓冲无缓冲:不管你写入的数据有多少,立即写往内核。行缓冲:当遇到换行符时,才写往内核。全缓冲:需要写到一定字节的数据,才写往内核。在fork_bufio.c中,在fork之前的printf的格式化字符串中加上换行符或者去掉换行符,看输出有什么不同。,25,进程编程 vfork,vfork和fork一样都是创建一个子进程,但子进程不会从父进程复制任何东西。子进程完全和父进程共享数据和堆栈,子进程对数据的修改不会触发写时复制,也就是说子进程所作的修改会出现在父进程中。#include#include pid_t vfork(void);返回值为-1时,创建子进程失败。返回0时,子进程开始执行。返回 0时,父进程开始执行。Vfork与fork的区别vfork产生的子进程必须以exit或者exec返回,否则会出现未定义错误。vfork一定保证子进程先运行,在子进程调用exit或者exec之前父进程是不可能运行的。vfork的使用场合是子进程不会用到父进程任何资源的情况下。vfork只产生一个进程控制块,然后再通过exec产生子进程所需要的资源和代码。在vfork.c中,当vfork的子进程用return返回时,看有什么结果?,26,进程编程 wait和waitpid,父子进程谁先终止时未定义的,如果父进程先于子进程终止。那么子进程会被init进程“领养”,子进程的父进程id变为1。如果父进程不去查询子进程的状态,那么子进程一直会处于“僵尸”状态。这也是init进程存在的原因之一。父进程可以通过wait和waitpid函数来等待子进程终止并查询子进程的状态。wait和waitpid可查询子进程的结束状态,如果子进程还处于运行状态,那么父进程则会阻塞。#include#include pid_t wait(int*status);pid_t waitpid(pid_t pid,int*status,int options);wait函数只要任意一个子进程结束,就会返回该子进程ID,如果出错则返回-1。status为子进程返回的状态,需要通过宏WIFEXITED和WEXITSTATUS最终获得返回状态。waitpid可以指定某个进程id或者进程组id,参数pid说明如下:pid=-1 等待任意一个进程结束,此时与wait等效。pid0 等待与pid相符的子进程结束。pid=0 等待组ID等于调用者进程组ID的任意进程结束。pid-1 等待组ID等于|pid|的任意进程结束。,27,进程编程 wait和waitpid,对于调用waitpid中的参数options的取值及其含义如下:WNOHANG:该选项要求如果没有子进程退出就立即返回。WUNTRACED:对已经停止但未报告状态的子进程,该调用也从等待中返回和报告状态。如果status不是空,调用将使status指向该信息。下面的宏可以用来检查子进程的返回状态。前面三个用来判断退出的原因,后面三个是对应不同的原因返回状态值:WIFEXITED(status):如果进程通过系统调用_exit或函数调用exit正常退出,该宏的值为真。WIFSIGNALED(status):如果子进程由于得到的信号(signal)没有被捕捉而导致退出时,该宏的值为真。WIFSTOPPED(status):如果子进程没有终止,但停止了并可以重新执行时,该宏返回真。这种情况仅出现在waitpid调用中使用了WUNTRACED选项。WEXITSTATUS(status):如果WIFEXITED(status)返回真,该宏返回由子进程调用_exit(status)或exit(status)时设置的调用参数status值。WTERMSIG(status):如果WIFSIGNALED(status)返回为真,该宏返回导致子进程退出的信号(signal)的值。WSTOPSIG(status):如果WIFSTOPPED(status)返回真,该宏返回导致子进程停止的信号(signal)值。,28,进程编程 wait和waitpid,该调用返回退出的子进程的PID;或者发生错误时返回-1;或者设置了WNOHANG选项没有子进程退出就返回0;发生错误时,可能设置的错误代码如下:ECHILD:该调用指定的子进程pid不存在,或者不是发出调用进程的子进程。EINVAL:参数options无效。ERESTARTSYS:WNOHANG没有设置并且捕获到SIGCHLD或其它未屏蔽信号。关于wait调用的例子,前面在介绍fork调用时,就有了简单的应用。此处不再举例。注意:子进程退出(SIGCHLD)信号设置不同的处理方法,会导致该调用不同的行为,详细情况见Linux信号处理机制。,29,进程编程 exec函数,系统调用exec是用来执行一个可执行文件来代替当前进程的执行映像。需要注意的是,该调用并没有生成新的进程,而是在原有进程的基础上,替换原有进程的 正文(即exec用一个全新的程序替换当前进程的正文,数据,堆和栈),调用前后是同一个进程,进程号PID不变。但执行的程序变了(执行的指令序列改变了)。它有六种调用的形式,随着系统的不同并不完全与以下介绍的相 同。它们的声明格式如下:#include int execl(const char*path,const char*arg,.);int execlp(const char*file,const char*arg,.);int execle(const char*path,const char*arg,.,char*const envp);int execv(const char*path,char*const argv);int execve(const char*filename,char*const argv,char*const envp);int execvp(const char*file,char*const argv);,30,进程编程 EXEC的函数关系,31,进程编程 execl和execv,execl和execv,这两个函数的区别在于程序的命令行参数如何传递。l代表list,意味着execl的每个命令行参数都是单独传入。v代表vector,所有命令行参数打包成 char*argvp的方式传给execv。其实execl所做的是把单独的命令行参数打包后传给execv。execlp和execvp,p代表path,也就是说你只要在第一个参数中指明可执行文件的名字,系统便会从PATH指定的路径中寻找那个可执行文件并执行。不设置environ,观察程序运行区别。execle和execve,函数的最后一个参数为环境变量。例:execv.c execvp.c execve.c,32,进程编程 system函数,system执行外部一个命令或者程序,相当于fork,exec,waitpid。#include int system(const char*command);返回-1表示失败,成功返回命令的返回状态。如果command中没有路径符/,则从PATH环境变量指定的目录中寻找该命令。,33,作业,1.下面的程序一共会创建多少个进程?int main(int argc,char*argv)int i;for(i=0;i3;i+)if(fork()=0)printf(“the child%dn”,i);else printf(“the parent%dn”,i);2.用fork、exec、waitpid实现system函数功能。3.编写一段程序,创建一个“僵尸”进程,然后调用system执行ps命令验证该进程是“僵尸”进程。验证完后父进程用wait或者waitpid对该“僵尸”进程“收尸”。,34,作业,4.pp.c文件内容如下pp.c int main()printf(test);则下面的程序执行的输出结果是什么?extern char*environ;int main(int argc,char*argv)printf(close-on-exec is%d,fcntl(1,F_GETFD);fcntl(1,F_SETFD,16);printf(close-on-exec is%d,fcntl(1,F_GETFD);execve(pp,argv,environ);printf(AH!);,35,作业,5.写一个程序,创建10个进程,每个进程分别打印出自己的进程号及父进程号6.写三个程序,分别执行如下功能:程序一:打印“I am process 1”,然后睡眠3秒,退出程序二:打印“I am process 2”,然后睡眠3秒,退出程序三:程序执行起来后创建两个子进程,此两个子进程分别使用exec运行程序一和程序二,当主进程检测到任何一个子进程退出时,打印出退出的子进程,并重新启动相应的子进程。,