对于UNIX环境编程,工作中经常会用到相关知识,作为学习UNIX环境编程的经典书籍–UNIX环境高级编程,是每个UNIX编程人员必看的经典书籍之一,为了将相关知识重新进行学习,以系统的整合所学知识,遂以博文形式作为总结。
1、概述
本节介绍UNIX系统的进程控制,包括创建新进程、执行程序和进程终止。还将说明进程属性的各种ID–实际、有效和保存的用户ID和组ID,以及他们如何受到进程控制原语的影响。还包括了解释器文件和system函数。
2、进程标识
每一个进程都有一个非负整形表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用来作其他标识符的一部分以保证其唯一性。例如,应用程序有时就把进程ID作为名字的一部分来创建一个唯一的文件名。
虽然是唯一的,但是进程ID是可以复用的。当一个进程终止后,其进程ID就成为复用的候选者。大多数UNIX系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的的ID。
系统中有一些专用进程。ID为0的通常是调度进程,常常被称为交换进程(swapper)。该进程是内核的一部分,他并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在UNIX的早期版本中是/etc/init,在较新的版本中是/sbin/init。此进程负责在在自举内核后启动一个UNIX系统。init通常读取与系统有关的初始化文件(/etc/rc*文件或者/etc/inittab文件,以及在/etc/init.d中的文件),并将系统引导到一个状态(如多用户)。init进程决不会终止。他是一个普通的用户进程,但是以超级用户特权运行。
每个UNIX系统实现都有他自己的一套提供操作系统服务的内核进程。例如,在某些UNIX的虚拟存储实现中,进程ID 2是页守护进程(page daemon),此进程负责支持虚拟存储器系统的分页操作。
#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
这些函数都没有出错返回。
3、函数fork
一个现有的进程可以调用fork函数创建一个新进程。
#include <unistd.h>
pid_t fork(void);
返回值:子进程返回0,父进程返回子进程ID,若出错,返回-1
由fork创建的新进程被称为子进程(child process)。fork函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值是新建子进程的进程ID。
子进程和父进程继续执行fork调用之后IDE指令。子进程是父进程的副本。例如,子进程获得父进程数据空间、堆和栈的副本。注意,这是子进程所拥有的副本。父进程和子进程并不共享这些存储空间部分。父进程和子进程共享正文段。 由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、站和堆的完全副本。作为替代,使用了写时复制(Copy-On_Write,COW)技术。这些区域由父进程和子进程共享,而且内核将他们的访问权限改变为只读。如果父进程和子进程中任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储系统中的一“页”。
以下程序演示了fork函数,从中看到子进程对变量所做的改变并不影响父进程中该变量的值。
#include <stdio.h>
#include <unistd.h>
int global_var = 6;
char buf[] = "a write to stdout\n";
int main(void)
{
int var;
pid_t pid;
var = 88;
if (write(STDOUT_FILENO, buf, sizeof(buf)-1) != sizeof(buf)-1)
{
printf("write error!\n");
}
printf("before fork\n");
if ((pid = fork()) < 0)
{
printf("fork error!\n");
} else if (pid == 0) {
global_var++;
var++;
printf("ppid = %ld\n", (long)getppid());
} else {
sleep(2);
}
printf("pid = %ld, glob = %d, var = %d\n", (long)getpid(), global_var, var);
return 0;
}
执行此程序得到:
如上两图所示程序中fork与I/O函数之间的交互关系。write函数是不带缓冲的。因为在fork之前调用write,所以其数据写到标准输出一次。但是,标准I/O库是带缓冲的。当以交互方式运行该程序时,只得到该printf输出的行一次,其原因是标准输出缓冲区由换行符冲洗。但是当将标准输出重定向到一个文件时,却得到printf输出行两次。其原因是,==在fork之前调用了printf一次,但当调用fork时,该行数据仍在缓冲区中,然后再将父进程数据空间复制到子进程中时,该缓冲区数据也被复制到子进程中,此时父进程和子进程各有了带行内容的缓冲区。在exit之前的第二个printf将其数据追加到已有的缓冲区中==。当每个进程终止时,其缓冲区中的内容都被写到相应文件中。
freopen函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用freopen清除该定向。此函数一般用于将一个指定文件打开为一个预定义的流:标准输入、标准输出和标准错误
进行重定向:
stdout = freopen("./tmp.out", "w", stdout);
- 文件共享
在重定向父进程的标准输出时,子进程的标准输出也被重定向。实际上,fork的一个特性是父进程的所有打开文件文件描述符都被复制到子进程中。我呢说“复制”是因为对每个文件描述符来说,就好像执行了dup函数。父进程和子进程每个相同的打开文件描述符共共享一个文件表项。
考虑下列情况,一个进程具有3个不同的打开文件,他们是标准输入、标准输出和标准错误。在从fork返回时,有如下所示结构:
重要一点是,父进程和子进程共享同一文件偏移量。考虑下述情况:一个进程fork了一个子进程,然后等待子进程终止。假定,作为普通处理的一部分,父进程和子进程都向标准输出进行写操作。如果父进程的标准输出已经重定向,那么子进程写到该标准输出时,它将更新与父进程共享的该文件的偏移量。当前例子中,当父进程等待子进程时,子进程写到标准输出:而在子进程终止后,父进程也写到标准输出上,并知道其输出会追加在子进程所写数据之后。如果子进程和父进程不共享同一文件偏移量,要实现这种形式的交互就困难的多,可能需要父进程显示的动作。
如果父进程和子进程写同一描述符指向的文件,但又没有任何形式的同步(如父进程等待子进程),那么他们的输出就会相互混合。虽然这种情况是可能发生的,但并不是常用的操作模式。
在fork之后处理文件描述符有以下两种常见的情况。
(1)父进程等待子进程的完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已经做了相应的更新。
(2)父进程和子进程各自执行不同的程序段。在这种情况下,在fork之后,父进程和子进程各自关闭他们不需使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。
父进程和子进程之间的区别如下。
* fork的返回值不同。
* 进程ID不同。
* 这两个进程的父进程ID不同:子进程的父进程ID是创建他的进程的ID,而父进程的父进程ID则不变。
* 子进程的tms_utime、tms_stime和tms_ustime的值设置为0。
* 子进程不继承父进程设置的文件锁。
* 子进程的未处理闹钟被清除。
* 子进程的未处理信号集设置为空集。
使用fork失败的原因主要有两个:(a)系统中已经有了太多的进程,(b)该实际用户ID的进程总数超过了系统限制。
fork有以下两种用法。
(1)一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务进程中是常见的–父进程等待客户端服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求。
(2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
有些操作系统将第二种用法的两个操作组合成一个操作,称为spawn。UNIX系统将这两个操作分开,因为在很多场合需要单独使用fork,其后并不更随exec。另外,将这两个操作分开,使得子进程在fork和exec之间可以更改自己的属性,如I/O重定向,用户ID、信号安排等。
4、函数vfork
vfork函数的调用序列和返回值与fork相同,但两者的语义不同。
vfork函数用于创建一个新进程,而该进程的目的是exec一个新程序。vfork与fork一样都创建一个子进程,但是他并不将父进程的地址空间完全复制到子进程中因为子进程会立即调用exec(或exit),于是也就不会引用该地址空间。不过在子进程调用exec或exit之前,他在父进程的空间中进行。这种优化工作方式在某些UNIX系统的实现中提高了效率,但如果子进程修改数据、进行函数调用、或者没有调用exec或exit就返回都可能带来未知的结果。
vfork和fork之间的另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。当子进程调用这两个函数中的任意一个时,父进程会恢复运行。
以下程序是用vfork代替fork后运行的代码。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int global_var = 6;
int main(void)
{
int var;
pid_t pid;
var = 88;
printf("before vfork\n");
if ((pid = vfork()) < 0) {
printf("vfork error!");
} else if (pid == 0) {
global_var++;
var++;
_exit(0);
}
printf("pid = %ld,glob = %d,var = %d\n", (long)getpid(), global_var, var);
exit(0);
}
执行后结果为:
5、函数exit
进程有5种正常终止以及3种异常终止方式。5种正常终止方式如下:
(1)、在main函数内执行return语句。这等效于调用exit。
(2)、调用exit函数。其包括调用各终止处理程序(终止处理程序在调用atexit函数时登记),然后关闭所有标准I/O流等。因为ISO C并不处理文件描述符、多进程(父进程和子进程)以及作业控制,所以这一定义对于UNIX系统而言是不完整的。
(3)、调用_exit或_Exit函数。ISO C定义_Exit,其目的是为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。对标准I/O是否进行冲洗,这取决于实现。在UNIX系统中,_Exit和_exit是同义的,并不冲洗标准I/O流。_exit函数由exit调用。他处理UNIX系统的特定的细节。
(4)、进程的最后一个线程在其启动例程中执行return语句。但是,该线程的返回值不用做进程的返回值。当最后一个线程从启动例程返回时,该进程以终止状态0返回。
(5)、进程的最后一个线程调用pthread_exit函数。在这种情况下,进程终止状态总是0,这与传递给pthread_exit的参数无关。
3种异常终止具体如下:
(1)调用abort。它产生SIGABR信号;
(2)当进程接收到某些信号时。信号可由进程自身、其它进程或内核产生。例如,若进程引用地址空间之外的存储单元、或者除以0,内核就会为该进程产生相应的信号。
(3)最后一个线程对“取消”请求作出响应。默认情况下,“取消”以延迟方式发生:一个线程要求取消另一个线程,若干时间之后,目标线程终止。
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放他所使用的存储器等。
对于上述任意一种终止情形,我们都希望终止进程能够通知他是如何终止的。对于3个终止函数(exit、_exit和_Exit),实现这一点的方法是,将其退出状态作为参数传递给函数。在异常终止情况,内核产生一个指示其异常终止的原因的终止状态。在任一一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
在说明fork函数时,子进程是在父进程调用fork后生成的。子进程将其终止状态返回给父进程。但是如果父进程在子进程之前终止,该如何?回答是:对于父进程已经终止的所有进程,他们的父进程都改变为init进程。我们称这些进程由init进程收养。其操作过程大概是:在一个进程终止时,内核诸葛检查所有活动进程,以判断他是否是正要终止进程的子进程,如果是,则该进程的父进程的ID更改为1(init进程的ID)。这种处理方法保证了每一个进程都有一个父进程。
如果子进程在父进程之前终止,那么父进程如何在做相应检查时得到子进程的终止状态呢?如果子进程完全消失了,父进程在最终准备好检查子进程是否终止时是无法获得他的终止状态的。内核为每个终止子进程保存了一定量的信息。所以当终止进程的父进程调用wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID、该进程的终止状态以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在UNIX术语中,一切已经终止、但是其父进程尚未对其进行善后处理的进程被称为僵尸进程。==如果一个长期运行的程序,它fork了很多子进程,那么除非父进程取得子进程的终止状态,不然子这些进程终止后就会变成僵尸进程==。
一个由init进程收养的进程最终终止时会发生什么?他会不会变成一个僵尸进程,不会的,因为init被编写成无论何时只要有一个子进程终止,init就会调用一个wait函数取得其终止状态。这样也就防止了在系统中塞满了僵尸进程。当提及“一个init的子进程”时,这指的是init直接产生的进程,也可能是其父进程已经终止,由init收养的过程。
6、函数wait和waitpid
当一个进程正常或者异常终止时,