Duangw

进程

索引:

  1. exit和_exit
  2. atexit
  3. 环境变量
  4. C存储布局
  5. 动态内存分配
  6. 非局部跳转setjmp和longjmp
  7. getrlimit和setrlimit
  8. 进程标识
  9. fork函数
  10. vfork函数
  11. 进程终止
  12. wait和waitpid函数
  13. wait3和wait4函数
  14. exec函数
  15. setuid和setgid
  16. setreuid和setregid
  17. seteuid和setegid
  18. system函数
  19. getlogin函数
  20. 进程执行时间
  21. 进程组
  22. 对话期
  23. 控制终端
  24. 前台进程组
  25. 后台进程与终端
  26. 孤儿进程组

1.exit和_exit

exit和_exit函数用于正常终止一个程序:_exit立即进入内核,exit则先执行一些清除处理(包括调用执行各终止处理程序,关闭所有标准I/O流等),然后进入内核。

#include <stdlib.h>
void exit(int status);

#include <unistd.h>
void _exit (int status);

exit和_exit都带一个整型参数,称之为终止状态(exit status)。

大多数UNIX shell都提供检查一个进程终止状态的方法。如果(a)若调用这些函数时不带终止状态,或(b)main执行了一个无返回值的return语句,或(c)main执行隐式返回,则该进程的终止状态是未定义的。

 

2.atexit

#include <stdlib.h>
int atexit(void (*func)(void));
返回:若成功则为0,若出错则为非0

按照ANSI C的规定,一个进程可以登记多至32个函数,这些函数将由exit自动调用。我们称这些函数为终止处理程序(exit handler),并用atexit函数来登记这些函数。

其中,atexit的参数是一个函数地址,当调用此函数时无需向它传送任何参数,也不期望它返回一个值。exit以登记这些函数的相反顺序调用它们。同一函数如若登记多次,则也被调用多次。

根据ANSI C和POSIX.1,exit首先调用各终止处理程序,然后按需多次调用fclose,关闭所有打开流。

 

3.环境变量

每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null结束的字符串的地址。全局变量environ则包含了该指针数组的地址:

extern char **environ;

ANSI C定义了一个函数getenv,可以用其取环境变量值,但是该标准又称环境的内容是由实现定义的:

#include <stdlib.h>
char *getenv(const char *name);
返回:指向与name关联的value的指针,若未找到则为NULL

注意,此函数返回一个指针,它指向name = value字符串中的value。

除了取环境变量值,有时也需要设置环境变量,或者是改变现有变量的值,或者是增加新的环境变量。我们能影响的是当前进程及其后生成的子进程的环境,但不能影响父进程的环境。

#include <stdlib.h>
int putenv(const char *str);
int setenv(const char *name, const char *value, int rewrite);
void unsetenv(const char *name);

putenv:取形式为name = value的字符串,将其放到环境表中。如果name已经存在,则先删除其原来的定义。

setenv将name设置为value。如果在环境中name已经存在,那么若rewrite非0,则首先删除其现存的定义;若rewrite为0,则不删除其现存定义(name不设置为新的value,而且也不出错)。

unsetenv删除name的定义。即使不存在这种定义也不算出错。

 

4.C存储布局

由于历史原因,C程序一直由下列几部分组成:

size命令报告正文段、数据段和bss段的长度(单位:字节)。

 

5.动态内存分配

#include <stdlib.h>
void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
三个函数返回:若成功则为非空指针,若出错则为NULL

void free(void *ptr);

malloc分配指定字节数的存储区。此存储区中的初始值不确定。

calloc为指定长度的对象,分配能容纳其指定个数的存储空间。该空间中的每一位(bit)都初始化为0。

realloc更改以前分配区的长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定。

这三个分配函数所返回的指针一定是适当对齐的,使其可用于任何数据对象。

函数free释放ptr指向的存储空间。被释放的空间通常被送入可用存储区池,以后可在调用分配函数时再分配。

注意,realloc的最后一个参数是存储区的newsize(新长度),不是新、旧长度之差。作为一个特例,若ptr是一个空指针,则realloc的功能与malloc相同,用于分配一个指定长度newsize的存储区。

 

6.非局部跳转setjmp和longjmp

