main函数是如何被调用的?
命令行参数是如何传送给执行程序的?
典型的存储器布局是什么样式?
如何分配另外的存储空间?
进程如何使用环境变量?
进程终止的不同方式?
main函数的原型是:
int main(int argc, char *argv[ ]) ;
argc:命令行参数的数目。
argv:指向各个参数的指针所构成的数组。
(1) 正常终止:
(a) 从main返回。
(b) 调用exit。
© 调用_exit。
(2) 异常终止:
(a) 调用abort。
(b) 由一个信号终止。
exit和_exit函数用于正常终止一个程序:_exit立即进入内核,exit则先执行一些清除处理(包括调用执行各终止处理程序,关闭所有标准I / O流等),然后进入内核。
#include
void exit(int status) ;
#include
void _exit (int status) ;
按照ANSIC的规定,一个进程可以登记多至32个函数,这些函数将由exit自动调用。我们称这些函数为终止处理程序(exit handler),并用atexit函数来登记这些函数。
e x i t以登记这些函数的相反顺序调用它们。同一函数如若登记多次,则也被调用多次。
注意,内核使程序执行的唯一方法是调用一个e x e c函数。
进程自愿终止的唯一方法是显式/隐式地(调用exit)调用_exit。
当执行一个程序时,调用exec的进程可将命令行参数传递给该新程序。这是UNIX shell的一部分常规操作。
每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null结束的字符串的地址。全局变量environ则包含了该指针数组的地址。
extern char **environ;
正文段:这是由CPU执行的机器指令部分。通常,正文段是可共享的,所以即使是经常执行的程序在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由于意外事故而修改其自身的指令。
初始化数据段
非初始化数据段
栈:自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调用时,其返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。然后,新被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈, C函数可以递归调用。
堆:通常在堆中进行动态存储分配。
共享库使得可执行文件中不再需要包含常用的库函数,而只需在所有进程都可存取的存储区中保存这种库例程的一个副本。程序第一次执行或者第一次调用某个库函数时,用动态连接方法将程序与共享库函数相连接。这样减少了每个可执行文件的长度,但增加了一些运行时间开销。共享库的另一个优点是可以用库函数的新版本代替老版本而无需对使用该库的程序重新连接编辑。
ANSI C说明了三个用于存储空间动态分配的函数。
(1) malloc。分配指定字节数的存储区。此存储区中的初始值不确定。
(2) calloc。为指定长度的对象,分配能容纳其指定个数的存储空间。该空间中的每一位( bit )都初始化为0。
(3) realloc。更改以前分配存储区的长度(增加或减少)。当增加长度时,可能需将以前分配存储区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。
每个进程都有一个非负整型的唯一进程ID。
一个现存进程调用 fork 函数是 UNIX 内核创建一个新进程的唯一方法。
pid_t fork(void);//返回值:子进程中返回0,父进程中返回子进程ID,出错返回-1
该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。
子进程和父进程继续执行 fork 之后的指令。子进程是父进程的复制品。例如,子进程获得父进程数据空间、堆和栈的复制品。注意,这是子进程所拥有的拷贝。父、子进程并不共享这些存储空间部分。
fork的一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程每个相同的打开描述符共享一个文件表项。
在fork之后处理文件描述符有两种常见的情况:
(1) 父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件位移量已做了相应更新。
(2) 父、子进程各自执行不同的程序段。在这种情况下,在 fork 之后,父、子进程各自关闭它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。
(1)一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待委托者的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
(2)一个进程要执行一个不同的程序。
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则我们认为这发生了竞态条件(race condition)。
用fork函数创建子进程后,子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用另一个新程替换了当前进程的正文、数据、堆和栈段。
每个进程除了有一进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合。每个进程组有一个唯一的进程组ID。
每个进程组有一个组长进程。进程组ID等于组长进程ID。
进程组组长可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生命期。某个进程组中的最后一个进程可以终止,也可以参加另一个进程组。
对话期(session)是一个或多个进程组的集合。通常是由shell的管道线将几个进程编成一组的。
信号是软件中断,信号提供了一种处理异步事件的方法。
(1)忽略此信号。
(2)捕捉信号。为了做到这一点要通知内核在某种信号发生时,调用一个用户函数。
(3)执行系统默认动作。注意,对大多数信号的系统默认动作是终止该进程。
void (*signal (int signo , void(*func)(int))) (int);
signal函数的原型说明此函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void)。第一个参数signo是一个整型数,第二个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。用一般语言来描述也就是要向信号处理程序传送一个整型参数,而它却无返回值。signal的返回值则是指向以前的信号处理程序的指针。
当执行一个程序时,所有信号的状态都是系统默认或忽略。
当一个进程调用fork时,其子进程继承父进程的信号处理方式。
非阻塞I/O使我们可以调用不会永远阻塞的I/O操作,例如open,read和write。如果这种操作不能完成,则立即出错返回,表示该操作如继续执行将继续阻塞下去。
当两个人同时编辑一个文件时,其后果将如何呢?在很多UNIX系统中,该文件的最后状态取决于写该文件的最后一个进程。
但是对于有些应用程序,例如数据库,有时进程需要确保它正在单独写一个文件。为了向进程提供这种功能,较新的UNIX系统提供了记录锁机制。
记录锁(recordlocking)的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区。对于UNIX,“记录”这个定语也是误用,因为UNIX内核根本没有使用文件记录这种概念。一个更适合的术语可能是“区域锁”,因为它锁定的只是文件的一个区域(也可能是整个文件)。
I/O多路转接(I/Omultiplexing)。其基本思想是:先构造一张有
关描述符的表,然后调用一个函数,它要到这些描述符中的一个已准备好进行I/O时才返回。在返回时,它告诉进程哪一个描述符已准备好可以进行I/O。
传向select的参数告诉内核:
(1)我们所关心的描述符。
(2)对于每个描述符我们所关心的条件(是否读一个给定的描述符?是否想写一个给定的描述符?是否关心一个描述符的异常条件?)。
(3)希望等待多长时间(可以永远等待,等待一个固定量时间,或完全不等待)。
从select返回时,内核告诉我们:
(1)已准备好的描述符的数量。
(2)哪一个描述符已准备好读、写或异常条件。
poll函数类似于select,但是其调用形式则有所不同。与select不同,poll不是为每个条件构造一个描述符集,而是构造一个pollfd结构数组,每个数组元素指定一个描述符编号以及对其所关心的条件。
使用s e l e c t和p o l l可以实现异步I / O。关于描述符的状态,系统并不主动告诉我们任何信息,我们需要主动地进行查询(调用s e l e c t或p o l l)。
进程之间相互通信的其他技术—IPC(InterProcessCommunication)
管道有两种限制;
(1)它们是半双工的。数据只能在一个方向上流动。
(2)它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
管道是由调用pipe函数而创建的。intpipe(intfiledes[2]);
经由参数filedes返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。
通常,调用pipe的进程接着调用fork,这样就创建了从父进程到子进程或反之的IPC通道。
FIFO有时被称为命名管道。管道只能由相关进程使用,它们共同的祖先进程创建了管道。但是,通过FIFO,不相关的进程也能交换数据。FIFO是一种文件类型,创建FIFO类似于创建文件。
消息队列是消息的链接表,存放在内核中并由消息队列标识符标识。我们将称消息队列为“队列”,其标识符为“队列ID”。
信号量与已经介绍过的IPC机构(管道、FIFO以及消息列队)不同。它是一个计数器,用于多进程对共享数据对象的存取。
为了获得共享资源,进程需要执行下列操作:
(1)测试控制该资源的信号量。
(2)若此信号量的值为正,则进程可以使用该资源。进程将信号量值减1,表示它使用了一个资源单位。
(3)若此信号量的值为0,则进程进入睡眠状态,直至信号量值大于0。若进程被唤醒后,它返回至(第(1)步)。
当进程不再使用由一个信息量控制的共享资源时,该信号量值增1。如果有进程正在睡眠等待此信号量,则唤醒它们。
为了正确地实现信息量,信号量值的测试及减1操作应当是原子操作。为此,信号量通常是在内核中实现的。
允许两个或多个进程共享一给定的存储区。因为数据不需要在客户机和服务器之间复制,所以这是最快的一种IPC。
使用共享存储的唯一窍门是多个进程之间对一给定存储区的同步存取。若服务器将数据放入共享存储区,则在服务器做完这一操作之前,客户机不应当去取这些数据。通常,信号量被用来实现对共享存储存取的同步。