转自:blog.youkuaiyun.com/minpro
进程控制
1、 进程表示
某些专用进程:
进程 ID 0 (调度进程 / 交换进程 swapper ):该进程并不执行任何磁盘上的程序——它是内核 的一部分,因此也被称为系统进程 。
进程 ID 1 ( init 进程 ):在自举过程结束时由内核调用。该进程的程序文件在 UNIX 的早期版本中是 etc/init ,在较新版本中是 sbin/init 。此进程负责在内核自举后启动一个 UNIX 系统。 Init 通常读与系统有关的初始化文件(/etc/rc* 文件),并将系统引导到一个状态(例如多用户)。 Init 进程决不会终止。是一个普通的用户进程 ,但是它以超级用户特权运行。
进程 ID 2 :在某些 UNIX 的虚存实现中,进城 ID 2 是页精灵进程 ( pagedaemon )。此进程负责支持虚存系统的请页操作。它是内核进程 。
除了进程 ID ,每个进程还有一些其它标识符。下列函数返回这些标识符:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid( void ); // 返回:调用进程的进程 ID
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
2、 fork 函数
父、子进程:
一个现存进程调用 fork 函数是 UNIX 内核创建一个新进程的唯一方法(以上的 0/1/2 进程除外,这些进程是由内核作为自举过程的一部分以特殊方式创建的)。
#include <sys/types.h>
#include <unistd.h>
pid_t fork( void ); // 返回:子进程中为 0 ,父进程中为子进程 ID ,出错为 -1
(该函数被调用一次,但返回两次)
子进程和父进程继续执行 fork 之后的指令。子进程是父进程的复制品。例如,子进程获得父进程数据空间、堆和栈的复制品。注意:这是子进程所拥有的拷贝。父、子进程并不共享这些存储空间部分。如果正文段是只读的,则父、子进程共享正文段。
现在很多的实现并不做一个父进程数据段和堆的完全拷贝,因为在 fork 之后经常跟随着 exec 。作为替代,使用了在写时复制( Copy-On-Write , COW ) 的技术。这些区域由父子进程共享,而且内核将它们的存取许可权改变为只读 的。如果有进程试图修改这些区域,则内核为有关部分,典型的是虚存系统中的“页”,做一个拷贝。
文件共享:
fork 的一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程每个相同的打开描述符共享一个文件表项。(文件表项:文件状态标志 / 当前文件位移 /v 节点指针)。这种共享文件的方式使父、子进程对同一文件使用了一个文件位移量。达到同步。
在 fork 之后处理文件描述符有两种常见的情况:
1) 父进程等待子进程完成。此时,父进程无需对其描述符做任何处理,当子进程终止后,它曾进行过读、写操作的人一共享描述符的文件位移量已做了相应更新。
2) 父、子进程各自执行不同的程序段。此时,在 fork 之后,父、子进程各自关闭它们不需使用的文件描述符,并且不干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。
使 fork 失败的两个主要原因是:
1) 系统中已经有了太多的进程(通常意味着某个方面除了问题)
2) 该实际用户 ID 的进程总数超过了系统限制。( CHILD_MAX 规定了每个实际用户 ID 在任一时刻可具有的最大进程数)
fork 有两种用法:
1) 一个父进程复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中常见——父进程等待委托者的服务请求。当这种请求到达时,父进程调用 fork ,使子进程处理此请求。父进程则继续等待下一个服务请求。
2) 一个进程要执行一个不同的程序。这对 shell 是常见的情况。此时,子进程在从 fork 返回后立即调用 exec。
附注:某些操作系统将 2 )中的两个操作 fork/exec 组合成一个,并称其为 spawn 。 UNIX 将这两个操作分开,因为很多场合需要单独使用 fork ,而并不跟随 exec 。另外,将这两个操作分开,使得子进程在 fork 和 exec 之间可以更改自己的属性。
3、 vfork 函数
vfork 函数用于创建一个新进程,而该新进程的目的是 exec 一个新程序。 vfork 并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用 exec( 或 exit) ,也就不会存访该地址空间。不过在子进程调用 exec 或 exit 之前,它在父进程的空间中运行。这种工作方式在某些 UNIX 的页式虚存实现中提高了效率。
与 fork 的另一个区别是: vfork 保证子进程先运行,在它调用 exec 或 exit 之后父进程才可能被调度运行。(如果再调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁)。
不常用,简单了解。
4、 exit 函数(进程的终止)
如果子进程正常终止,可以通过退出状态( exit status )参数来使父进程获得子进程的退出状态。如果子进程异常中止,其父进程可以用 wait 或 waitpid 函数取得其终止状态。
如果父进程在子进程之前终止 ,则子进程将由 init 进程领养 。其操作过程大致是:在一个进程终止时,内核逐个检查所有活动进程,以判断它是否是正要终止的进程的子进程,如果是,则该进程的父进程 ID 就更改为 1 ( init进程 ID )。这保证了每个进程都有一个父进程。
如果子进程在父进程之前终止 ,父进程是如何得到子进程的终止状态呢?——内核为每个终止子进程保存了一定量的信息,所以当终止进程的父进程调用 wait 或 waitpid 时,可以得到有关信息。这种信息至少包括进程 ID 、该进程的终止状态、以及该进程使用的 CPU 时间总量。(一个已经终止,但其父进程尚未对其进行善后处理(获取终止子进程的有关信息、释放它仍占用的资源)的进程成为 僵死进程( zombie ) 。 ps(1) 命令将僵死进程的状态打印为 Z )。(进程已经终止,但父进程尚未调用 wait 或者 waitpid 等取其状态,自进程就会变成僵死进程。)
一个由 init 领养的进程终止时, init 就会调用一个 wait 函数取得其终止状态。这防止了在系统中有很多僵死进程。
Exit 和 _exit 函数:
_exit 立即进入内核, exit 则先执行一些清楚处理,(包括调用执行各终止处理程序,关闭所有标准 I/O 流等),然后进入内核。
5、 wait 和 waitpid 函数
当一个进程正常或异常终止时,内核就向其父进程发送 SIGCHLD 信号。因为子进程终止是个异步事件(这可以在父进程运行的任何时候发生),所以这种信号也是内核向父进程发的异步通知。
调用 wait 或 waitpid 的进程可能会:
阻塞(如果其所有子进程都还在运行)—— wait 不到一个子进程的终止状态
带子进程的终止状态立即返回(如果一个子进程已终止,正等待父进程存取其终止状态)——顺利取到子进程的终止状态,返回
出错立即返回(如果它没有任何子进程)——出错,返回
#include <sys/types.h>
#include <sys/wait.h>
Pid_t wait(int *statloc); // 返回终止子进程的进程 ID
Pid_t waitpid(pid_t pid, int *statloc, int options); // 返回终止子进程的进程 ID
Wait:
如果一个进程已经终止,是一个僵死进程,则 wait 立即返回并取得该子进程的状态,否则 wait 使其调用者阻塞知道一个子进程终止。如果调用者阻塞而且它有多个子进程,则在任一子进程终止时, wait 就立即返回。
Statloc 是一个整型指针,或者为空(不关心终止状态),或者返回终止进程的终止状态。
Waitpid:
Waitpid 并不等待第一个终止的子进程,它有一选择项,可使调用者不阻塞,还有若干选择项,可以控制它所等待的进程。
Statloc 参数同 wait 。
Pid 参数:
Pid == -1 等待任一子进程。于是在这一功能方面 waitpid 与 wait 等效。
Pid > 0 等待其进程 ID 与 pid 相等的子进程。
Pid == 0 等待其组 ID 等于调用进程的组 ID 的任一子进程。
Pid < -1 等待其组 ID 等于 pid 的绝对值得任一子进程。
Options 参数:
或者是0或者下表中常数的逐位或运算。
常数 |
说明 |
WNOHANG |
若由 pid 指定的子进程并不立即可用,则 waitpid 不阻塞,此时其返回值为 0 |
WUNTRACED |
若某实现支持作业控制,则由 pid 指定的任一子进程状态已暂停,且其状态自暂停以来还未报告过,则返回其状态。 WIFSTOPPED 宏确定返回值是否对应于一个暂停子进程。 |
Waitpid 支持作业控制。
如何用 wait 等待一个指定的进程终止:
调用 wait ,然后将其返回的进程 ID 和所期望的进程 ID 比较。如果终止进程不是所期望的,则将该进程 ID 和终止状态保存起来,然后再次调用 wait 。反复直到所期望的进程终止。下一次又想等待一个特定进程时,先查看已终止的进程表,若其中已有要等待的进程,则取有关信息,否则调用 wait 。
Fork 两次以避免僵死进程(不懂)
POSIX.1 规定终止状态用定义在 <sys/wait.h> 中的各个宏来查看。有三个互斥的宏可用来取得进程终止的原因,它们的名字都以 WIF 开始。这三个宏中某一个为真,就可以利用一些其他的宏来取得终止状态、信号编号等。详见下表:
宏 |
说明 |
WIFEXITED(status) |
若为正常终止子进程返回的状态,则为真。对于这种情况可执行WEXITSTATUS(status) 取子进程传送给 exit 或 _exit 参数的低 8 位 |
WIFSIGNALED(status) |
若为异常终止子进程返回的状态,则为真(接到一个不捕捉的信号)。对于这种情况可执行 WTERMSIG(status) 取使子进程终止的信号编号。 另外, SVR4 和 4.3+BSD (但是,非 POSIX.1 )定义宏: WCOREDUMP(status) ,若已产生终止进程的 core 文件,则它返回真 |
WIFSTOPPED(status) |
若为当前暂停子进程的返回的状态,则为真。对于这种情况,可执行WSTOPSIG(status) 取使子进程暂停的信号编号 |
例:使用以上宏打印进程的终止状态。
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
void
pr_exit(int status)
{
if(WIFEXITED(status)) // 如果是正常终止,使用 WEXITSTATUS 取状态
printf("normal termination, exit status = %d/n", WEXITSTATUS(status));
else
if(WIFSIGNALED(status)) // 如果是异常终止
{
printf("abnormal termination, signal number = %d%s/n",
WTERMSIG(status), // 取进程终止的信号编号
#ifdef WCOREDUMP
WCOREDUMP(status)? "(core file generated)" : "" );
#else
"");
#endif
}
else
if(WIFSTOPPED(status)) // 如果是暂停
printf("child stopped, signal number = %d/n", WSTOPSIG(status));
}
测试以上程序:
int main(void)
{
pid_t pid;
int status;
if( (pid = fork()) < 0 )
printf("fork error");
else if( pid == 0)
exit(7);
if(wait(&status) != pid) /*wait for child*/
printf("wait error");
pr_exit(status); /*and print its status*/ // 打印正常终止
if( (pid = fork()) < 0)
printf("fork error");
else if( pid == 0) /*chid*/
abort(); /*generates SIGABRT*/
//abort 函数用于异常终止一个进程,并发送 SIGABRT 信号
if(wait(&status) != pid) /*wait for child*/
printf("wait error");
pr_exit(status); /*and print its status*/
if( (pid = fork()) < 0)
printf("fork error");
else if( pid == 0) /*child*/
status /= 0; /*divide by 0 generates SIGFPE*/
if(wait(&status) != pid) /*wait for child*/
printf("wait error");
pr_exit(status); /*and print its status*/
exit(0);
}
结果:
> printexit.exe
normal termination, exit status = 7
abnormal termination, signal number = 6(core file generated)
abnormal termination, signal number = 8(core file generated)
>
6、 竞态条件
当多个进程都企图对共享数据进行某种处理,而最后的结果又取决于进程运行的顺序时,则认为发生了竞态条件(race condition )。
如果一个进程希望等待一个子进程终止,则它必须调用 wait 函数。如果一个进程要等待其父进程终止,则可使用下列形式的循环:
While( getpid() != 1 )
Sleep(1);
这种形式的循环(称为定期询问( polling )),浪费了 CPU 时间,因为调用者每隔 1 秒都被唤醒,然后进行条件测试。
为了避免竞态条件和定期询问,在多个进程之间需要有某种形式的信号机制。
( fork() 函数的返回:在子进程中为 0 ,父进程中为子进程 ID ,出错为 -1 )
例:具有竞态条件的程序
#include <sys/types.h>
#include <stdio.h>
static void
charatatime(char *str)
{
char *ptr;
char c;
ptr = str;
setbuf( stdout, NULL );
for(c=*ptr; c=*ptr++;)
putc(c, stdout);
}
以上函数中将标准输出设置为不带缓存的,于是每个字符输出都需调用一次 write 。
这样可以使内核尽可能多次地在两个进程之间进行切换,以例示竞态条件。
int
main(void)
{
pid_t pid;
if( (pid = fork()) < 0)
printf("fork error"); // 出错
else
if( pid == 0) // 在子进程中返回
{
charatatime("output from child /n");
}
Else // 在父进程中返回
{
charatatime("output from parent /n");
}
exit(0);
}
结果:
> fork.exe
output from child
output from parent
> fork.exe
outputo utpuft from child
rom parent
>
7、 Exec 函数
用 fork 可以创建新的进程,用 exec 可以执行新的程序, exit 函数可以处理终止, wait 可以等待终止,这些是我们需要的基本的进程控制原语。
当进程调用一种 exec 函数时,该进程完全由新程序代换,而新程序则从其 main 函数开始执行。因为调用 exec 并不创建新进程,所以前后的进程 ID 并未改变。 Exec 只是用另一个新程序替换了当前进程的正文、数据、堆和栈段。
六种不同的 exec 函数如下:
#include <unistd.h>
Int execl( const char *pathname, const char *arg0, … /* ( char *)0 */ );
Int execv( const char *pathname, char *const argv[] );
Int execle(const char *pathname, const char *arg0, … /* ( char *)0, char *const envp[] */ );
Int execve( const char *pathname, char *const argv[],char *const envp[] );
Int execlp( const char *filename, const char *arg0, … /* ( char *)0 */ );
Int execvp( const char *filename, char *const argv[] );
参数:
p :表示该函数取 file 那么作为参数,并且用 PATH 环境变量寻找可执行文件。
l :( list )表示该函数取一个参数表,与 v 互斥。将新程序的每个命令行参数都说明为一个单独的参数。参数表以空指针结尾。
v :( vector )表示该函数取一个 argv[] 。先构造一个指向各参数的指针数组,然后将该数组地址作为着三个函数的参数。
e :表示该函数取 envp[] 数组,而不使用当前环境。传递一个指向环境字符串指针数组的指针。否则使用调用进程中的 environ 变量为新程序复制现存的环境。
filename 参数:如果 filename 中包含 / ,则视为路径名。否则按 PATH 环境变量,在有关目录中搜寻可执行文件。如果找到了一个可执行文件,但不是由连接编辑程序产生的机器可执行代码文件,则认为是一个 shell 脚本,于是试着调用 /bin/sh ,并以该 filename 作为 shell 的输入。
8、 解释器文件
#! Pathname [optional-argument]
Pathname 是个绝对路径名
对这种文件的识别是由内核作为 exec 系统调用处理的一部分来完成的。内核使调用 exec 函数的进程实际执行的文件并不是该解释器文件,而是在该解释器文件的第一行中 pathname 所指定的文件。
解释器文件:文本文件,以 #! 开头
解释器:由该解释器文件第一行中的 pathname 指定
很多系统对解释器文件第一行有常读限制( 2 个字符)。
示例: #! /bin/sh
解释器 / 解释器文件 / 执行解释器文件的程序:
解释器:(回送每一个命令行参数)
///home/liuna/test/printargv/printargv.exe 是一个解释器,即一个可执行程序
#include <stdio.h>
int
main(int argc, char *argv[])
{
int i;
for(i=0; i<argc; i++)
{
printf("argv[%d]: %s/n", i, argv[i]);
}
exit(0);
}
// 当执行一个程序时,调用 exec 的进程可将命令行参数传递给该信程序。
解释器文件:
///home/liuna/test/awk/awk.c 就是一个执行解释器的脚本文件。如果用 awk 做解释器,那么解释器文件的第一行典型如下: #! /bin/awk
#! /home/liuna/test/printargv/printargv.exe foo
执行解释器文件的程序:
我们编写一个程序来执行解释器文件,并传递一些参数:
///home/liuna/test/execawk/execawk..exe
#include <sys/types.h>
#include <stdio.h>
int main(void)
{
pid_t pid;
if((pid = fork()) < 0)
printf("fork error");
else
if(pid == 0)
{
if(execl("/home/liuna/test/awk/awk.c",
"awk", "my argv1", "MY ARGV2", (char *)0 ) < 0)
printf("execl error");
}
if( waitpid(pid, NULL, 0) < 0)
printf("wait error");
exit(0);
}
执行以上可执行程序,得到如下结果:
> execawk.exe
argv[0]: /home/liuna/test/printargv/printargv.exe
argv[1]: foo
argv[2]: /home/liuna/test/awk/awk.c
argv[3]: my argv1
argv[4]: MY ARGV2
>
解析:当内核 exec 该解释器( printargv.exe )时, argv[0] 是该解释器的 pathname , argv[1] 是解释器文件( awk.c)中的可选参数 foo ,其余参数是 pathname ( /home/liuna/test/awk/awk.c ) ,以及第二个和第三个参数( my argv1和 MY ARGV2 )。注意:内核取 execl 中的 pathname 代替第一个参数( awk ),因为一般 pathname 包含了较第一个参数更多的信息。( 什么意思???)
效率 / 开销 / 原理,都还不清楚!!!
9、 system 函数
#include <stdlib.h>
Int system( const char *cmdstring);
如果 cmdstring 是一个空指针,则仅当命令处理程序可用时, system 返回非 0 值,这一特征可决定在一个给定的操作系统上是否支持 system 函数。在 UNIX 中, system 总是可用的。
因为 system 在其实现中调用了 fork 、 exec 和 waitpid ,因此有三种返回值:
如果 fork 失败或者 waitpid 返回除 EINTR 之外的出错,则 system 返回 -1 ,而且 errno 中设置了错误类型。
如果 exec 失败(表示不能执行 shell ),则其返回值如同 shell 执行了 exit(127) 一样。
否则所有三个函数都成功,并且 system 的返回值是 shell 的终止状态,其格式已在 waitpid 中说明。
以下是 system 函数的一种实现,它对信号没有进行处理:
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
// 这仅仅是 system 的一种实现方式,其实 system 是一个系统调用,用来执行命令字符串
// 所以,下面这个 system 函数完全可以改成另一个名字。
int
system(const char *cmdstring) /*version without signal handling*/
{
pid_t pid;
int status;
if(cmdstring == NULL)
return(-1);
if((pid = fork()) < 0)
status = -1;
else
if(pid == 0)
{
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0 );
_exit(127); /*execl error*/
}
else
{
while( waitpid(pid, &status, 0) < 0)
if(errno != EINTR)
{
status = -1;
break;
}
}
return(status);
}
如果 waitpid 由一个捕捉到的信号中断,则 system 很多当前的实现都返回一个错误( EINTR )。
Shell 的 -c 选项告诉 shell 程序取下一个命令行参数(即 cmdstring )作为命令输入,而不是从标准输入或者从一个给定的文件中读入命令。
如果不使用 shell 执行此命令,而是试图由我们自己去执行,那么将相当困难。首先,我们必须用 execlp 而不是 execl,像 shell 那样使用 PATH 变量。我们必须将 null 符结尾的命令字符串分成各个命令行参数,以便调用 execlp 。最后,我们也不能使用任何一个 shell 元字符。( 不明白 )
注意:我们调用 _exit 而不是 exit 。这是为了防止任一标准 I/O 缓存在子进程中被刷新。(这些缓存会在 fork 中由父进程复制到子进程)( exit 在调用时不是直接进入内核,而是要做一些清理工作)
测试上述 system 的实现:
int
main(void)
{
int status;
if( (status = system("date")) < 0)
printf("system() error");
pr_exit(status);
if( (status = system("nosuchcommand")) < 0)
printf("system() error");
pr_exit(status);
if( (status = system("who; exit 44")) < 0)
printf("system() error");
pr_exit(status);
exit(0);
}
结果:
> systemtest.exe
Thu Jan 31 06:56:08 MST 2008
normal termination, exit status = 0
sh: nosuchcommand: not found.
normal termination, exit status = 127
root pts/1 Jan 31 19:22
liu pts/tl Jan 31 06:50
normal termination, exit status = 44
>
总结:
这一章有很多东西读不懂了,所以只摘录了一些工作中用到的部分,好多不懂部分以后慢慢体会了。