《Unix环境高级编程》读书笔记 第8章-进程控制

1. 进程标识

  • 进程ID标识符是唯一、可复用的。大多数Unix系统实现延迟复用算法,使得赋予新建进程的ID不同于最近终止所使用的ID
  • ID为0的进程通常是调度进程,也常被称为交换进程。它是内核的一部分,是系统进程。
  • ID为1的进程通常是init进程,在自举过程结束时由内核调用。该进程负责在内核自举后启动一个Unix系统,它决不会终止,是一个普通的用户进程,但以超级用户特权运行。
  • ID为2的进程是页守护进程,负责支持虚拟存储器系统的分页操作。
#include <unistd.h>
pid_t getpid(void);
Returns: process ID of calling process
pid_t getppid(void);
Returns: parent process ID of calling process
uid_t getuid(void);
Returns: real user ID of calling process
uid_t geteuid(void);
Returns: effective user ID of calling process
gid_t getgid(void);
Returns: real group ID of calling process
gid_t getegid(void);
Returns: effective group ID of calling process
  • 注意:这些函数都没有出错返回。

2. 函数fork

  • fork函数被调用一次,返回两次。子进程中返回值是0,父进程中返回值是子进程的pid
  • 子进程是父进程的副本,子进程获得父进程的数据空间、堆和栈的副本。注意,在是子进程拥有的副本。父子进程并不共享这些存储空间部分。父子进程共享正文段。
  • 由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、堆和栈的完全副本。作为替代,使用了写时复制技术。
  • 4种平台都支持的变体:vfork;Linux的变体:clone系统调用,允许调用者控制哪些部分由父子进程共享。
  • fork之后是父进程先执行还是子进程先执行是不确定的
  • 父进程中的所有打开文件描述符都被复制到子进程中,父子进程为每个相同的打开描述符共享一个文件表项,故共享同一文件偏移量。如果父子进程写同一描述符执行的文件,又没有任何形式的同步,那么它们的输出就会混合。
  • 在fork之后处理文件描述符有以下两种常见的情况:
    1. 父进程等待子进程完成。这种情况下,父进程无需对其描述符做任何处理。
    2. 父进程和子进程各自执行不同的程序段。这种情况下,fork之后,父子进程各自它们不需要使用的文件描述符。
  • strlen和sizeof的区别:前者不包括null字节,一次函数调用;后者包括null字节,编译时计算

  • 除了文件描述符之外,父进程的很多其他属性也由子进程继承,包括:

    1. 实际用户ID、实际组ID、有效用户ID、有效组ID
    2. 附属组ID
    3. 进程组ID
    4. 会话ID
    5. 控制终端
    6. SUID和SGID标志(stat结构的st_mode成员)
    7. 当前工作目录
    8. 根目录
    9. 文件模式创建屏蔽字umask
    10. 信号屏蔽和处理
    11. 对任一打开文件描述符的执行时关闭(close-on-exec)标志
    12. 环境
    13. 连接的共享存储段
    14. 存储映像
    15. 资源限制
    16. 是否继承nice值由具体实现自行决定
  • 父进程和子进程之间的区别具体如下:

    1. fork的返回值不同
    2. pid不同
    3. 这两个进程的父进程不同
    4. 子进程的tms_utime、tms_stime、tms_cutime和tms_ustime的值设置为0
    5. 子进程不继承父进程设置的文件锁
    6. 子进程的未处理闹钟被清除
    7. 子进程的未处理信号集设置为空集
  • fork失败的两个主要原因:

    1. 系统中已经有了太多的进程
    2. 该实际用户ID的进程总数超过了系统限制
  • fork有以下两种用法:

    1. 一个父进程希望复制自己,使父进程和子进程同时执行不同的代码段。这在网络服务器中是常见的。
    2. 一个进程要执行一个不同的程序。这对shell是常见的情况。某些系统将fork+exec组合成一个操作spawn

3. 函数vfork

  • vfork函数的调用序列和返回值与fork相同,但两者的语义不同:
    1. vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序,故不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会引用该地址空间。不管在子进程调用exec或exit之前,它在父进程的空间中运行。
    2. 另一个区别是vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。故如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

