APUE笔记之7-9章:进程环境、进程控制、进程关系

版权声明:本文为博主原创文章,可以转载但必须注明出处。 https://blog.csdn.net/nirendao/article/details/88146259

第7章 进程环境

进程终止

有8种方式使得进程终止,其中5种为正常终止,3种为异常终止

正常终止的方式:

  1. 从main返回0
  2. 调用exit
  3. 调用 _exit 或 _Exit 
  4. 最后一个线程从其启动例程返回
  5. 最后一个线程调用 pthread_exit 

异常终止的方式:

  1. 调用 abort
  2. 接到一个 signal
  3. 最后一个线程对取消请求做出响应

_exit 和 _Exit 立即进入内核,而 exit 函数则先执行一些清理处理,然后返回内核。

按照ISO C的规定,一个进程可以登记最多32个函数,这些函数由 exit 自动调用。这些函数被称之为 终止处理程序(exit handler)。我们通过调用 atexit 函数来登记这些 exit handler.

exit 调用这些函数的顺序与登记它们的顺序相反。同一个函数如果被多次登记,则也会被多次调用。

为了确定一个平台支持的最大终止处理程序数,可以使用 sysconf 函数查看。

#include <stdllib.h>

int atexit(void (*func)(void));

环境表

每个程序都接收到一张环境表(注:即环境变量表)。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null结尾的C字符串地址。

全局变量 environ 则包含了该指针数组的地址: extern char **environ;

历史上,大多数UNIX系统支持 main 函数带3个参数,其中第3个参数是环境表:

int main(int argc, char *argv[], char *envp[]); 

因为ISO C规定main函数只有2个参数,而且第3个参数和全局变量 environ 相比也没有带来更多益处,所以 POSIX.1 规定应使用 environ ,而不使用第3个参数。

通常使用 getenv 和 putenv 函数来访问特定的环境变量,而不是使用 environ 变量。

C程序的存储空间布局

C程序由以下几个部分组成:

  1. 正文段(注:即代码段): 这是CPU执行的机器指令部分。
  2. 初始化数据段(注:也即数据段):它包含了程序中需明确赋值初值的变量(注:似乎指的是在源代码中已经赋值的全局和静态变量以及常量)
  3. 未初始化数据段(也称为BSS段):在程序开始执行之前,内核将此段中的数据初始化为0或空指针,比如:函数外的声明 long sum[1000];  (注:似乎是未赋值的全局和静态变量)
  4. :比如,递归函数每次调用自身时,都会使用一个新的栈桢
  5. : 由于历史惯例,堆位于未初始化数据段和栈之间

典型的存储空间安排:

 

共享库 (shared lib)

优点:

  1. 减少了可执行文件的长度
  2. 可以方便地使用库的新版本代替老版本,只要接口不变

使用无共享库的方式创建可执行文件:

gcc -static hello.c

默认是使用共享库编译程序的

gcc hello.c 

存储空间分配函数: malloc, calloc, realloc

环境变量相关函数: getenv, putenv, setenv, unsetenv 

setjmp和longjmp

在C语言中,goto是不能跨函数的,而具有这种跳转能力的是 setjmp 和 longjmp . 这2个函数对于处理发生在很深层嵌套函数调用中的出错情况是非常有用的。比如,函数调用情况如下: 

A -> B -> C -> D -> E 

从A到E有5层之多,若在E中发生了错误,需要将错误一层层返回到A中,非常费事费时。并且A-E各自在栈上都有自己的栈桢。若想在栈上跳过若干调用桢,则需使用 setjmp 和 longjmp . 

#include <setjmp.h>

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);   // val 是将来setjmp的返回值

setjmp参数env的类型是一个特殊类型 jmp_buf,它是某种形式的数组,其中存放当调用 longjmp 时能用来恢复栈状态的所有信息。通常,将 env 设置为全局变量。

另外,注意:

  1. 编译器优化之后,当由longjmp返回后,自动变量(auto)和寄存器变量(register)的值都会被改变,而全局变量,静态变量和volatile变量不会被改变;
  2. 若编译器没有优化,则以上5个值都不会被改变;
  3.  声明auto变量的函数返回之后,不能再引用这些auto变量。 - 这在UNIX手册中经常被提及。

getrlimit函数 和 setrlimit函数

每个进程都有一组资源限制,其中一些可以用 getrlimit 和 setrlimit 函数查询和更改。

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

  1. 任何一个进程都可以将一个软限制值更改为小于等于其硬限制值
  2. 任何一个进程都可以降低其硬限制值,但它必须大于等于其软限制值;这种降低,对于普通用户而言是不可逆的。
  3. 只有超级用户才能提高硬限制值

第8章 进程控制

大多数UNIX系统实现了延迟复用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID. 

ID=0 的进程叫做调度进程,也常常被称为 交换进程(swapper),属于内核的一部分。它并不执行任何磁盘上的程序,因此也被称为系统进程。

