终于抽出时间来研究一下进程了:
一.进程终止说明:
1.mian函数里执行return语句,等效于调用exit。
2.进程的最后一个线程在其启动例程中执行返回语句。但是,该线程的返回值不会用作进程的返回值。当最后一个线程从其启动例程返回时,该进程以终止状态0返回。
3.不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
4.在上述任意一种终止情形下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
5. 如果父进程在子进程之前终止了,那子进程就是“孤儿进程”,且该孤儿进程被init进程领养。
6. 如果一个进程已经终止,其父进程没有对它进行善后处理(即没有调用wait或waitpid 获取终止子进程的有关信息,释放它仍占用的资源)的进程被称为僵死进程。避免僵死进程的方法是调用fork两次。
exit函数
有三个函数用于正常终止一个程序:_exit和_Exit立即进入内核,exit则先执行一些清理处理(包括调用执行各终止处理程序 关闭所有标准I/O流等),然后进入内核。
#include <stdlib.h>
void exit(int status);
void1 _Exit(int status);
#include <unistd.h>
void _exit(int status);
说明:
1.exit和_Exit是由ISO C说明的,而_exit则是由POSIX.1说明的。
2.exit函数在退出时会执行一个标准I/O库的清理关闭操作:为所有打开流调用fclose函数,这会造成所有缓冲的输出数据都被冲洗(写到文件上)。
3.三个exit函数都带一个整型参数,称之为终止状态(或退出状态,exit status)。大多数UNIX shell都提供检查进程终止状态的方法。如果(a)若调用这些函数时不带终止状态,或(b)main执行了一个无返回值的return语句,或(c)main没有声明返回类型为整型,则该进程的终止状态是未定义的。但是,若main的返回类型是整型,并且main执行到最后一条语句时返回(隐式返回),那么该进程的终止状态是0. 还有就是exit(0)和return(0)是等价的。
atexit函数
按照ISO C的规定,一个进程可以登记多达32个函数,这些函数将由exit自动调用。我们称这些函数为终止处理程序,并调用atexit函数来登记这些函数。
#include <stdlib.h>
int atexit(void (*func)(void));
返回值:若成功则返回0,若出错则返回非0值
说明:
1.atexit的参数是一个函数地址,当调用此函数时无需向它传送任何参数,也不期望它返回一个值。
2.exit调用这些函数的顺序与它们登记时候的顺序相反。同一函数如若登记多次,则也会被调用多次。
3.根据ISO C,exit首先调用各终止处理程序,然后按需多次调用fclose,关闭所有打开流。POSIX.1扩展了ISO C标准,它指定如若程序调用exec函数族中的任一函数,则将清除所有已按装的终止处理程序。
4.注意,内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式或隐式地(通过调用exit)调用_exit或_Exit。进程也非自愿地由一个信号使其终止。
二 进程环境
环境表
每个程序都会接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。全局变量environ则包含了该指针数组的地址:extern char **environ; 我们称environ为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串。例如,如果该环境包含5个字符串,那就是如图所示:
说明:
说明:
1.按照惯例,环境由name = value 这样的字符串组成。大多数预定义的名字完全由大写字母组成,但这只是一个惯例。
2.在历史上,大多数unix系统支持main函数带有三个参数,其中第三个参数就是环境表的地址:
int main(int argc, char *argv[], char *envp[]);
因为ISO C规定main函数只有两个参数,而且第三个参数与全局变量environ相比也没有带来更多益处,所以POSIX.1也规定应使用environ而不使用第三个参数。通常用getenv和putenv函数来访问特定的环境变量,而不是environ变量。但是,如果要查看整个环境,则必须使用environ指针。
getenv
#include <stdlib.h>
char *getenv(const char *name);
返回值:指向与name关联的value的指针,若未找到则返回NULL
说明:此函数返回一个指针,它指向name = value字符串的value。我们应当使用getenv从环境中取一个指定环境变量的值,而不是直接访问environ。
putenv setenv unsetenv
#include <stdlib.h>
int putenv(char *str);
int setenv(const char *name,const char *value, int rewrite);
int unsetenv(const char *name);
返回值:三个函数的返回值:若成功则返回0,若出错则返回非0值
说明:
1. 这三个函数的操作是:
putenv取形式为name = value的字符串,将其放到环境表中。如果name已经存在,则先删除其原来的定义。
setenv将name设置为value。如果在环境中name已经存在,那么若rewrite非0,则首先删除其现有的定义;若rewrite为0,则不删除其现有定义(name不设置为新的value,而且也不出错)。
unsetenv删除name的定义。即使不存在这种定义也不算出错。
注意:putenv和setenv之间的差别是:setenv必须分配存储区,以便依据其参数创建name = value字符串。同时,putenv则无需分配存储区,而是将传送给它的参数字符串直接放到环境表中。
2. 这些函数在修改环境表时是如何进行操作的呢?环境表(指向实际name = value 字符串的指针数组)和环境字符串通常存放在进程存储空间的顶部(栈之上)。删除一个字符串很简单 —— 只要先在环境表中找到该指针,然后将所有后续指针都指向环境表首部顺次移动一个位置。但是增加一个字符串或修改一个现有的字符串就困难的多。环境表和环境字符串通常占用的是进程地址空间的顶部,所以它不能再向高地址方向(向上)扩展:同时也不能移动在它之下的各栈帧,所以它也不能向低地址方向(向下)扩展。两者组合使得该空间的长度不能再增加。
(1). 如果修改一个现有的name:
(a). 如果新value的长度少于或等于现有value的长度,则只要在原字符串所用空间中写入新字符串。
(b). 如果新value的长度大于原长度,则必须调用malloc为新字符串分配空间,然后将新字符串复制到该空间中,接着使 环境表中针对name的指针指向新分配区。
(2).如果要增加一个新的name,则操作就更加复杂。首先,调用malloc为name = value 字符串分配空间,然后将该字符串复制 到此空间中,则:
(a) 如果这是第一次增加一个新name,则必须调用malloc为新的指针表分配空间。接着,将原来的环境表复制到新分配区,并将指向新name = value字符串的指针存放在给指针表的表尾,然后又将一个空指针存放在其后。最后使environ指向新指针表。如果原来的环境表位于栈顶之上(这是一种常见情况),那么必须将此表移至堆中。但是,此表中的大多数指针仍指向栈顶之上的各name = value字符串。
(3). 如果这不是第一次增加一个新name,则可知以前已调用malloc在堆中为环境表分配了空间,所以只要调用realloc,以分配比原空间多存放一个指针的空间。然后将指向新name = value字符串的指针存放在该表表尾,后面跟着一个空指针。
setjmp 和 longjmp函数
在C中,goto语句是不能跨越函数的,而执行这类跳转功能的是函数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时能用来恢复栈状态的所有信息。
下图是一个存储器的典型安排,对于X86处理器上的Linux,正文段从0x08048000单元开始,栈底则在0xC0000000之下开始。
getrlimit 和 setrlimit函数
每个进程都有一组资源限制,其中一些可以用getrlimit和setrlimit函数查询和更改。
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit *rlptr);
两个函数返回值:若成功则返回0,若出错则返回非0值。
说明:进程的资源限制通常是在系统初始化时由进程0建立的,然后由每个后续进程继承。对这两个函数的每一次调用都会指定一个资源以及一个指向下列结构的指针。
struct rlimit {
rlim_t rlim_cur; /* soft limit : current limit */
rlim_t rlim_max; /* soft limit : current limit */
};
在更改资源限制时,须遵循下列三条规则:
(1). 任何一个进程都可将一个软限制值更改为小于或等于其硬限制值。
(2). 任何一个进程都可降低其硬限制值,但它必须大于或等于软限制值。这种降低对普通用户而言是不可逆的。
(3). 只有超级用户进程可以提高硬限制值。
常量RLIM_INFINITY指定了一个无限量的限制。这两个函数的resource参数取下列值之一。
资源限制影响到调用进程并由其子进程继承。
三 进程控制
进程标识符
每个进程都有一个非负整型表示的唯一进程ID。因为进程ID标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性。虽然是唯一的,但是进程ID可以重用,当一个进程终止后,其进程ID就可以再次使用了。大多数unix系统实现延迟重用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID。这防止了将新进程误认为是使用同一ID的某个已终止的先前进程。
系统中有一些专用的进程,但具体细节因实现而异。ID为0的进程通常是调度进程,常常被称为交换进程。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为交换进程。该进程是内核的一部分,它并不执行任何磁盘上的程序,因此也被称为系统进程。进程ID 1通常是init进程,在自举过程结束时由内核调用。该进程的程序文件在unix的早期版本中是 /etc/init, 在较新的版本中是/sbin/init 。此进程负责在自举内核后启动一个unix系统。init通常读与系统有关的初始化文件(/etc/rc* 文件或/etc/inittab文件,以及/etc/init.d中的文件),并将系统引导到一个状态(例如多用户)。init进程决不会终止。它是一个普通的用户进程(与交换进程不同,它不是内核中的系统进程),但是它以超级用户特权运行,进程ID 2是页守护进程,此进程负责支持虚拟存储系统的分页操
注意,这些函数都没有出错返回。
注意,这些函数都没有出错返回。
fork函数
#include <unistd.h>
pid_t fork( void );
返回值:子进程中返回0,父进程中返回子进程ID。出错返回-1
说明:
1.fork函数被调用一次,但返回两次,子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。
2.子进程是父进程的副本,子进程获得父进程数据空间 堆和栈的副本,而且是分开的,并不共享,但是正文段是父子进程共享的。
3.由于fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段 栈和堆的完全复制,而是使用了写时复制技术,即内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一“页”。
4.父进程的所有打开文件描述符都被复制到子进程中,父子进程的每个相同的打开描述符共享一个文件表项。除了打开文件外,父进程的很多其他属性也由子进程继承,包括:
父子进程之间的区别
5. 使fork失败的两个主要原因是:系统中已经有了太多的进程,或者该实际用户ID的进程总数超过了系统限制,CHILD_MAX规定了每个实际用户ID在任一时刻可具有的最大进程数。
6.fork有下面两种用法:
(1).一个父进程希望复制自己,使父子进程同时执行不同的代码段,这在网络服务进程中常见,父进程等待客户请求,子进程处理客户请求。
(2). 一个进程要执行一个不同的程序,这对shell是常见的情况。
vfork
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
返回值:子进程中为0,父进程中为子进程ID,出错为-1
说明:vfork与fork一样都是创建一个子进程,然后子进程调用exec或exit,但是在这之前子进程会完全借用父进程的环境在运行,父进程此时阻塞。
wait和waitpid
#include <sys/wait.h>
pid_t wait(int *statloc );
pid_t waitpid(pid_t pid, int *statloc, int options );
两个函数返回值:若成功则返回进程ID,0,若出错则返回-1
说明:
1.调用wait或waitpid的进程可能会发生什么情况:
2.wait会等待任一个结束的进程,而waitpid会等待指定的一个进程。
3.这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的内存单元内。如果不关心终止状态,则可将该参数指定为空指针。 posix.1规定终止状态用定义在<sys/wait.h>中的各个宏来查看:
4.对于wait,其唯一的出错是调用进程没有子进程。但是对于waitpid,如果指定的进程或进程组不存在,或者参数pid指定的进程不是调用进程的子进程则都将出错。
2.wait会等待任一个结束的进程,而waitpid会等待指定的一个进程。
3.这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的内存单元内。如果不关心终止状态,则可将该参数指定为空指针。 posix.1规定终止状态用定义在<sys/wait.h>中的各个宏来查看:
4.对于wait,其唯一的出错是调用进程没有子进程。但是对于waitpid,如果指定的进程或进程组不存在,或者参数pid指定的进程不是调用进程的子进程则都将出错。
5.options参数使我们能进一步控制waitpid的操作,此参数可以是0,或者下表常量按位“或”运算的结果:
6.waitpid函数提供了wait函数没有提供的三个功能:
(1). waitpid可等待一个特定的进程,而wait则返回任一终止子进程的状态。
(2). waitpid提供了一个wait的非阻塞版本。有时用户希望取得一个子进程的状态,但不想阻塞。
(3). waitpid支持作业控制(利用WUNTRACED 和 WCONTINUED选项)。
exec函数
四 进程组
进程组是一个或多个进程的集合。通常,它们与同一个作业相关联,可以接收来自同一个终端的各种信号。每个进程组有一个唯一的进程组ID。每个进程组都可以有一个组长进程,组长进程的标识是其进程组ID等于其进程ID。组长进程可以创建一个进程组,创建改组中的进程,然后终止。只要在某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否终止无关。从进程组创建开始到其中最后一个进程离开为止的时间区间称为进程组的生存期。进程组中的最后一个进程可以终止,或者转移到另一个进程组。
getpgrp getpgid setpgid
#include <unistd,h>
pid_t getpgrp(void);
返回值:调用进程的进程组ID
pid_t getpgid(pid_t pid);
返回值:若成功则返回进程组ID,若出错则返回-1
说明:若pid为0,则返回调用进程的进程组ID,于是 getpgid(0) 等价于 getpgrp();
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid );
返回值:若成功则返回0,若出错则返回-1
说明:
1.进程可以通过调用setpgid来加入一个现有的组或者创建一个新进程组。这个函数将pid进程的进程组ID设置为pgid。如果这两个参数相等,则由pid指定的进程变成进程组组长。如果pid是0,则使用调用者的进程ID。另外,如果pgid是0,则由pid指定的进程ID将用作进程组ID。
2. 一个进程只能为它自己或它的子进程设置进程组ID。在它的子进程调用了exec函数之一后,它就不再能改变该子进程的进程组ID。
五 会话
会话是一个或多个进程组的集合,例如如下图这个会话中有三个进程组:
setsid getsid
setsid getsid
#include <unistd.h>
pid_t setsid(void);
返回值:若成功则返回进程组ID,若出错则返回-1
说明:
1.如果调用此函数的进程不是一个进程组的组长,则此函数就会创建一个新会话,结果将发生下面3件事:
(1)该进程变成新会话首进程(会话首进程是创建该会话的进程)。此时,该进程是新会话中唯一的进程。
(2)该进程成为一个新进程组的组长进程,新进程组ID是该调用进程的进程ID。
(3)该进程没有控制终端,如果在调用setsid之前该进程有一个控制终端,那么这种联系也会被中断。
2. 如果该调用进程已经是一个进程组的组长,则此函数返回出错。为了保证不会发生这种情况,通常先调用fork,然后使其父进程终止,然后使其父进程终止,而子进程则继续。因为子进程继承了父进程的进程组ID,而其进程ID则是新分配的,两者不可能相等,所以这就保证子进程不会是一个进程组的组长。
#include <unistd.h>
pid_t getsid(pid_t pid);
返回值:若成功则返回会话首进程的进程组ID,若出错则返回-1
说明:如若pid是0,getsid返回调用进程的会话首进程的进程组ID。出于安全方面的考虑,某些实现会有如下限制:如若pid并不属于调用者所在的会话,那么调用者就不能得到该会话首进程的进程组ID。