4. 函数exit

  • 5种正常终止方式:
    1. 从main中执行return,等效于调用exit
    2. 调用exit函数,调用各终止处理程序,关闭标准I/O流,最后调用_exit函数
    3. 调用_exit或_Exit
    4. 进程的最后一个线程在其启动例程执行return语句,该进程以终止状态0返回
    5. 进程的最后一个线程调用pthread_exit,进程终止状态总是0
  • 3种异常终止方式:
    1. 调用abort,它产生SIGABRT信号
    2. 当进程接收到某些信号时,信号可由进程自身(如调用abort函数)、其他进程或内核产生
    3. 最后一个线程对“取消”请求做出响应
  • 不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应的进程关闭所有打开描述符,释放它所使用的存储器等。
  • 注意:“退出状态”(3个exit函数的参数或main的返回值)区别于“终止状态”。在最后调用_exit时,内核将退出状态转换为终止状态。

  • 如果父进程在子进程之前终止,则称子进程为孤儿进程。子进程 ppid变为1,称这些进程由init进程收养。一个init进程收养的进程终止时,init会调用一个wait函数取得其终止状态,防止它成为僵尸进程。

  • 如果子进程在父进程之前终止,内核为每个终止子进程保存了一定量的信息,至少包括pid、该进程的终止状态以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储区,关闭其所有打开文件。在Unix术语中,一个已经终止、但其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程被称为僵尸进程zombie/defunct。

5. 函数wait、waitpid

  • 当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。子进程终止是异步事件。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
Both return: process ID if OK, 0 (see later), or 1 on error
  • 调用wait或waitpid的进程可能:
    1. 如果其所有子进程都还在运行,则阻塞
    2. 如果一个子进程终止,正等待其父进程获取其终止状态,则取得该子进程的终止状态立即返回
    3. 如果它没有任何子进程,则立即出错返回
  • 如果进程由于收到SIGCHLD信号而调用wait,我们期望wait会立即返回。
  • wait与waitpid的区别
    1. waitpid有一选项,可使调用者不阻塞
    2. waitpid可以控制它所等待的进程
  • 若statloc不是NULL,则终止进程的终止状态就存放在它所指向的单元内。该整型状态字由实现定义,其中某些位表示退出状态(正常返回),其他位则指示信号编号(异常返回),有一位指示是否产生了core文件。

  • waitpid函数中的pid参数的解释:

    pid == -1,等待任一子进程,等价于wait函数
    pid > 0,等待pid等于该值的子进程
    pid == 0,等待组ID等于调用进程组ID的任一子进程
    pid < 0,等待组ID等于pid绝对值的任一子进程

  • waitpid函数中的options参数:WNOHANG(不阻塞)、WCONTINUED、WUNTRACED

  • 如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵尸状态直到父进程终止,实现这一要求的诀窍是调用fork两次。

#include "apue.h"
#include <sys/wait.h>
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* first child */
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid > 0)
exit(0); /* parent from second fork == first child */
/*
* We’re the second child; our parent becomes init as soon
* as our real parent calls exit() in the statement above.
* Here’s where we’d continue executing, knowing that when
* we’re done, init will reap our status.
*/
sleep(2);
printf("second child, parent pid = %ld\n", (long)getppid());
exit(0);
}
if (waitpid(pid, NULL, 0) != pid) /* wait for first child */
err_sys("waitpid error");
/*
* We’re the parent (the original process); we continue executing,
* knowing that we’re not the parent of the second child.
*/
exit(0);
}

6. 函数waitid

#include <sys/wait.h>
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
Returns: 0 if OK, 1 on error
  • idtype参数:P_PID、P_PGID、P_ALL
  • options参数:WEXITED、WNOHANG...
  • infop参数是指向siginfo结构的指针

7. 函数wait3、wait4

#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);
Both return: process ID if OK, 0, or 1 on error
  • 允许内核返回由终止进程及其所有子进程使用的资源概况,包括用户CPU时间总量、系统CPU时间总量、缺页次数、接收到信号的次数等。 man 2 getrusage

8. 竞争条件

  • 当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则发生了竞争条件。fork函数是竞争条件活跃的滋生地。
#include "apue.h"
TELL_WAIT(); /* set things up for TELL_xxx & WAIT_xxx */
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
/* child does whatever is necessary ... */
TELL_PARENT(getppid()); /* tell parent we’re done */
WAIT_PARENT(); /* and wait for parent */
/* and the child continues on its way ... */
exit(0);
}
/* parent does whatever is necessary ... */
TELL_CHILD(pid); /* tell child we’re done */
WAIT_CHILD(); /* and wait for child */
/* and the parent continues on its way ... */
exit(0);
  • 5个例程