#include <setjmp.h>
int setjmp(jmp_buf env);
返回:若直接调用则为0,若从longjmp返回则为非0

void longjmp(jmp_buf env, int val);

在希望返回到的位置调用setjmp,setjmp的参数env是一个特殊类型jmp_buf。这一数据类型是某种形式的数组,其中存放在调用longjmp时能用来恢复栈状态的所有信息。一般,env变量是个全局变量,因为需从另一个函数中引用它。

在要跳转的地方以两个参数调用longjmp函数。第一个就是在调用setjmp时所用的env;第二个val,是个非0值,它成为从setjmp处返回的值。

大多数实现并不滚回自动变量和寄存器变量的值,而所有标准则说它们的值是不确定的。如果你有一个自动变量,而又不想使其值滚回,则可定义其为具有volatile属性,即易失变量。

说明为全局和静态变量的值在执行longjmp时保持不变。

如果要编写一个使用非局部跳转的可移植程序,则必须使用volatile属性。

 

7.getrlimit和setrlimit

#include <sys/time.h>
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit *rlptr );
两个函数返回:若成功则为0,若出错则为非0

每个进程都有一组资源限制,其中某一些可以用getrlimit和setrlimit函数查询和更改。对这两个函数的每一次调用都指定一个资源以及一个指向下列结构的指针:

struct rlimit {
    rlim_t rlim_cur; /*soft limit: current limit*/
    rlim_t rlim_max; /*hard limit: maximum value for rlim_cur*/
};

在更改资源限制时,须遵循下列三条规则:

一个无限量的限制由常数RLIM_INFINITY指定。

这两个函数的resource参数取下列值之一,注意并非所有资源限制都受到SVR4和4.3+BSD的支持:

资源限制影响到调用进程并由其子进程继承。这就意味着为了影响一个用户的所有后续进程,需将资源限制设置构造在shell之中。确实,Bourne shell和Korn Shell具有内部ulimit命令,C shell具有内部limit命令。

 

8.进程标识

#include <sys/types.h>
#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

注意,这些函数都没有出错返回。

 

9.fork函数

#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
返回:子进程中为0,父进程中为子进程ID,出错为-1

一个现存进程调用fork函数是UNIX内核创建一个新进程的唯一方法(除交换进程、init进程和页精灵进程),由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。

子进程和父进程继续执行fork之后的指令。子进程是父进程的复制品。例如,子进程获得父进程数据空间、堆和栈的复制品。注意,这是子进程所拥有的拷贝。父、子进程并不共享这些存储空间部分。如果正文段是只读的,则父、子进程共享正文段。现在很多的实现使用了在写时复制(Copy-On-Write, COW)技术。

一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。如果要求父、子进程之间相互同步,则要求某种形式的进程间通信。

fork的一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程每个相同的打开描述符共享一个文件表项。

除了打开文件之外,很多父进程的其他性质也由子进程继承:

父、子进程之间的区别是:

使fork失败的两个主要原因是:(a)系统中已经有了太多的进程(通常意味着某个方面出了问题),或者(b)该实际用户ID的进程总数超过了系统限制。

 

10.vfork函数

vfork 函数的调用序列和返回值与fork相同,但两者的语义不同:

 

11.进程终止

进程有三种正常终止法及两种异常终止法。

正常终止:

  1. 在main函数内执行return语句,这等效于调用exit。
  2. 调用exit函数。
  3. 调用_exit系统调用函数。

异常终止:

  1. 调用abort。它产生SIGABRT信号,所以是下一种异常终止的一种特例。
  2. 当进程接收到某个信号时。

对上述任意一种终止情形,终止进程都能够通知其父进程它是如何终止的:

在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。如果子进程正常终止,则父进程可以获得子进程的退出状态。

 

12.wait和waitpid函数

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
两个函数返回:若成功则为进程ID,若出错则为-1

当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。父进程可以忽略该信号,或者提供一个该信号发生时即被调用执行的函数,对于这种信号的系统默认动作是忽略它。

调用wait或waitpid的进程可能会:

如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。但是如果在一个任一时刻调用wait,则进程可能会阻塞。

这两个函数的区别是:

这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。

依据传统,这两个函数返回的整型状态字是由实现定义的。POSIX.1规定终止状态用定义在<sys/wait.h>中的各个宏来查看。有三个互斥的宏可用来取得进程终止的原因,如下:

