Unix系统实现延迟重用算法,使得赋予新建进程的ID不同于最近终止进程所使用的ID。防止了将新进程误认为是使用同一ID的某个已终止的先前进程。
ID为0的进程通常是调度进程,因此也被称为系统进程。
ID为1的通常是init进程,在自举过程结束时由内核调用。早期版本中是/etc/init,新版本中是/sbin/init,此进程负责在自举内核后启动一个unix系统。init通常读与系统有关的初始化文件(/etc/rc*文件或/etc/inittab文件,以及/etc/init.d中的文件,并将系统引导到一个状态。init进程绝不会终止。它是一个普通的用户进程,但是它以超级用户特权运行。
ID为2是页守护进程。负责分页操作。
pid_t getpid(void)// 返回调用进程的进程ID
pid_t getppid(void)// 返回进程的父进程ID
uid_t getuid(void)// 调用进程的实际用户ID
uid_t getuid(void)// 调用进程的有效用户ID
gid_t getgid(void)// 调用进程的实际组ID
gid_t getgid(void)// 调用进程的有效组ID
-----------------------------------------------------------------------------------
#include<unistd.h>
pid_t fork(void) 子进程中返回0,父进程中返回子进程ID,出错返回-1
fork被调用一次,返回两次。两次返回的唯一区别是子进程的返回值是0,而父进程的返回值则是新子进程的进程ID。
子进程是父进程的副本,获得父进程数据空间,堆和栈的副本。这是子进程所拥有的副本。父,子进程并不共享这些存储空间部分,但共享正文段。
由于fork之后经常跟随exec,所以现在的很多实现并不执行一个父进程数据段,栈和堆的完全复制。作为替代,使用了写时复制技术。这些区域由父子进程共享,而且内核将它们的访问权限改变为只读的。如果父子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是“一页”
在重定向父进程的标准输出时,子进程的标准输出也被重定向。父子进程共享一个文件表项。
fork进程用法
1)父进程复制自己。使父子进程同时执行不同的代码段。比如网络服务进程,父进程监听,收到请求后复制自己的。父进程继续等待。
2)一个进程要执行一个不同的程序。
---------------------------------------------------------------------------------------
vfork创建一个新进程,而该新进程的目的是exec一个新程序。并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec,于是也就不会存访该地址空间。
vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程,则会死锁。
---------------------------------------------------------------------
exit
1)main函数内执行return。等效于调用exit。
2)调用exit,操作包括调用各终止处理程序,然后关闭所有标准I/O流等。
3)调用_exit或_Exit函数,为进程提供一种无需运行终止处理程序或信号处理程序而终止的方法。对标准I/O流是否进行冲洗,取决于实现。
不管进程如何终止,最后都会执行内核中的同一段代码。这段代码为相应进程关闭所有打开描述符,释放它所使用的存储器等。
对于exit,_exit,_Exit,将其退出状态作为参数传送给函数。在异常终止情况下,内核产生一个指示其异常终止原因的终止状态。在任意一种情况下,该终止进程的父进程都能用wait或waitpid函数取得其终止状态。
父进程提前终止,其子进程由init领养。
过程:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止进程的子进程,如果是,则将该进程的父进程ID更改为1。
内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程wait或waitpid时,可以得到这些信息。这些信息至少包括进程ID,该进程的终止状态,以及该进程使用的CPU时间总量。内核可以释放终止进程所使用的所有存储去,关闭其所有打开文件。
一个已经终止,但是其父进程尚未对其进行善后处理的进程称为僵死进程。除非父进程等待取得子进程的终止状态,否则这些子进程终止后就会变成僵死进程。
-------------------------------------------------------------------------
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options)
当一个进程正常或异常终止时,内核就向其父进程发送SIGCHLD信号。因为子进程终止是个异步事件,所以这种信号也是内核向父进程发的异步通知。父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用执行的函数
调用wait或waitpid的进程可能会发生什么情况:
1)如果其所有子进程都还在运行,则阻塞。
2)如果一个子进程已终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回。
3)如果它没有任何子进程,则立即出错返回。
如果进程由于接收到SIGCHLD信号而调用wait,则可期望wait会立即返回。如果在任意时刻调用wait,则进程可能会阻塞。
wait函数在一个子进程终止前,wait使其调用者阻塞,而waitpid有一个选项,可使调用者不阻塞。waitpid并不等带在其调用之后的第一个终止子进程,它有若干选项,可以控制它所等待的进程。如果一个子进程已经终止,并且是一个僵死进程,则wait立即返回并取得该子进程的状态,否则wait使其调用者阻塞直到一个子进程终止。如调用者阻塞而且它有多个子进程,则在其一个子进程终止时,wait就立即返回。因为wait返回终止子进程的进程ID。
如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则指定为空指针。
函数返回的整型状态字由实现定义,其中某些为表示退出状态,其他位则指示信号编号,有一位指示是否产生了一个core文件等。有4个互斥的宏用来取得进程终止原因。
waitpid可以等待一个特定进程。
pid==-1等待任一子进程
pid>0等带pid
pid==0等待其组ID等于调用进程组ID的任一子进程。
pid<=-1等待其组ID等于pid绝对值的任一子进程。
waitpid函数返回终止子进程的进程ID,并将该子进程的终止状态存放在由statloc指向的存储单元中。如果指定的进程不存在,或不属于调用进程,将出错。
options参数:
WCONTINUED 若实现支持作业控制,那么由pid指定的任一子进程在暂停后已经继续。但其状态尚未报告,则返回其状态
WNOHANG若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,此时其返回值为0
WUNTRACED若某实现支持作业控制,而由pid指定的任一子进程已处于暂停状态,并且其状态自暂停以来还未报告过,则返回其状态。
如果一个进程fork一个子进程,但不要它等待子进程终止,也不希望子进程处于僵死状态直到父进程终止,实现这一要求的技巧是调用fork两次。
#include<sys/wait.h>
int main(void)
{
pid_t pid;
if((pid = fork())< 0)
{
//error
}
else if (pid == 0)
{
// in first child
if((pid=fork())<0)
{
// errr
}
else if (pid > 0)
exit(0) // the parent of second child
//
sleep(2)
// second child, it parent become init.
exit(2)
}
}
----------------------------------------------------------------------
#include <sys/wait.h>
int waitid(idtype_t idtype, it_t id, siginfo_t *infop, int options);
使用单独的参数表示要等待的子进程的类型,而不是将此与进程ID或进程组ID组合成一个参数。id参数的作用与idtype的值相关。
idtyp:
P_PID 等待一个特定的进程,id包含要等待的子进程的进程ID
P_PGID等待一个特定进程组中的任一个进程:id包含要等待子进程的进程组ID
P_ALL等待任一子进程:忽略id
options:指示调用者关注那些状态变化。
WCONTINUED等待一个进程,它以前曾被暂停,此后又已继续,但其状态尚未报告
WEXITED等待已退出的进程
WNOHANG如无可用的子进程退出状态,立即返回而非阻塞
WNOWAIT不破坏子集成退出状态,该子进程退出状态可由后续的wait,waitid或waitpid调用取得。
WSTOPPED等待一个进程,它已经暂停,但其状态尚未报告。
info:包含了有关引起子进程状态改变的生成信号的详细信息。
-----------------------------------------------------------------------------------
#include<sys/types.h>
#include<sys/wati.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)
rusage返回由终止进程及其所有子进程使用的资源汇总
---------------------------------------------------------------------------------
竞争条件
如果一个进程希望等待父进程终止,可使用下列形式循环
while(getppid() != 1) sleep(1);//词方法浪费cpu时间
TELL_PARAENT(getppid())
WAIT_PARENT();
TELL_CHILD(pid);
WAIT_CHILD();
---------------------------------------------------------------
#include<unistd.h>
int execl(const char *pathname, const char *arg0, ...);
int execv(const char *pathname, char *const argv[]);
int execle(const char * pathname, const char *arg0,...);
int execve(const char * pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0,...);
int execvp(const char *filename, char *const argv[]);
区别1:
前四个取路径名作为参数,后两个则取文件名作为参数。当指定fiename作为参数时:
如果filename中包含/,则将其视为路径名。
否则就按PATH环境变量,在它所指定的各目录中搜寻可执行文件。
如果execlp或execvp使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则认为该文件是一个shell脚本,于是试着调用/bin/sh,并以该filename作为shell的输入。
区别2:
l表示list,v表示矢量vector,函数execl,execlp和execle要求将新程序的每个命令行参数都说明为一个单独的参数。这种参数表以空指针结尾。对于另外3个函数,则应先构造一个指向各参数的指针数组,然后将该数组地址作为这三个函数的参数。
execl,execle,execlp三个函数命令行参数的一般方法是
char *arg0, char *arg1, ..., char *argn, (char*)0
在最后一个命令行参数之后跟了一个空指针。如果用常数0来表示一个空指针,则必须将它强制装环卫一个字符指针,否则将被解释为整型参数。如果一个整型数的长度与char*的长队不同,那么exec函数的实际参数就将出错。
区别3
以e结尾的两个函数execle,execve可以传递一个指向环境字符串指针数组的指针。其他四个函数则使用调用进程中的environ变量为新程序复制现有的环境。
字母p表明该函数取filename作为参数,并且用PATH环境变量寻找可执行文件。
字母l表示函数取一个参数表,与v互斥。
字母v表示取一个argv[]矢量。
字母e表示取envp[]数组,而不使用当前环境。
只有execve是内核的系统掉用,其他5个只是库函数,他们最终都要调用该系统调用。
-----------------------------------------------------------------------------------
#include <unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
更改用户ID和组ID
总是试图使用最小特权模型
改变用户ID的规则:
1)若进程具有超级用户特权,则setuid函数将实际用户ID,有效用户ID,以及保存的设置用户ID设置为uid
2)若进程没有超级用户特权,但是uid等于实际用户ID或保存的设置用户ID,则seuid只将有效用户ID设置为uid。不改变实际用户ID和保存的设置用户ID
3)如果上面两个条件都不满足,则将errno设置为EPERM,返回-1。
_POSIX_SAVED_IDS提供设置用户ID功能。
内核维护的三个用户ID
1)只有超级用户进程可以更改实际用户ID。实际用户ID是在用户登录时,由login(1)程序设置的,而且永远不会改变它。因为login释义个超级用户进程,当它调用setuid时,会设置所有三个用户ID
2)仅当对程序文件设置了设置用户ID位时,exec函数才会设置有效用户ID。如果设置用户ID位没有设置,则exec函数不会改变有效用户ID,而将其维持为原先值。任何时候都可以调用setuid,将有效用户ID设置为实际用户ID或保存的设置用户ID。
3)保存的设置用户ID是由exec复制有效用户ID而得来的。如果设置了文件的设置用户ID位,则在exec根据文件的用户ID设置了进程的有效用户ID以后,就将这个副本保存起来。
#include<unistd.h>
int setreuid(uid_t ruid, uid_t euid)
int setregid(gid_t rgid, gid_t egid)
交换实际用户ID和有效用户ID的值。如果其中任一参数的值为-1,则表示相应的ID应当保持不变。
规则:一个非特权用户总能交换实际用户ID和有效用户ID。这就允许一个设置用户ID程序转换成只具有用户的普通权限,以后又可再次转换回设置用户ID所得到的额外权限。
#include<unistd.h>
int seteuid(uid_t uid)
int setegid(gid_t gid)
一个非特权用户可将其有效用户ID设置为其实际用户ID或保存的设置用户ID。对于一个特权用户,则可将有效用户设置为uid。
-------------------------------------------------------------------
解释器文件
开始行的形式是:
#! pathname [optional-argument]
------------------------------------------------------------------
#include <stdlib.h>
int system(const char*cmdstring)
如果cmdstring是一个空指针,则仅当命令处理程序可用时,system返回非0值,这一特征可以确定在一个给定的操作系统上是否支持system函数。
system返回值:
1)如果fork失败或者waitpid返回除EINTR之外的出错,则system返回-1,而且errno设置错误类型值
2)如果exec失败,则其返回值如同shell执行了exit(127)一样。
3)否则所有三个函数forl,exec,waitpid都执行成功,并且system返回值是shell终止状态。
如果在一个设置用户ID程序中调用system,system将继承程序ID
----------------------------------------------------------------------
进程会计
每当进程结束时,内核就写一个会计记录。
acct
会计记录对应于进程而不是程序。在fork之后,内核为子进程初始化一个记录,而不是在一个新程序被执行时做这项工作。
----------------------------------------------------------------
用户标识
#include<unistd.h>
char *getlogin(void)
返回指向登录名字字符串的指针。
--------------------------------------------------------------
进程时间
#include<sys/times.h>
clock_t times(struct tms *buf)
返回流逝的墙上时钟时间。