进程标识符操作函数
程序员文章站
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,每个进程还有其他一些标识符。下列函数可返回这些标识符(它们都没有出错返回)。
一个现有进程可以调用 fork 函数创建一个新的子进程(某些平台提供了 fork 的几种变体,比如 vfork 以及 Linux 3.2.0 提供的 clone 系统调用,它允许调用者控制哪些部分由父进程和子进程共享)。
fork 函数被调用一次,但返回两次:子进程返回 0,而父进程返回新建子进程的进程 ID。子进程和父进程会继续执行 fork 调用之后的指令。子进程是父进程的副本,例如,子进程获得父进程的数据空间、堆和栈的副本,而不是共享这些存储空间部分,但子进程和父进程共享正文段。不过由于在 fork 之后经常跟随着 exec,所以现在很多实现并不执行一个父进程数据段、堆和栈的完全副本,而是使用了写时复制(Copy-On-Write,COW)技术。这些区域由父进程和子进程共享,而且内核将它们的访问权限改变为只读。如果父进程和子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一页。
下面是一个 fork 函数使用示例,从中可以看到子进程对变量的修改并不影响父进程。
运行结果如下。
一般来说,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 的进程总数超过了限制。
系统中有一些专用进程,但具体细节随实现而不同。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 的进程总数超过了限制。