不幸的是,没有一种可移植的方法将WTERMSIG得到的信号编号映射为说明性的名字。我们必须查看<signal.h>头文件才能知道具体的信号名字。

waitpid函数的参数说明:

对于waitpid的pid参数的解释与其值有关:

waitpid返回终止子进程的进程ID,而该子进程的终止状态则通过statloc返回。对于wait,其唯一的出错是调用进程没有子进程(函数调用被一个信号中断时,也可能返回另一种出错)。但是对于waitpid,如果指定的进程或进程组不存在,或者调用进程没有子进程都能出错。

options参数使我们能进一步控制waitpid的操作。此参数或者是0,或者是下列常数的逐位或运算:

 

13.wait3和wait4函数

4.3+BSD提供了两个附加函数wait3和wait4。这两个函数提供的功能比POSIX.1函数wait和waitpid所提供的分别要多一个,这与附加参数rusage有关。该参数要求内核返回由终止进程及其所有子进程使用的资源摘要。

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options,
struct rusage *rusage);
两个函数返回:若成功则为进程ID,若出错则为-1

资源信息包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。这些资源信息只包括终止子进程,并不包括处于停止状态的子进程。

 

14.exec函数

当进程调用一种exec函数时,该进程完全由新程序代换,新程序从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变,exec只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。

有六种不同的exec函数可供使用,它们常常被统称为exec函数:

#include <unistd.h>
int execl(const char *pathname, const char *arg0,
.../* (char *)0 */);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char * rg0,
.../* (char *)0, char *const envp[] */);
int execve(const char *pathname, char *const argv[],
char *const envp[]);
int execlp(const char *filename, const char * arg0,
.../* (char *)0 */);
int execvp(const char *filename, char *const argv[]);
六个函数返回:若出错则为-1,若成功则不返回

这些函数之间的区别是:

  1. 前四个取路径名作为参数,后两个则取文件名作为参数。当指定filename作为参数时,如果filename中包含/,则就将其视为路径名;否则就按PATH环境变量,在有关目录中搜寻可执行文件。如果execlp和execvp中的任意一个使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑程序产生的机器可执行代码文件,则就认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入。
  2. 第二个区别与参数表的传递有关(l表示表(list),v表示矢量(vector))。函数execl、execlp和execle要求将新程序的每个命令行参数都说明为一个单独的参数,这种参数表以空指针结尾。对于另外三个函数(execv、execvp和execve),则应先构造一个指向各参数的指针数组,然后将该数组地址作为这三个函数的参数。
  3. 最后一个区别与向新程序传递环境表相关。以e结尾的两个函数(execle和execve)可以传递一个指向环境字符串指针数组的指针。其他四个函数则使用调用进程中的environ变量为新程序复制现存的环境。

每个系统对参数表和环境表的总长度都有一个限制,这种限制是ARG_MAX。在POSIX.1系统中,此值至少是4096字节。

在执行exec后,除进程ID没有改变之外,执行新程序的进程还保持了原进程的下列特征:

对打开文件的处理与每个描述符的exec关闭标志值有关。进程中每个打开描述符都有一个exec关闭标志。若此标志设置,则在执行exec时关闭该描述符,否则该描述符仍打开。除非特地用fcntl设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开。

POSIX.1明确要求在exec时关闭打开目录流。

注意,在exec前后实际用户ID和实际组ID保持不变,而有效ID是否改变则取决于所执行程序的文件的设置-用户-ID位和设置-组-ID位是否设置。如果新程序的设置-用户-ID位已设置,则有效用户ID变成程序文件所有者的ID,否则有效用户ID不变。对组ID的处理方式与此相同。

 

15.setuid和setgid

#include <sys/types.h>
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
两个函数返回:若成功则为0,若出错则为-1

setuid函数设置实际用户ID和有效用户ID。与此类似,setgid函数设置实际组ID和有效组ID。规则如下:

  1. 若进程具有超级用户特权,则setuid函数将实际用户ID、有效用户ID,以及保存的设置-用户-ID设置为uid。
  2. 若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置-用户-ID,则setuid只将有效用户ID设置为uid。不改变实际用户ID和保存的设置-用户-ID。
  3. 如果上面两个条件都不满足,则errno设置为EPERM,并返回出错。