ID=1 的进程叫做init进程,在自举过程结束时由内核调用。此进程负责在自举内核后启动一个UNIX系统。init进程通常读取与系统有关的初始化文件( /etc/rc* 文件或 /etc/inittab 文件,以及在 /etc/init.d 中的文件),并将系统引导到一个状态(如多用户)。

init进程是一个普通的用户进程,但它决不会终止,且它以超级用户特权运行。

一些进程函数

#include <unistd.h>

pid_t getpid(void);
pid_t getppid(void);
uid_t getuid(void);
uid_t geteuid(void);    // 有效用户
gid_t getgid(void);
gid_t getegid(void);     // 有效组


 函数 fork

#include <unistd.h>

pid_t fork(void);

fork函数创建子进程。它被调用一次,但返回2次:在父进程中,返回子进程的pid;而在子进程中,返回0. 

  • 子进程拷贝获得父进程的数据空间、堆、栈
  • 父子进程共享代码段。
  • 父子进程共享同一个文件的偏移量。

由于fork之后经常跟着exec,所以现在的很多实现并不执行一个父进程的数据段、堆和栈的完全拷贝,而是使用了写时拷贝技术(Copy-On-Write)。意思就是,父进程的数据段、堆、栈由父进程和子进程共享,但访问权限为只读;只有当父进程和子进程中的任何一个试图修改这些区域时,内核才为要修改的那块区域的内存制作一个副本,通常是虚拟存储系统中的一“页”。

strlen计算字符串长度时不包含null字节;而sizeof计算长度时则计算包含null字节的缓冲区长度。

使用strlen需进行一次函数调用,而对于sizeof而言,因为缓冲区已用已知字符串进行初始化,其长度是固定的,所以sizeof是在编译时计算缓冲区的长度

fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。说“复制”,是因为对每个文件描述符而言,就好像执行了dup函数。父进程和子进程每个相同的打开描述符共享同一个文件表项(见图3-9)。

在fork之后,处理文件描述符,通常有以下2种常见的方法:

  1. 父进程等待子进程完成。此时,父进程无需对文件描述符做任何处理。
  2. 父进程和子进程各自执行不同的程序段。这样的话,在fork之后,父进程和子进程就各自关闭它们不需使用的文件描述符,也就不会干扰对方使用的文件描述符。这种方法是网络服务进程经常使用的。

子进程继承了父进程的很多内容,见P185-186.

父子进程的区别:见P186,如:

  • 子进程不继承父进程设置的文件锁
  • 子进程的未处理闹钟被清除
  • 子进程的未处理信号集设置为空集

fork有以下2种用法:

  1. 一个父进程希望复制自己,使得父进程和子进程同时执行不同的代码段。这在网络服务进程中比较常见:父进程等待客户端的服务请求;当请求到达时,父进程调用fork,生成子进程来处理此请求;父进程则继续等待下一个服务请求。
  2. 一个进程要执行另一个不同的程序。在这种情况下,子进程从fork返回后,立即调用exec. 

vfork 函数

#include <unistd.h>

pid_t vfork(void);

vfork函数用于创建一个新进程,目的就是 exec 一个新程序。

vfork并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),所以也就不会引用该地址空间。

vfork 保证子进程先运行。在它调用exec或exit之后,父进程才可能被调度运行。当子进程调用这2个函数的任意一个时,父进程会恢复运行。

进程终止

不管进程如何终止(7.3节,进程有5种正常终止和3种异常终止),最后都会执行内核中的一段代码,这段代码为相应进程关闭所有打开的描述符,释放它所使用的存储器等。

当一个进程终止时,内核逐个检查所有active的进程,看它是否是正要终止的进程的子进程。如果是,则该进程的父进程ID就更改为1(init进程)。也就是说,若父进程在子进程之前终止,则子进程会被init进程收养。被init进程收养的进程,也叫孤儿进程。

内核为每个终止子进程都保存了一些信息,包括进程ID、进程的终止状态、该进程使用的CPU时间总量。当父进程调用wait或waitpid时,可以获取到这些信息;然后内核释放终止进程所使用的所有存储区,关闭其所有打开文件。

僵尸进程(zombie)

一个已经终止、但是父进程还没有对其进行善后处理(获取终止子进程的相关信息、释放它所占用的资源)的进程就是僵尸进程。 ps命令将僵尸进程的状态打印为Z. (即父进程中没有调用wait函数)

若有一个长期运行的程序,它fork了很多子进程,那么除非父进程wait以取得子进程的终止状态,否则这些子进程终止后就会变成僵尸进程。

被init进程收养的进程终止时会变成僵尸进程吗?

不会。因为无论何时只要有一个init进程的子进程终止,init进程就会调用一个wait函数取得其终止状态。这样也就没有僵尸进程了。

函数 wait 和 waitpid

当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号。父进程可以选择忽略该信号,也可以设定信号处理函数;系统默认行为是忽略

调用wait或waitpid的进程会发生:

  • 如果其所有子进程都还在运行,则阻塞;
  • 如果一个子进程已经终止,正等待父进程获取其终止状态,则取得该子进程的终止状态后立即返回;
  • 如果它没有任何子进程,则立即返回。

