每个进程都有一个非负整型表示的唯一进程ID,因为进程ID标识符总是唯一的,常将其用作其他标识符的一部分以保证其唯一性。虽然是唯一的,但是进程ID是可服用的,当一个进程终止后,其进程ID就成为服用的候选者。
系统中有一些专用的进程:
ID为0的进程通常是调度进程(常常被称为交换进程swapper)。该进程是内核的一部分,它不执行任何磁盘上的程序。
ID为1通常是init进程。此进程负责在自举内核后启动一个UNIX系统。init通常读与系统有关的初始化文件,并将系统引导到一个状态。init进程绝不会终止,它是一个普通的用户进程,但是它以超级用户特权运行。
<span style="font-family:Microsoft YaHei;font-size:18px;">#include <unistd.h> pid_t getpid(void) ; //获取调用进程的进程ID,失败返回-1,错误原因存储在errno中 pid_t getppid(void) ; //获取调用进程的父进程ID uid_t getuid(void) ; //获取调用进程的实际用户ID uid_t geteuid(void); //获取调用进程的有效用户ID gid_t getgid(void) ; //获取调用进程的实际组ID gid_t getegid(void); //获取调用进程的有效组ID</span>
1、进程真实用户号(RUID)
对于进程而言,创建该进程的用户UID(执行此程序的用户)为此程序真实用户号(RUID)。
2、进程有效用户号(EUID)
EUID主要用于权限检查。多数情况下,EUID和UID相同。如果执行文件的setuid位有效,则该文件的拥有者之外的用户执行该程序,EUID和UID不相同;即当某可执行文件设置了setgid位后,任何用户(包括root用户)运行此程序时,其有效用户组EUID为该文件的拥有者。
3、进程用户组号(GID)
创建进程的用户所在的组号为该进程的用户组号(GID)。
4、有效进程用户组号(EGID)
多数情况下,EGID和GID相同。如果执行文件的setuid位有效,则该文件的拥有者之外的用户执行该程序,EGID和GID不相同;即当某可执行文件设置了setgid位后,任何用户(包括root用户)运行此程序时,其有效用户组EGID为该文件的拥有者。
<span style="font-family:Microsoft YaHei;font-size:18px;">#include <unistd.h> pid_t fork(void); //返回值:子进程返回0,父进程返回子进程ID;若出错,返回-1</span>
fork函数创建子进程的过程为:在进程调用fork时,系统根据现有的进程的特性几乎是完全意义的复制出一个新的子进程,复制的内容包括原进程的当时内存空间的代码、变量、对象等所有内存状态,真实和有效uid和gid,环境、资源限制、打开的文件等。通过这种复制方式创建出子进程后,原有进程和子进程都从函数fork返回,各自继续往下运行,但是原进程的fork返回值于子进程的fork返回值不同,在原进程中,fork返回子进程的pid,而在进程中,fork返回0,如果fork返回负值,表示创建子进程失败。
<span style="font-family:Microsoft YaHei;font-size:18px;">#include<unistd.h> #include<stdio.h> #include<errno.h> #include<stdlib.h> int main() { pid_t pid ; int x = 1 ; pid = fork() ; if (pid == 0) /*Child*/ { printf("child : x=%d\n", ++x) ; exit(0) ; } /*Parent*/ printf("parent :x=%d\n", --x) ; exit(0) ; } //执行结果: //parent: x=0 //child: x=2</span>
注意:
并发执行。父进程和子进程是并发运行的独立进程。内核能以任何方式交替执行它们的逻辑控制流中的指令。(故此程序的执行结果在不同的系统上可能相反)
相同的但是独立的地址空间。因为父进程和子进程是独立的进程,它们都有自己的私有地址空间。父进程和子进程对x所做的任何改变都是独立的,不会反映在另一个进程的存储器中。
共享文件。当运行这个示例程序时,我们注意到父进程和子进程都把它们的输出显示在屏幕上。原因是子进程继承了父进程所有的打开文件。当父进程调用fork时,stdout文件是被打开的,并指向屏幕。子进程继承了这个文件,因此它的输出也是指向屏幕的。
fork的特性是父进程的数据段、BBS段、代码段、堆空间、栈空间和文件描述符都被复制到子进程中,而父进程和子进程每个相同的打开描述符共享一个文件表项。父子进程对于局部变量(即栈空间)执行复制操作,而对文件描述符的文件表项信息(文件的读写位置)则是共享使用的。
vfork:
vfork函数用于创建一个新进程,而该新进程的目的是exec一个新程序。
vfork与fork一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会引用该地址空间。
vfork和fork之间另一个区别是:vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度执行,当子进程调用这两个函数中的任意一个时,父进程会恢复运行。(如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁)。
当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。
<span style="font-family:Microsoft YaHei;font-size:18px;">#include <unistd.h> extern char **environ; int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);//若成功,不反回。若出错,返回-1。</span>其中只有execve是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。
<span style="font-family:Microsoft YaHei;font-size:18px;">#include <sys/wait.h> pid_t wait(int *statloc); pid_t waitpid(pid_t pid, int *static, int options); //两个函数返回值:若成功,返回进程ID。若出错,返回0或-1</span>这两个函数的参数statloc是一个整型指针。如果statloc不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。如果不关心终止状态,则可将该参数指定为空指针。
依据传统,这两个函数返回的整型状态字是由实现定义的。其中某些位表示退出状态(正常返回),其他位则指示信号编号(异常返回),有一位指示是否产生了一个c o r e文件等等。P O S I X . 1规定终止状态用定义在< s y s / w a i t . h >中的各个宏来查看。有三个互斥的宏可用来取得进程终止的原因,它们的名字都以W I F开始。基于这三个宏中哪一个值是真,就可选用其他宏来取得终止状态、信号编号等。
waitpid:
对于waitpid的pid参数的解释与其值有关:
(1)pid == -1 等待任一子进程。于是在这一功能方面w a i t p i d与w a i t等效。
(2)pid > 0 等待其进程I D与p i d相等的子进程。
(3)pid == 0 等待其组I D等于调用进程的组I D的任一子进程。
(4)pid < -1 等待其组I D等于p i d的绝对值的任一子进程。
waitpid返回终止子进程的进程ID,而该子进程的终止状态则通过statloc返回。对于wa t,其唯一的出错是调用进程没有子进程(函数调用被一个信号中断时,也可能返回另一种出错。)。但是对于waitpid,如果指定的进程或进程组不存在,或者调用进程没有子进程都能出错。
options参数使我们能进一步控制waitpid的操作。此参数或者是0,或者是表中常数的逐位或运算。
watipid函数提供了wait函数没有提供的三个功能:
(1) waitpid等待一个特定的进程(而wait则返回任一终止子进程的状态)。
(2) waitpid提供了一个wait的非阻塞版本。有时希望取得一个子进程的状态,但不想阻塞。
(3) waitpid支持作业控制(以WUNTRACED选择项)。
正常终止:
(1)从main返回;
(2)调用exit;
(3)调用_exit或_Exit;
(4)从最后一个线程调用pthread_exit;
(5)最后一个线程从其启动例程返回;
异常终止:
(6)调用abort;
(7)接到一个信号;
(8)最后一个线程对取消请求做出响应;
3个函数用于正常终止一个程序:_exit和_Exit立即进入内核,exit则先执行一些清除处理(包括调用执行各终止处理程序,关闭所有标准I / O流等),然后进入内核。
<span style="font-family:Microsoft YaHei;font-size:18px;">#include <stdlib.h> void exit(int status) ; void _Exit(int status); #include <unistd.h> void _exit (int status) ;</span>三个函数都带一个整型参数,称之为终止状态( exit status)。
按照ANSI C的规定,一个进程可以登记多至32个函数,这些函数将由exit自动调用。我们称这些函数为终止处理程序(exit handler),并用atexit函数来登记这些函数。
<span style="font-family:Microsoft YaHei;font-size:18px;">#include <stdlib.h> int atexit(void (*func) (void)) ; //返回:若成功则为0,若出错则为非0</span>其中, atexit的参数是一个函数地址,当调用此函数时无需向它传送任何参数,也不期望它返回一个值。exit以登记这些函数的相反顺序调用它们。同一函数如若登记多次,则也被调用多次。
注意,内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显式或隐式地(调用exit)调用_ exit。进程也可非自愿地由一个信号使其终止(图7 - 1中没有显示)。