在这里假定_POSIX_SAVED_IDS为真。如果没有提供这种功能,则上面所说的关于保存的设置-用户-ID部分都无效。

注意下列几点:

 

16.setreuid和setregid

#include <sys/types.h>
#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
两个函数返回:若成功则为0,若出错则为-1

4.3+BSD支持setreuid函数,其功能是交换实际用户ID和有效用户ID的值。

 

17.seteuid和setegid

#include <sys/types.h>
#include <unistd.h>
int seteuid(uid_t uid);
int setegid(gid_t gid);
两个函数返回:若成功则为0,若出错则为-1

它们只更改有效用户ID和有效组ID。

一个非特权用户可将其有效用户ID设置为其实际用户ID或其保存的设置-用户-ID。对于一个特权用户则可将有效用户ID设置为uid。(这区别于setuid函数,它更改三个用户ID。)这一建议更改也要求支持保存的设置-用户-ID。

 

18.system函数

#include <stdlib.h>
int system(const char *cmdstring);

如果cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0值,这一特征可以决定在一个给定的操作系统上是否支持system函数。在UNIX中,system总是可用的。

因为system在其实现中调用了fork、exec和waitpid,因此有三种返回值:

  1. 如果fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,而且errno中设置了错误类型。
  2. 如果exec失败(表示不能执行shell),则其返回值如同shell执行了exit(127)一样。
  3. 否则所有三个函数(fork、exec和waitpid)都成功,并且system的返回值是shell的终止状态,其格式已在waitpid中说明。

 

19.getlogin函数

用getlogin函数可以获得执行程序的用户的登录名:

#include <unistd.h>
char *getlogin(void);
返回:若成功则为指向登录名字符串的指针,若出错则为NULL

如果调用此函数的进程没有连接到用户登录时所用的终端,则本函数会失败。通常称这些进程为精灵进程(daemon)。

 

20.进程执行时间

任一进程都可调用times函数以获得它自己及终止子进程的墙上时钟时间、用户CPU时间和系统CPU时间:

#include <sys/times.h>
clock_t times(struct tms *buf);
返回:若成功则为经过的墙上时钟时间(单位:滴答),若出错则为-1

此函数填写由buf指向的tms结构,该结构定义如下:

struct tms {
    clock_t tms_utime; /* user time */
    clock_t tms_stime; /* system time */
    clock_t tms_cutime; /* user time, children */
    clock_t tms_cstime; /* system time, children */
};

注意,此结构没有包含墙上时钟时间。作为代替,times函数返回墙上时钟时间作为函数值。此值是相对于过去的某一时刻度量的,所以不能用其绝对值而必须使用其相对值。

结构中两个针对子进程的字段包含了此进程已等待到的各子进程的值。

所有由此函数返回的clock_t值都用_SC_CLK_TCK(由sysconf函数返回的每秒时钟滴答数)变换成秒数。

 

21.进程组

每个进程除了有一进程ID之外,还属于一个进程组,进程组是一个或多个进程的集合。每个进程组有一个唯一的进程组ID。进程组ID类似于进程ID,它是一个正整数,并可存放在pid_t数据类型中。

函数getpgrp返回调用进程的进程组ID:

#include <sys/types.h>
#include <unistd.h>
pid_t getpgrp(void);
返回:调用进程的进程组ID

每个进程组有一个组长进程。组长进程的标识是,其进程组ID等于其进程ID。

进程组组长可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生命期。某个进程组中的最后一个进程可以终止,也可以参加另一个进程组。

进程调用setpgid可以参加一个现存的组或者创建一个新进程组:

#include <sys/types.h>
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
返回:若成功则为0,出错为-1

这将pid进程的进程组ID设置为pgid。如果这两个参数相等,则由pid指定的进程变成进程组组长。

一个进程只能为它自己或它的子进程设置进程组ID。在它的子进程调用了exec后,它就不再能改变该子进程的进程组ID。

如果pid是0,则使用调用者的进程ID。另外,如果pgid是0,则由pid指定的进程ID被用作为进程组ID。