9. 函数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 *arg0, ... /* (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[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
All seven return: 1 on error, no return on success
  • l表示列表list,新程序的每个命令行参数都是一个单独的参数,空指针结尾
  • v表示矢量vector,指针数组
  • e代表传递一个指向环境字符串指针数组的指针
  • p代表使用调用进程中的environ变量为新程序复制现有的环境

  • 在执行exec后,pid没有改变。但新程序从调用进程继承了下列属性:

    1. pid和ppid
    2. 实际用户ID和实际组ID
    3. 附属组ID
    4. 进程组ID
    5. 会话ID
    6. 控制终端
    7. 闹钟尚余留的时间
    8. 当前工作目录
    9. 根目录
    10. 文件模式创建屏蔽字umask
    11. 文件锁
    12. 进程信号屏蔽
    13. 未处理信号
    14. 资源限制
    15. nice值
    16. tms_utime、tms_stime、tms_cutime、tms_cstime
    17. 对打开文件的处理:若文件描述符的执行时关闭(close-on-exec,默认通过fcntl设置)标志被设置(默认没有设置),则在执行exec时关闭该描述符;否则仍保持打开。POSIX.1明确要求在exec时关闭打开目录流。

    

10. 更改用户ID和更改组ID

#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
Both return: 0 if OK, 1 on error
  • 关于谁能更改ID的若干规则(这里讨论更改用户ID的规则,同样适用于组ID)
  • 实际用户ID ruid、有效用户ID euid、保存的设置用户ID sSUID。假定_POSIX_SAVED_IDS为真
    1. 若进程具有root特权,则setuid函数将ruid、euid和sSUID设置为参数uid的值
    2. 若进程没有root特权,但uid等于ruid或sSUID,则setuid函数只将euid设置为uid
    3. 如果上面两个条件都不满足,则errno设置为EPERM,并返回-1
  • 关于内核所维护的3个用户ID,还有注意以下几点:
    1. 只有root进程可以更改ruid。通常,ruid是在用户登录时,由login程序设置的,而且决不会改变它。
    2. 仅当对程序文件设置了SUID位,exec函数才设置euid。没有设置SUID位,则euid = ruid。
    3. sSUID是由exec复制euid而得到的。

   
10.1 函数setreuid、setregid

#include <unistd.h>
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
Both return: 0 if OK, 1 on error
  • 规则:一个非root用户总能交换ruid和euid,这就允许一个设置了SUID的程序交换成普通用户权限后,可以再次交换会SUID权限

10.2 函数seteuid、setegid

#include <unistd.h>
int seteuid(uid_t uid);
int setegid(gid_t gid);
Both return: 0 if OK, 1 on error
  • 对于非root用户,可将euid设置为其ruid或sSUID;这与setuid函数一样
  • 对于root用户,可将其euid设置为uid,而ruid、sSUID保持不变

  • 组ID:上面所说的一切都以类似方式适用于各个组ID。附属组ID不受setgid、setregid、setegid函数的影响。

11. 解释器文件

12. 函数system

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

13. 进程会计

  • accton命令启用会计处理;在Linux中,该文件是/var/account/pacct
typedef u_short comp_t; /* 3-bit base 8 exponent; 13-bit fraction */
struct acct
{
char ac_flag; /* flag (see Figure 8.26) */
char ac_stat; /* termination status (signal & core flag only) */
/* (Solaris only) */
uid_t ac_uid; /* real user ID */
gid_t ac_gid; /* real group ID */
dev_t ac_tty; /* controlling terminal */
time_t ac_btime; /* starting calendar time */
comp_t ac_utime; /* user CPU time */
comp_t ac_stime; /* system CPU time */
comp_t ac_etime; /* elapsed time */
comp_t ac_mem; /* average memory usage */
comp_t ac_io; /* bytes transferred (by read and write) */
/* "blocks" on BSD systems */
comp_t ac_rw; /* blocks read or written */
/* (not present on BSD systems) */
char ac_comm[8]; /* command name: [8] for Solaris, */
/* [10] for Mac OS X, [16] for FreeBSD, and */
/* [17] for Linux */
};

14. 用户标识

  • 获取登陆名
#include <unistd.h>
char *getlogin(void);
Returns: pointer to string giving login name if OK, NULL on error
  • 如果调用此函数的进程没有连接到用户登陆时所用的终端,则函数会失败。通常称这些进程为守护进程。

15. 进程调度

  • 进程通过调整nice值选择以更低优先级运行。只有特权进程允许提高调度权限。
  • nice值的范围在0~(2*NZERO)-1之间,NZERO为系统默认的nice值。nice值越小,优先级越高。
#include <unistd.h>
int nice(int incr);
Returns: new nice value NZERO if OK, 1 on error
#include <sys/resource.h>
int getpriority(int which, id_t who);
Returns: nice value between NZERO and NZERO1 if OK, 1 on error
#include <sys/resource.h>
int setpriority(int which, id_t who, int value);
Returns: 0 if OK, 1 on error

16. 进程时间

#include <sys/times.h>
clock_t times(struct tms *buf );
Returns: elapsed wall clock time in clock ticks if OK, 1 on error
struct tms {
clock_t tms_utime; /* user CPU time */
clock_t tms_stime; /* system CPU time */
clock_t tms_cutime; /* user CPU time, terminated children */
clock_t tms_cstime; /* system CPU time, terminated children */
};
posted on 2014-08-31 23:13  DayByDay  阅读(808)  评论(0编辑  收藏  举报