操作系统通过进程控制块(PCB)表示进程,每一个进程均有一个PCB,在创建进程时,建立PCB,伴随进程运行的全过程,直到进程撤消而撤消。
通常一个PCB中包含以下几项:
当将CPU转向另一个进程时需要保存当前进程的状态并载入为新进程存储的状态。这个状态被称为上下文转换。一个进程的上下文表示在进程的PCB中:它包括了CPU寄存器值、进程状态和内存管理信息。当上下文转换发生时,内核存储当前进程PCB中的上下文信息并载入被调度运行的新进程存储的上下文。上下文转换时间是纯粹的开销,因为在转换进行时系统不能做任何有用的工作。上下文转换是一个性能瓶颈,如果有可能,程序就会使用新的结构(线程)来避免。
进程在运行期间可以创建多个新进程,被创建的新进程称为子进程。Unix系统中通过fork系统调用创建一个新进程。新进程由原始进程的地址空间的一个拷贝构成。如果父进程终止,它所有的子进程都会将init设为自己的父进程。从而子进程仍旧有一个父进程来维护他们的状态和运行统计数据。
有时被称为轻量级进程(lightweight process LWP),是CPU使用的基本单元;它由线程ID、程序计数器、寄存器集合(被中断时保存当前上下文)和栈组成。它与属于同一进程的其它线程共享其代码段、数据段和其他操作系统资源(如打开文件和信号)。下图可以直观的说明传统的单线程进程和多线程进程之间的差别
(注意:多线程中“堆区”仍是多个线程共享,所以一个线程用new动态创建了新指针变量后,另一个线程也可以使用并销毁,只能被销毁一次)
使用多线程编程线程可以默认共享它们所属进程的内存和资源,进程创建所需要的昂贵的内存和资源的分配,多线程创建由于能够共享所属进程的资源,所以线程创建和上下文切换会更经济。
1.系统调用fork和exec
如果程序中一个线程调用fork,那么新进程会复制所有线程还是新进程只有单个线程?有的unix系统有两种形式的fork,一种复制所有线程,另一种只复制调用了系统调用fork的线程。如果一个线程调用了系统调用exec,那么exec参数所指定的程序会替换整个进程,包括所有的线程。
fork的两种形式的使用与应用程序有关。如果调用fork之后立即调用exec,那么复制所有线程就没有必要。因为exec参数所指定的程序就会替换整个进程。在这种情况下,只复制调用线程比较恰当。不过,如果在fork之后另一个进程并不调用exec,那么另一个进程就应复制所有的线程。
由于子进程通过继承整个地址空间的副本,也从父进程里继承了所有的互斥量、读写锁和条件变量的状态。如果父进程包含多个线程,子进程在fork返回之后,如果不是马上调用exec的话,就需要清理锁状态。
2.信号处理
信号在unix系统中用做通知进程某个特定事件已经发生了,根据来源和被通知信号的时间,信号可以被同步或者异步接受。不管信号是同步或是异步的,所有的信号具有同样的模式:
1. 信号是由特定的事件所发生的
2. 产生的信号要发送到进程。
3.一旦发送,信号必须要加以处理。
同步信号的例子包括非法内存访问或者被零所除。在这种情况下,如果运行程序执行这些动作中的任何一个,就会产生信号。同步信号发送到执行操作而产生信号的同一进程(这就是为什么它们被认为是同步的)。
当一个信号是由运行进程之外的事件所产生,那么进程就异步地接受这一信号。这种信号的例子包括用特殊键比如Ctrl+C或定时器事件到期以终止进程。通常,异步信号被发送到另一个进程。
单线程程序的信号处理比较直接;信号总是发送给进程,不过,对于多线程程序,发送信号就比较复杂,因为进程可能有多个线程。那么信号应该被发送到哪里呢?
通常有如下选择
1. 发送信号到信号所应用的线程
2. 发送信号到进程内的每个线程
3. 发送信号到进程内的某些线程
4.规定一个特定的线程以接受进程的所有信号
发送信号的方法依赖于所产生信号的类型。例如,同步信号需要发送到产生这一信号的线程,而不是进程中的其他线程。但是对于异步信号,有的异步信号,比如终止进程的信号Ctrl-C应该发送到所有线程。有的多线程版UNIX允许线程描述它会接受什么信号和拒绝什么信号。因此,有些异步信号只能发送给那些不拒绝它的献策很难过。不过,因为信号只能被处理一次,所以信号通常被发送到进程中不决绝它的第一个线程。Solaris2按照第四种方法处理:它在每个进程内创建了一个专门处理信号的线程。当异步信号被发送到一个进程时,它被发送到该特定的线程,进而再将信号传递给第一个不拒绝它的线程。
3. 线程池:
传统的服务器收到请求后,就创建一个独立线程处理请求,虽然创建一个独立线程要比创建一个独立进程要好,但是多线程服务器也有一些潜在问题。第一个是:处理请求之前用以创建线程的时间,以及线程在完成工作后会被丢弃。 第二个是:如果允许所有并发请求都通过新线程来处理,那么并没有限制在系统中并发执行的线程的数量。无限制的线程会用尽系统资源。
可以使用线程池解决此问题。
思想:在进程开始时创建一定数量的线程,并放到池中等待工作。当服务器收到请求时,会唤醒池中的一个线程(如果有可用线程),并将要处理的请求传递给它。一旦线程完成了它的服务,它会返回到池中再等待更多的工作。如果池中没有可用线程,那么服务器会一直等待直到有空线程为止。这样有如下好处:
1. 通常用现有线程处理请求要比等待创建新的线程要快
2.线程池限制了在任何时候可存在线程的数量。对那些不能支持大量并发线程的系统尤为重要。
Unix支持在不同进程间共享打开文件。为了说明文件共享,先来说明内核用于所有I/O的数据结构。
内核使用了三种数据结构,他们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
(1) 每个进程在进程表中都有一个记录项,每个记录项中有一张打开文件描述符标,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
(a) 文件描述符标志。
(b) 指向一个文件表项的指针。
(2) 内核为所有打开文件维持一张文件表(注:两个相互独立的进程会各自维护一个文件表,有父子关系的进程共享一个文件表)。每个文件表项包含:
(a) 文件状态标志(读、写、增写、同步、非阻塞等)。
(b) 当前文件位移量。
(c) 指向改文件v节点表项的指针。(注:对于Unix系列的操作系统,大多都有v节点。但是对于linux来说,只有通用的i节点,却没有v节点。i节点存储信息见附录)
(3) 每个代开文件(或设备)都有一个v节点结构。v节点包含了文件类型和对此文件进行各种操作的函数和指针信息。对于大多数文件,v节点还包含了该文件的i节点(索引节点)这些信息是打开文件时从盘上读入内存的,所以所有关于文件的信息都是快速可供使用的。例如,i节点包含了文件的所有者、文件长度、文件所在的设备、指向文件在盘上所使用的实际数据块的指针等等。
我们忽略了某些并不影响我们讨论的视线细节。例如,打开文件描述符表通常在用户区而不在进程中。在SVR4中,此数据结构是一个链接表结构。文件表可以用多种方法实现—不一定是文件表项数组。在4.3+BSD中,v节点包含了实际i节点(见图3-1)。SVR4对于大多数文件系统类型,将v节点存放在i节点中。这些视线细节并不影响我们对文件共享的讨论。
图3-1显示了进程的三张表之间的关系。该进程有两个不同的打开文件—一个文件打开为标准输入(文件表述符0),另一个打开为标准输出(文件描述符1)。
从unix的早期半杯[Thompson1987]以来,这三张表之间的基本关系一直保存至今。这种安排对于在不同进程之间共享文件的方式非常重要。在以后的章节中述及其他的文件共享方式时还会回到这张图上来。
如果两个独立进程各自打开了同一文件,则有图3-2中所示的安排。我们假定第一个进程使该文件在文件描述符3上打开,而另一个进程则使此文件在文件描述符4上打开。打开此文件的每一个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。每个进程都有自己的文件表项的一个理由:这种安排使每个进程都有它自己对该文件的当前位移量。
给出了这些数据结构后,现在对面前所述的操作作进一步说明。
在完成每一个write后,在文件表项中的当前文件位移量即增加所写的字节数。如果这使当前文件位移量超过了当前文件长度,则在i节点表象中的当前文件长度被设置为当前文件位移量(也就是该文件加长了)。
如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有填写标志的文件执行写操作时,在文件表项中的当前文件位移量首先被设置为i节点表项中的文件长度。这就使得每次写的数据都添加到文件的当前尾端处。
lseek值修改文件表项中的当前文件位移量,没有进行任何I/O操作。
若一个文件用lseek被定位到文件当前的尾端,则文件表项中的当前文件位移量被设置为i节点表项中的当前文件长度。
可能有多个文件描述符项指向同一文件表项。在后面讨论dup函数时,我们就能看到这一点。在fork后也发生同样的情况,此时父、子进程对于每一个打开的文件描述符共享同一个文件表项。
注意,文件描述符标志和文件状态标志在作用防卫方面的区别,前者指用于一个进程的一个描述符,而后者则是用于指向该给定文件表项的任何进程中的所有描述符。在后面说明fcntl函数时,我们将会了解如何存取和修改文件描述符标志和文件状态标志。
上述的一切对于多个进程读同一文件都能正确工作。每个进程都有它自己的文件表项,其中也有它自己的文件位移量。但是,当多个进程写同一文件时,则可能产生预期不到的结果。这就涉及到原子操作的概念(谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位,因此这里的原子实际是使用了物理学里的物质微粒的概念)更多内容请参照Unix环境高级编程3.10
由fork产生的进程为子进程。fork的一个特性是父进程所有的打开文件描述符都被复制到子进程中,父子进程的每个相同的打开描述符共享一个文件表项如图:
这种共享的方式使父、子进程对同一个文件使用了同一个文件偏移量。如果父、子进程写到同一个文件描述符,但有没有任何形式的同步,那么它们的输出就会相互混合。在fork之后处理文件描述符有两种常见的情况:
(1)父进程等待子进程完成。在这种情况下,父进程无须对其描述符做任何处理。当子进程终止之后,它曾进行过读、写的人一个共享描述符的文件偏移量已经执行了相应的更新。
(2)父、子进程各自执行不同的程序段。这种情况下,在fork之后,父、子进程各自关闭它们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方式是网络服务进程中常用的方式。
注:文件表项只有在所有引用它的fd(即文件描述符)全部关闭的情况下才会真正关闭。所以如上述情况,如果子进程关闭父、子进程共享的文件描述符后父进程仍可以使用对应的文件表项。