如果系统不支持作业控制,则不定义_POSIX_JOB_CONTROL,在这种情况下,此函数返回出错,errno设置为ENOSYS。

 

22.对话期

对话期(session)是一个或多个进程组的集合。进程调用setsid函数就可建立一个新对话期:

#include <sys/types.h>
#include <unistd.h>
pid_t setsid(void);
返回:若成功则为进程组ID,若出错则为-1

如果调用此函数的进程不是一个进程组的组长,则此函数创建一个新对话期,结果为:

  1. 此进程变成该新对话期的对话期首进程(session leader,对话期首进程是创建该对话期的进程)。此进程是该新对话期中的唯一进程。
  2. 此进程成为一个新进程组的组长进程。新进程组ID是此调用进程的进程ID。
  3. 此进程没有控制终端。如果在调用setsid之前此进程有一个控制终端,那么这种联系也被解除。

如果此调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不处于这种情况,通常先调用fork,然后使其父进程终止,而子进程再继续setsid。

 

23.控制终端

一个对话期可以有一个单独的控制终端(controlling terminal)。这通常是我们在其上登录的终端设备(终端登录情况)或伪终端设备(网络登录情况)。

建立与控制终端连接的对话期首进程,被称之为控制进程(controlling process)。

一个对话期中的几个进程组可被分成一个前台进程组(foreground process group)以及一个或几个后台进程组(background process group)。

如果一个对话期有一个控制终端,则它有一个前台进程组,其他进程组则为后台进程组。

无论何时键入中断键(常常是DELETE或Ctrl-C)或退出键(常常是Ctrl-\),以及挂起字符(一般采用Ctrl-Z),就会造成将中断信号或退出信号或挂起信号送至前台进程组的所有进程。

如果终端界面检测到调制解调器已经脱开连接,则将挂断信号送至控制进程(对话期首进程)。

系统如何分配一个控制终端依赖于实现:

有时不管标准输入、标准输出是否重新定向,程序都要与控制终端交互作用。保证程序读写控制终端的方法是打开文件/dev/tty,在内核中,此特殊文件是控制终端的同义语。自然,如果程序没有控制终端,则打开此设备将失败。

 

24.前台进程组

使用如下函数能获得哪一个进程组是前台进程组:

#include <sys/types.h>
#include <unistd.h>
pid_t tcgetpgrp(int filedes);
返回:若成功则为前台进程组ID,若出错则为-1

int tcsetpgrp(int filedes, pid_t pgrpid);
返回:若成功则为0 ,若出错则为-1

函数tcgetpgrp返回前台进程组ID,它与在filedes上打开的终端相关。

如果进程有一个控制终端,则该进程可以调用tcsetpgrp将前台进程组ID设置为pgrpid。pgrpid值应当是在同一对话期中的一个进程组的ID。filedes必须引用该对话期的控制终端。

大多数应用程序并不直接调用这两个函数。它们通常由作业控制shell调用。只有定义了_POSIX_JOB_CONTROL,这两个函数才被定义了,否则它们返回出错。

 

25.后台进程与终端

如果后台作业试图读终端,那么这并不是一个错误,但是终端驱动程序检测这种情况,并且发送一个特定信号SIGTTIN给后台作业。这通常会停止此后台作业。

但对于一个孤儿进程组例外,如果内核用此信号停止它,则此进程组中的进程就再也不会继续。POSIX.1规定,read返回出错,其errno设置为EIO。

后台进程组进程的输出是否出现在终端是可选择的。通常,可以用stty(1)命令改变这一选择。向终端输出可能会收到SIGTTOU信号。

 

26.孤儿进程组

一个父进程已终止的进程称为孤儿进程(orphan process),这种进程由init进程收养。

整个进程组也可成为孤儿,POSIX.1将孤儿进程组(orphaned process group_定义为:

对孤儿进程组的另一种描述可以是:

如果进程组不是孤儿进程组,那么在属于同一对话期的另一个组中的父进程就有机会重新起动该组中停止的进程。

POSIX.1要求向新孤儿进程组中处于停止状态的每一个进程发送挂断信号(SIGHUP),接着又向其发送继续信号(SIGCONT)。在处理了挂断信号后,子进程继续。对挂断信号的系统默认动作是终止该进程,为此必须提供一个信号处理程序以捕捉该信号。