本文实际上是 "UNIX环境高级编程" 的读书笔记.
所以许多细节并没有表述出来, 想要刨根问底的同学建议再看看原书.
之所以把读书笔记贴到博客上, 出于两个目的:
1. 加深自己的学习效果.
2.
提供一个快速浏览的方式.
本文提到的技术在下面的环境中实际验证过:
Linux version 2.6.18-164.el5 x86_64 GNU/Linux
(gcc version 4.1.2 20080704 (Red Hat 4.1.2-46))
程序和进程
程序是指磁盘上的可执行文件, 内核使用 exec 函数将程序读入内存, 并使其执行.
程序的执行实例称为进程. 一个程序可以启动若干个进程.
进程标识符
内核使用一个唯一的非负整数来标识每一个进程, 称为进程ID (process ID).
当进程终止后, pid 就可以再次使用了, (过一段时间后才会被使用).
#include <unistd.h>
pid_t getpid();//进程id
pid_t getppid();//父进程id
pid 为 0 的进程通常是调度进程, 也被称为 交换进程(swapper). 它是系统进程.
pid 为 1 的进程通常是 init 进程, 在自举过程结束时由内核调用. 负责启动系统.
它是一个普通的用户进程, 以超级用户特权运行.
进程启动
C程序总是从 main 函数开始执行, main 函数的原型是:
int main(int argc, char *argv[]);
内核将启动进程的命令行参数通过 main 函数的参数传递给进程.
此外, 内核还会通过变量 environ 传递环境变量.
通常, C程序不直接使用 environ, 而是通过 getenv 函数来取环境变量的值.
参数表和环境表一样, 都是一个c字符串指针数组.
不同的是, 参数表中参数的个数, 是由一个参数表示的.
环境表不提供环境变量的个数, 它把最后一个环境变量指针设为null来表明环境表结束.
一个进程可以调用 fork 函数创建一个新进程.
#include <unistd.h>
pid_t fork();//子进程返回0, 父进程返回子进程ID, 出错返回-1.
通常, fork 之后是父进程先执行还是子进程先执行是不确定的.
父子进程共享正文段, 但是不共享数据段.
fork 函数会让子进程复制当前父进程的数据段和堆栈.
vfork 函数也能创建一个新进程, 但是它不会复制父进程的数据段和堆栈.
而且, 父进程调用 vfork 后会被阻塞, 直到子进程调用exec 或 exit;
进程终止
有八种方式使进程终止, 它们是:
1. 从main函数返回.
2. 调用 exit.
3. 调用 _exit 或 _Exit
4. 最后一个线程从其启动例程返回.
5. 最后一个线程调用 pthread_exit.
6. 调用 abort. 它产生 SIGABRT 信号.
7. 收到一个信号并终止.
8. 最后一个线程对取消请求作出相应.
其中 1-5 是正常退出, 6-8 是异常终止.
有三个函数用于正常退出一个进程.
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
_exit 和 _Exit 立即进入内核,
exit 则先执行一些清理 (调用执行各终止处理程序, 关闭所有标准IO流等), 然后进入内核.
status 是进程的终止状态.
以下几种情况, 进程的终止状态是未定义的.
1. 调用这些函数时不带终止状态.
2. main 执行了一个无返回值的 return.
3. main 没有声明返回类型为整形.
如果main 的返回类型是整形, 并且最后一条语句没有提供返回值, 那么该进程的终止状态是0.
在 main 函数中 exit(0) 等价于 return 0;
不管进程如何终止, 最后都会执行内核中的同一段代码.
这段代码为相应进程关闭所有打开的描述符, 释放它使用的寄存器.
子进程的终止
当进程终止时, 内核会向其父进程发送 SIGCHLD 信号.
这个信号系统默认是忽略它. 程序员可以为这个信号注册一个信号处理函数来捕捉这个信号.
父进程可以用 wait 或 waitpid 函数来获得子进程的终止状态.
exit, _Exit, _exit 三个函数会把终止状态传递给父进程.
如果进程是异常终止的, 内核会产生一个指明其原因的终止状态.
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
成功则返回进程id, 出错返回-1;
调用 wait 或 waitpid 函数时, 可能发生这些情况:
1. 如果其所有子进程都还在运行, 则阻塞.
2. 如果一个子进程已经终止(僵死进程), 返回该子进程的终止状态.
3. 如果它没有任何子进程, 立即出错返回;
如果进程收到 SIGCHLD 信号后调用 wait , 可期望 wait 会立即返回.
在其它时候, 进程很可能会阻塞.
对于父进程已经终止的进程, 他们的父进程都将改变为 init 进程.
waitid, wait3, wait4 这几个函数也可以用来获取子进程的终止状态.
僵死进程
当一个子进程终止时, 如果它的父进程还在运行, 内核会为这个终止的子进程保留一定量的信息.
父进程可以根据这些信息知道子进程的情况. 直到父进程对其进行了善后处理, 子进程才会完全终止.
在这期间, 这个子进程会成为 僵死进程, 它仍然占用一定资源.
用户终止处理程序
按照 ISO C 的规定, 一个进程可以登记多达 32 个函数, 这些函数将由 exit 自动调用.
这些函数称为终止处理程序. 使用 atexit 函数来登记这些函数.
#include <stdlib.h>
int atexit(void (*func)(void));//成功返回0, 出错返回非0
exit 调用这些函数的顺序与登记顺序相反. 同一个函数如果被登记多次, 也会被调用多次.
要确定一个平台支持的最大终止处理程序数, 可以使用 sysconf 函数.
进程执行和终止流程
在进程中执行新程序
fork 用来创建子进程, exec 函数用来执行另一个程序.
当进程调用exec后, 一个全新的程序替换了当前进程的正文, 数据, 和堆栈段.
新程序从其main函数开始执行.
新程序的 pid 不会变化, 此外, 原进程的其它一些特征也会被保留.
有六种不同的exec 函数: execl, execv, execle, execve, execlp, execvp;
进程的权限是由启动进程的用户和组id决定的, 有时为了安全考虑, 只给以进程最小权限.
程序可以通过一些函数动态的修改进程所属的用户和组id, 以限制进程的权限, 这些函数包括:
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
int setreuid(uid_t ruid, uid_t euid);
int setregid(gid_t rgid, gid_t egid);
成功返回0, 出错返回-1
uid_t getuid();//用户id
uid_t geteuid();//有效用户id
gid_t getgid();//实际组id
gid_t getegid();//有效组id
进程环境
C程序的存储空间布局:
正文段. 由CPU执行的机器指令组成. 通常, 正文段由多个进程共享, 并且是只读的.
初始化数据段
. 通常称为数据段, 包含所有在函数外声明并明确地赋初值的变量.
非初始化数据段
. 也被称为bss(由符号开始的块)段.
在程序开始执行前, 内核将此段中的数据初始化为0或空指针.
任何在函数外声明(但是没有明确赋初值)的变量都存放在非初始化数据段中.
栈. 自动变量和函数调用时需要保存的信息, 都放在栈中.
堆. 在堆中进行动态内存分配. 由于历史惯例, 堆位于非初始化数据段和栈之间.
除了上面的段, 还有若干其它类型的段. 比如:
包含符号表的段, 包含调试信息的段, 以及包含动态共享库链接表的段等等.
这些部分并不装载到进程执行的程序映象中.
size 指令可以显示程序的正文段, 数据段 和 bss段的长度:
第四和第五列分别以十进制和十六进制表示三个段的总长度.
资源限制
每个进程都有一组资源限制, 其中的一些可以用 getrlimit 和 setrlimit 函数查询和更改.
更改资源限制时, 需要遵循下列规则:
1. 任何一个进程都可以将一个软限制值更改为小于等于其硬限制值.
2. 任何一个进程都可以降低其硬限制值, 但必须大于或等于其软限制值.
这种降低对普通用户是不可逆的.
3. 只有超级用户进程才可以提高硬限制值.
常量 RLIM_INFINITY 表明无限享.
下面列出了一些资源类型:
RLIMIT_AS 进程可用存储区的最大字节数. 影响 sbrk 函数 和 mmap 函数.
RLIMIT_CORE core文件的最大字节数, 为0则阻止创建core文件.
RLIMIT_CPU CPU时间的最大量(秒), 超过此软限制时, 进程会收到 SIGXCPU 信号.
RLIMIT_DATA 数据段的最大字节长度. 包括初始化数据段, 非初始化数据段 和 堆的总和.
RLIMIT_FSIZE 创建的文件的最大字节长度. 超过此软限制时, 进程会收到 SIGXFSZ 信号.
RLIMIT_LOCKS 可持有的文件锁的最大数. 此数包括Linux特有的文件租借数.
RLIMIT_MEMLOCK 进程使用 mlock 能够锁定在寄存器中的最大字节长度.
RLIMIT_NOFILE 能打开的最大文件数. 会影响 sysconf 函数在参数 _SC_OPEN_MAX中的返回值.
RLIMIT_NPROC 每个实际用户ID可拥有的最大子进程数. 会影响 sysconf 函数在参数 _SC_CHILD_MAX中的返回值.
RLIMIT_RSS 最大驻内存集(RSS)的字节长度. 如果物理存储器供不应求, 内核将从进程取回超过RSS的部分.
RLIMIT_STACK
栈的最大字节长度.
资源限制会被子进程继承.
我们通常使用 shell 的 limit 命令在进程启动前设置资源限制.
以下代码用来查看进程的资源限制:
#include <iostream>
#include <sys/resource.h>
#include <assert.h>
#define PRINT_LIMIT(NAME) \
assert(getrlimit(NAME, &limit)==0);\
printf("%s: soft limit = %d, hard limit = %d\n", \
#NAME, limit.rlim_cur, limit.rlim_max);
int main(){
printf("RLIM_INFINITY = %d\n", RLIM_INFINITY);
rlimit limit;
PRINT_LIMIT(RLIMIT_AS)
PRINT_LIMIT(RLIMIT_CORE)
PRINT_LIMIT(RLIMIT_CPU)
PRINT_LIMIT(RLIMIT_DATA)
PRINT_LIMIT(RLIMIT_FSIZE)
PRINT_LIMIT(RLIMIT_LOCKS)
PRINT_LIMIT(RLIMIT_MEMLOCK)
PRINT_LIMIT(RLIMIT_NOFILE)
PRINT_LIMIT(RLIMIT_NPROC)
PRINT_LIMIT(RLIMIT_RSS)
PRINT_LIMIT(RLIMIT_STACK)
return 0;
}
执行命令字符串
ISO C 定义了 system 函数, 可以很方便的执行一个命令字符串.
#include <stdlib.h>
int system(const char *cmdstring);
例如: system("date > file");//把日期存入文件.
由于system 在其实现中调用了 fork, exec 和 waitpid, 因此有三种返回值.
1. 如果fork 失败或者waitpid 返回除 EINTR 之外的错, 则返回 -1, 并在 errno 中设置错误类型值.
2. 如果 exec 失败(表示不能执行shell), 其返回值如同shell执行了 exit(127) 一样.
3. 如果 fork, exec, waitpid 都执行成功, 返回值是shell 的终止状态.
进程组
每个进程都属于某个进程组.
进程组是一个或多个进程的集合. 通常它们与同一作业相关联, 可以接收来自同一终端的各种信号.
每个进程组有一个唯一的进程组ID.
#include <unistd.h>
pid_t getpgrp();//当前进程的进程组id
pid_t getpgid(pid_t pid);//根据pid返回对应的进程组id.
每个进程组都有一个组长进程. 进程id等于进程组id的进程就是组长进程.
会话
会话(session) 是一个或多个进程组的集合.