引言
介绍UNIX系统的进程控制,包括创建新进程、执行程序和进程终止。还将说明进程属性的各种ID—实际、有效和保存的用户ID和组ID,以及它们如何受到进程控制原语的影响。
进程标识
每个进程都有一个非负整形表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性。例如,应用程序有时就把进程ID作为名字的一部分来创建一个唯一的文件名。
虽然是唯一的,但是进程ID是可复用的。当一个进程终止后,其进程ID就成为复用的候选者。大多数UNIX系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID。这防止了将新进程误认为是使用同一ID的某个已终止的先前进程。
- ID为0的进程通常是调度进程,常常被称为交换进程,也被成为系统进程。
- ID为1的进程通常是init进程。init进程绝不会终止,他是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行。
- ID为2的进程是页守护进程,此进程负责支持虚拟存储器系统的分页操作。
进程标志函数
|
|
实际用户Id 和实际组id 是 用户的性质。他是当前登录用户的信息。
有效用户id 和 有效组 id是进程拥有的性质。放该进程访问一些资源时,他们是内核用来测试该进程是否具有权限的依据。通常情况下他们和用户实际id 和实际组id 一样。
函数fork
一个现有的进程可以调用fork函数创建一个新进程。
|
|
由fork创建的新进程被称为子进程。fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新建子进程的进程ID,将子进程ID返回给父进程的理由是:因为一个进程的子进程可以有多个,并且没有一个函数可以获得其所有子进程的进程ID。fork使子进程得到返回值0的理由是:一个进程只会有一个父进程,所以子进程总是可以调用getppid以获得其父进程的进程ID(进程ID 0总是由内核交换进程使用,所以一个子进程的进程ID不可能为0,这样就可以区别哪个是父进程哪个是子进程了)。
当创建了子进程后,子进程和父进程继续执行fork调用之后的命令,包括都执行fork()调用后的返回值。
子进程和父进程继续执行fork调用之后的指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。
如下演示了fork函数,从中可以看到子进程对变量所做的改变并不影响父进程中该变量的值。
|
|
|
|
fork之后,父进程先执行还是子进程先执行是不确定的,则取决于内核所用的调度算法,该程序中,父进程使自己休眠2s,以此使子进程先执行。但不保证2s已经足够。
当写标准输出时,我们将buf长度减去1作为输出字节数,这是为了避免将终止null字节输出。
标准I/O库是带缓冲的,如果标准输出(printf)连到终端设备,则它是行缓冲的;否则它是全缓冲的,当以交互方式运行该程序时,只得到该printf输出的行一次,其原因是标准输出缓冲区由换行符冲洗。但是当将标准输出重定向到一个文件时,却得到printf输出行两次。其原因是,在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中,然后在将父进程数据空间赋值到子进程中时,该缓冲区数据也被复制到子进程中,此时父进程和子进程各自有了带该行内容的缓冲区。在exit之前的第二个printf将其数据追加到已有的缓冲区中。当每个进程终止时,其缓冲区中的内容都被写到相应文件中。
文件共享
fork的一个特性时父进程的所有打开文件描述符都被复制到子进程中。我们说“复制”是因为对每个文件描述符来说,就好像执行了dup函数。父进程和子进程每个相同的打开描述符共享一个文件表项(文件表项含有文件的偏移量),也就是说,父进程和子进程共享同一个文件偏移量。
假设父子进程都向标准输出进行写操作。如果父进程的标准输出已重定向,那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量。当父进程等待子进程时,子进程写到标准输出;而在子进程终止时,父进程也写到标准输出上,并且知道其输出会追加到子进程所写数据之后。如果父进程和子进程不共享同一文件偏移量,要实现这种形式的交互就要困难得多,可能需要父进程显式地动作。
如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步(如使父进程等待子进程),那么它们的输出就会相互混合(假定所用的描述符是在fork之前打开的)。
在fork之后处理文件描述符有以下两种常见情况。
- 父进程等待子进程完成。在这种情况下,父进程无须对其描述符做任何处理。当子进程终止后,它曾进行过读写操作的任一共享描述符的文件偏移量已做了相应更新。
- 父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭它们不需使用的文件描述符,这样就不会干扰到对方使用的文件描述符。这种方法是网络服务进程经常使用的。
除了打开文件之外,父进程的很多其他属性也由子进程继承,包括:
- 实际用户id、实际组id、有效用户id、有效组id
- 附属组id
- 进程组id
- 会话id
- 控制终端
- 设置用户id标志和设置组id标志
- 当前工作目录
- 根目录
- 文件模式创建屏蔽字
- 信号屏蔽和安排
- 对任一打开文件描述符的执行时关闭(close-on-exec)标志
- 环境
- 连接的共享存储段
- 存储映像
- 资源限制
父进程和子进程之间的区别具体如下
- fork的返回值不同。
- 进程ID不同。
- 这两个进程的父进程ID不同。
- 子进程的tms_utime、tms_stime、tms_cutime和tms_ustime的值设置为0。
- 子进程不继承父进程设置的文件锁。
- 子进程的未处理闹钟被清除。
- 子进程的未处理信号集设置为空集。
fork失败的两个主要原因
- 系统中已经有太多的进程。
- 该实际用户ID的进程总数超过了系统限制。
fork有以下两种用法
- 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是最常见的—父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
- 一个进程要执行一个不同的程序。这对shell是常见的情况,在这种情况下,子进程从fork返回后立即调用exec。