欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

进程标识符操作函数

程序员文章站 2022-05-26 10:25:19
...
    每个进程都有一个非负整型表示的唯一进程 ID。虽说是唯一的,但进程 ID 是可复用的,当一个进程终止时,其进程 ID 就成为复用的候选者。多数 UNIX 系统使用延迟复用算法,使得赋予新建进程的 ID 不同于最近终止进程的 ID,以免将新进程误认为是使用同一 ID 的某个已终止的先前进程。
    系统中有一些专用进程,但具体细节随实现而不同。ID 为 0 的进程通常是调度进程,常常被称为交换进程。该进程是内核的一部分,并不执行任何磁盘上的程序,因此也被称为系统进程。进程 ID 1 通常是 init 进程(在 Mac OS X 10.4 中是 launchd 进程),在自举过程结束时由内核调用,以启动一个 UNIX 系统。该进程的程序文件一般是 /etc/init 或 /sbin/init,它通常读取与系统有关的初始化文件,如 /etc/rc* 文件、/etc/inittab 文件 和 /etc/init.d 中的文件等,并将系统引导到一个状态(如多用户)。init 进程不会终止,它是一个普通的用户进程而非内核中的系统进程,但它是以超级用户特权运行的。此外,每个 UNIX 系统实现都有它自己的一套提供操作系统服务的内核进程,例如,在某些 UNIX 的虚拟存储器实现中,进程 ID 2 是页守护进程,负责支持虚拟存储器系统的分页操作。
    除了进程 ID,每个进程还有其他一些标识符。下列函数可返回这些标识符(它们都没有出错返回)。
#include <unistd.h>
pid_t getpid(void);          /* 返回值:调用进程的进程 ID */
pid_t getppid(void);         /* 返回值:调用进程的父进程 ID */

uid_t getuid(void);          /* 返回值:调用进程的实际用户 ID */
uid_t geteuid(void);         /* 返回值:调用进程的有效用户 ID */
gid_t getgid(void);          /* 返回值:调用进程的实际组 ID */
gid_t getegid(void);         /* 返回值:调用进程的有效组 ID */


    一个现有进程可以调用 fork 函数创建一个新的子进程(某些平台提供了 fork 的几种变体,比如 vfork 以及 Linux 3.2.0 提供的 clone 系统调用,它允许调用者控制哪些部分由父进程和子进程共享)。
#include <unistd.h>
pid_t fork(void);  /* 返回值:子进程返回 0,父进程返回子进程 ID;若出错,返回 -1 */

    fork 函数被调用一次,但返回两次:子进程返回 0,而父进程返回新建子进程的进程 ID。子进程和父进程会继续执行 fork 调用之后的指令。子进程是父进程的副本,例如,子进程获得父进程的数据空间、堆和栈的副本,而不是共享这些存储空间部分,但子进程和父进程共享正文段。不过由于在 fork 之后经常跟随着 exec,所以现在很多实现并不执行一个父进程数据段、堆和栈的完全副本,而是使用了写时复制(Copy-On-Write,COW)技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一页。
    下面是一个 fork 函数使用示例,从中可以看到子进程对变量的修改并不影响父进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int globval = 6;	// external variable in initialized data.
char buf[] = "a write to stdout\n";

int main(void){
	int	var = 88;	// automatic variable on the stack
	pid_t pid;
    // 不写末尾的 null 字节
	if(write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1){
		printf("write error\n");
		exit(2);
	}
	printf("before fork\n");	// we don't flush stdout

	if((pid=fork()) < 0){
		printf("fork error\n");
		exit(2);
	}else if(pid == 0){		// child
		globval++;
		var++;
	}else{				// parent
		sleep(2);
	}

	printf("pid=%ld, glob=%d, var=%d\n", (long)getpid(), globval, var);
	exit(0);
}

    运行结果如下。
$ ./forkDemo.out
a write to stdout
before fork
pid=430, glob=7, var=89           # 子进程的变量值改变了
pid=429, blob=6, var=88
$
$ ./forkDemo.out > temp.out
$ cat temp.out
a write to stdout
before fork                       # 子进程输出一次
pid=432, blob=7, var=89
before fork                       # 父进程输出一次
pid=431, blob=6, var=88
$

    一般来说,fork 之后父进程和子进程的执行先后顺序是不确定的,这取决于内核所使用的调度算法。如果要求父进程和子进程之间相互同步,则要求某种形式的进程间通信。
    本程序中需要注意 fork 与 I/O 函数之间的交互关系。由于 write 函数是不带缓冲的,write 又是在 fork 之前调用,所以其数据写到标准输出一次。但是标准 I/O 库是带缓冲的,如果标准输出连到终端设备,则它是行缓冲的;否则它是全缓冲的。所以当以交互方式运行该程序时,只得到该 printf 输出的行一次,因为标准输出缓冲区由换行符冲洗。而当将标准输出重定向到一个文件时,却得到 printf 输出行两次。这是因为在 fork 之前调用了 printf 一次,但当调用 fork 时,该行仍在缓冲区中,然后在将父进程数据空间复制到子进程中时,该缓冲区数据也被复制到子进程中,此时父进程和子进程各自有了该行内容的缓冲区。在 exit 之前的第二个 printf 将其数据追加到已有的缓冲区中。当每个进程终止时,其缓冲区中的内容都被写到相应文件中。
    另外,还需要注意的是,fork 的一个特性是父进程的所有打开文件描述符都会被复制到子进程中。重要的一点是,父进程和子进程共享同一个文件偏移量(具体可参考文件共享一节)。所以如果上面程序中若没有调用 sleep() 之类的函数来等待子进程退出的话,它们的输出就可能是相互混合的(当然这里调用 sleep 其实也不一定能保证)。
    除了打开文件之外,父进程的其它大部分属性也由子进程继承,比如进程组 ID、实际组 ID、存储映像和资源限制等。
    父进程和子进程的区别主要如下:
    * fork 的返回值不同。
    * 进程 ID 不同。
    * 各自的父进程 ID 不同。
    * 子进程的 tms_utime、tms_stime、tms_cutime 和 tms_ustime 的值设置为 0。
    * 子进程不继承父进程设置的文件锁。
    * 子进程的未处理闹钟被清除。
    * 子进程的未处理信号集设置为空集。
    一般使 fork 失败的两个主要原因是:(a)系统中已经有了太多的进程,(b)该实际用户 ID 的进程总数超过了限制。