所以,如果进程由于接收到 SIGCHLD 信号而调用wait,则我们期望wait会立即返回;但是如果是在随机时间点调用wait,则进程很可能阻塞。

#include <sys/wait.h>

pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);  // 若成功,返回终止子进程的进程ID;若出错,返回 0 或者 -1

waitpid 函数中 pid 参数的作用如下:

  • pid == -1    等待任一子进程,此时等同于 wait
  • pid >0         等待该pid的进程
  • pid == 0      等待 组ID 等于 调用进程的进程组ID 的 任一子进程
  • pid < -1       等待组ID等于pid绝对值的任一子进程 

这2个函数的区别是:

  • 在一个子进程终止前,wait使其调用者阻塞;而waitpid有一个选项,可使调用者不阻塞;
  • waitpid 并不等待在其调用之后的第一个终止子进程,它有若干个选项,可以控制它所等待的进程。

终止状态用定义在 <sys/wait.h> 中的各个宏来查看。有4个互斥的宏可用来取得进程终止的原因,它们的名字都以WIF开始。

如果一个进程 fork 一个子进程,但是不想等待子进程终止,也不想子进程处于僵尸状态直到父进程终止,那么,实现这一要求的诀窍就是fork两次。

其他类似函数: waitid、wait3、wait4

函数exec

有7个不同的exec函数可供使用。这些函数的区别是前4个函数取路径名为参数,后2个函数取文件名为参数,最后1个取文件描述符为参数。

在很多的UNIX实现中,这7个exec函数中只有execve是内核的系统调用,而另外6个只是库函数(它们最终调用execve). 

调用exec之后,进程ID没有改变,新进程从调用进程继承了:

  • 进程ID、父进程ID、实际用户ID、实际组ID、附属组ID、进程组ID、
  • 控制终端、闹钟余留时间、当前工作目录、根目录、文件模式创建屏蔽字、
  • 文件锁、进程信号屏蔽、未处理信号、资源限制、nice值、
  • tms_utime, tms_stime, tms_cutime, tms_cstime

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

更改用户ID和更改组ID

P204-207

  1. 只有超级用户进程可以更改实际用户ID. 通常,实际用户ID是在用户登录时由login程序设置的,而且不会改变它。(因为login程序是一个超级用户进程,当它调用setuid时,设置所有3个用户ID)
  2. 仅当对程序文件设置了设置用户ID位时,exec函数才设置有效用户ID. 如果设置用户ID位没有设置,exec函数不会改变有效用户ID,保持不变. 
  3. 保存的设置用户ID是由exec复制有效用户ID得到的。若设置了文件的设置用户ID位,则exec根据文件的用户ID设置进程的有效用户ID. 

一个非特权用户总能交换实际用户ID和有效用户ID. 这就允许一个设置用户ID程序交换成用户的普通权限,以后又可再交换回设置用户ID权限。

进程调度

进程可以通过调整nice值选择以更低优先级运行;只有特权进程允许提高调度权限。

  • nice值的范围在 0-(2*NZERO-1)之内;有些实现支持 0-2*NZERO. NZERO是系统默认的nice值。
  • nice值越小,优先级越高

第9章 进程关系

当子进程终止时,父进程得到通知并能取得子进程的退出状态。

终端

在传统的终端登录中,init知道哪些终端设备可以用来进行登录,并为每个设备生成了一个getty进程。

但是对于网络登录,所有登录都经由内核的网络接口驱动程序(如以太网驱动程序),所以事先不知道会有多少个登录。

为使同一个软件既能处理终端登录,又能处理网络登录,系统使用了一种称为伪终端(pseudo terminal)的软件驱动程序,它仿真串行终端的运行行为,并将终端操作映射为网络操作,反之亦然。

进程组

每个进程除了有一个进程ID之外,还属于一个进程组。进程组是一个或多个进程的集合。

同一个进程组中的各进程接收来自同一终端的各种信号。

每一个进程组有一个进程组ID. 每个进程组有一个组长进程;组长进程的进程ID等于进程组ID. 

在进程组中,只要有一个进程存在,该进程组就存在,和组长进程是否终止无关。

进程调用 setpgid 可以加入一个现有的进程组或者创建一个新进程组。

#include <unistd.h>

int setpgid(pid_t pid, pid_t pgid);

若2个参数相等,则pid指定的进程变为进程组组长;若pgid为0,则由pid指定的进程ID用作进程组ID. 

会话

会话(session)是一个或多个进程组的集合。

通常是由shell的管道将几个进程编成一组的。比如, proc1 |  proc2 &; 又比如: proc3 | proc4 | proc5

进程调用 setsid 函数建立一个新会话。

#include <unistd.h>

pid_t setsid(void);

如果调用此函数的进程不是一个进程组的组长,则会创建一个新会话;否则,该函数返回出错。

一般不使用“会话ID”这样的说话,只是说“会话首进程的进程组ID”;会话首进程总是一个进程组的组长进程。

所谓“会话首进程”,就是创建这个会话的进程。

作业控制

略 (P237-240)


猜你喜欢

转载自blog.csdn.net/nirendao/article/details/88146259