PHP CLI编程基础知识积累(进程、子进程、线程)
本文是从《第三版UNIX 环境高级编程 第3版》 摘录出来的。逐字打出来的,书中讲的示例都是使用C语言,恰好上半年已经学习了C语言,下半年系统的学习了Linux,因此扫平了许多障碍。书到用时方恨少,功在平时。记得曾经有位前辈给我说过,程序员重要的是解决问题的思路,不需要学习那么多的知识,看那么多的书。我觉得也有道理,值得学习。但更重要的是,我们所提到的思路和随机应变都是在自己潜在的知识结构中归纳出来的,如果根本没有接触过,头脑中根本没有这一块理论的知识,当然也就谈不上推陈出新了。
进程控制
每个进程都有一个非负整形表示的唯一进程ID。因为进程ID表示总是唯一的,常将其用作其他标识符的一部分保证其唯一性。例如应用程序有时就把进程ID作为名字的一部分来创建一个唯一的文件名。
虽然是唯一的,但是进程ID是可以复用的。当一个进程终止后,其进程ID就成为复用的候选者。大多数UNIX系统实现了延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID。这防止了将新进程误认为是使用同一ID的某个
已终止的先前进程。
函数fork
由fork创建的新进程称之为子进程
(child process)。fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新建子进程的进程ID。将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数使一个进程可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid 以获得其父进程的进程ID(进程ID 0 总是有内核交换进程使用,所以一个子进程的进程ID不可能为0)。
子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程的数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。
一般来说,在fork之后是父进程先执行还是子进程是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。
父进程和子进程之间的区别具体如下:
1.fork的返回值不同。
2.进程ID不同。
3.这两个进程的父进程ID不同:子进程的父进程ID是创建它的进程ID,而父进程的父进程ID则不变。
4.子进程的tms_utime 、tms_stime、tms_cutime 和 tms_ustime的值设置为0.
5.子进程不继承父进程设置的文件锁。
6.子进程未处理闹钟被清除。
7.子进程的未处理信号集设置为空集。
使fork失败的两个主要原因是:
(a)系统中已经有了太多的进程。
(b)该实际用户ID的进程总数超过了系统限制。系统规定了每个实际用户ID在任一时刻可拥有的最大进程数。
fork的应用场景:
一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
在说明fork函数时,显而易见,子进程是在父进程调用fork后生成的。上面又说了子进程将其终止状态返回给父进程。但是如果父进程在子进程之前终止,则将如何呢?其回答是:对于父进程已经终止的所有进程,它们的父进程都改变为init进程。我们称这些进程由init进程领养。其操作过程大致如下:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则将该进程的父进程ID更改为1(init进程的ID)。这种处理方法保证了每个进程都有一个父进程。
另一个我们关心的情况是如果子进程在父进程之前终止,那么父进程又如何能在做相应检查时得到子进程的终止状态呢?对此问题的回答是:内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态、以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在UNIX术语中,一个已经终止,但是其父进程尚未对其进行善后处理(使用wait获取终止子进程的有关信息,释放它仍占用的资源)的进程称为僵死进程(zombie)。
ps(1)命令将僵死进程的状态打印为Z。如果编写一个长期运行的程序,它调用fork产生了很多子进程,那么除非父进程等待取得子进程的终止状态,否则这些子进程终止后就会变成僵死进程。
最后一个要考虑的问题是:一个由init进程领养的进程终止时会发生什么?它会不会变成一个僵死进程?对此问题的回答是:“否”,因为init被编写成无论何时只要有一个子进程终止,init就会调用wait函数取得其终止状态。这样也就防止了在系统中有很多僵死进程。当提及“一个init的子进程”时,这指的可能是init直接产生的进程,也可能是其父进程已终止,由init领养的进程。
函数wait 和waitpid
当一个进程正常或者异常终止时,内核就向其父进程发送 SIGCHLD
信号。因为子进程终止时一个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数(信号处理程序)。对于这种信号的系统默认动作是忽略他。
调用wait或waitpid 的进程可能会发生什么?
1.如果其所有的子进程都还在运行,则阻塞。
2.如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
3.如果它没有任何子进程,则立即出错返回。
如果进程由于接受到SIGCHID信号而调用wait,我们期望wait会立即返回。但是如果再随机时间点调用wait,则进程可能会阻塞。
这两个函数的区别如下:
1.在一个子进程终止前,wait使其调用者阻塞。而waitpid有一选项,可使调用者不阻塞。
2.waitpid 并不等待在其调用之后的第一个终止进程,它有若干个选项,可以控制它所等待的进程。
如果进程已经终止,并且是僵死进程,则wait立即返回并取得该子进程的状态;否二wait使其调用者阻塞,直到一个子进程终止。如果调用者阻塞并且它有多个子进程,则在其某一个进程终止时,wait就立即返回。因为wait返回终止进程的进程ID,所以它总能了解是哪一个子进程终止了。
信号
信号是软件中断。很多比较重要的应用程序都需要处理信号。信号提供了一种处理异步事件的方法,例如终端用户键入终端键,会通过信号机制停止一个程序,或及早终止管道中的下一个程序。
信号概念
首先每个信号都有一个名字。这些名字都以3个字符SIG开头。例如,SIGABRT是夭折信号,当进程调用abort函数时成这种信号。SIGALRM 是闹钟信号,由alarm函数设置的定时器超时后将产生此信号。
不存在编号为 0 的信号。但kill 函数对信号编号 0 有特殊的应用。
产生信号的条件:
当用户按某些终端键时,引发终端产生的信号。
硬件异常产生信号。
进程调用kill(2)函数可将信号发送给另一个进程或进程组。(自然,对此有所限制:接收信号进程和发送信号进程的所有者必须相同,或者发送信号进程的所有者必须是超级用户。)
用户可用kill(1)命令将信号发送给其他进程。
当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。(这里指的不是硬件产生的条件,而是软件条件。)
信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量(例如errno)来判别是否出现了一个信号,而是必须告诉内核“在此信号出现时,请执行下列操作”。
可以要求内核在某个信号出现时按照下列三种方式之一进行处理,我们称之为信号的处理或者与信号相关的动作。
(1)忽略此信号。大多数信号都可使用这种方法进行处理,但是有两种信号决不能被忽略:SIGKILL和SIGSTOP。这两种信号不能被忽略的原因是:它们向超级用户提供了使进程终止或停止的可靠方法。另外,如果忽略某些由硬件异常产生的信号(例如除以0),则进程的运行行为是未定义的。
(2)捕捉信号。为了做到这一点,要通知内核在某种信号发生时调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。注意,不能捕捉SIGKILL和SIGSTOP信号。
(3)执行系统默认动作。注意,针对大多数信号的系统默认动作是终止进程。
线程
典型的UNIX进程可以看成只有一个控制线程:一个进程在某一时刻只能做一件事情。有了多个控制线程以后,在程序设计时就可以把进程设计成某一时刻能够做不止一件事,每个线程处理各自独立的任务。这种方法有很多好处。
1.通过为每种时间类型分配单独的处理线程,可以简化处理异步时间的代码。每个线程在进程时间处理是可以采用同步编程模式,同步编程模式要比异步编程简单得多。
2.多个进程必须使用操作系统提供的复杂机制才能实现内存和文件描述符的共享,而多个线程自动地可以访问相同的存储地址空间和文件描述符。
3.有些问题可以分解从而提高整个程序的吞吐量。在只有一个控制线程的情况下,一个单线程要完成多个任务,只需要把这些任务串行化。但是有多个控制线程时,相互独立的任务的处理就可以交叉进行,此时只需要为每个任务分配一个单独的线程。但是只有在两个任务的处理过程互不依赖的情况下,两个任务才可以交叉执行。
4.交互的程序同样可以通过使用多线程来改善响应时间,多线程可以把程序中处理用户输入属兔的部分与其他部分分开。
有些人把多线程的程序设计与多处理器或多核系统联系起来。但是即使程序运行在单处理器上,也能够得到多线程编程模型的好处。处理器的数量并不影响程序结构,所以不管处理器的个数多少,程序都可以通过使用线程得以简化。而且,即使多线程程序在串行化任务时不得不阻塞,由于某些线程在阻塞的时候还有另外一些线程可以运行,所以多线程程序在单处理上运行还可以改善响应时间和吞吐量。
每个线程都包含有表示执行环境所必须的信息,其中包括进程中标识线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno变量以及线程私有数据。一个进程的所有信息对于该进程的所有线程都是共享的,包括可执行的代码、程序的全局内存和堆内存、栈以及文件描述符。
线程标示
就像每个进程有一个进程ID一样,每个进程也有一个线程ID。进程ID在这个系统中是唯一的,但线程ID不同,线程ID只有在它所属的进程上下文中才有意义。
应用:
主线程把新的作业放到一个工作队列中,由三个工作线程组成的线程池从队列中移出作业。主线程不允许每个线程任意处理从队列顶端取出的作业,而是由主线程控制作业的分配,主线程会在每个待处理的结构中放置处理该作业的线程ID,每个工作线程只能移出标有自己线程ID的作业。
线程创建
在传统UNIX进程模型中,每个进程只有一个控制线程。从概念上讲,这与基于线程的模型中每个进程只包含一个线程是相同的。在POSIX线程(pthread)的情况下,程序开始运行时,它也是以单进程中的单个控制线程启动的。在创建多个控制线程以前,程序的行为与传动的进程并没有什么区别。线程的线程可以通过pthread_create函数创建。