进程和信号 (1)

来源:互联网 发布:淘宝充值平台怎么样 编辑:程序博客网 时间:2024/06/04 18:30

2011-10-07 wcdj


BLP 4th P.388

知识点:
(1) 进程的结构、类型和调度
(2) 用不同的方法启动新进程
(3) 父进程、子进程和僵尸进程

(4) 什么是信号以及如何使用它们


1 什么是进程


UNIX标准(特别是IEEE Std 1003.1, 2004年版)把进程定义为:“一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源。”

2 进程的结构


(1) 进程标识符(PID),通常是一个取值范围从2到32768的正整数。当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的PID,当数字已经回绕一圈时,新的PID重新从2开始。数字1一般是为特殊进程init保留的,init进程负责管理其他进程。
(2) 程序代码是以只读方式加载到内存中的。虽然不能对这个区域进行写操作,但它可以被多个进程安全地共享。系统函数库也可以被共享。
(3) 并不是程序在运行时所需要的所有东西都可以被共享。
(4) 在许多Linux系统(也包括一些UNIX系统)上,在目录/proc中有一组特殊的文件,这些文件的特殊之处在于它们允许你“窥视”正在运行的进程的内部情况,就好像这些进程是目录中的文件一样。
(5) 因为Linux和UNIX有一个虚拟内存系统,能够把程序代码和数据以内存页面的形式放到硬盘的一个区域中,所以Linux可以管理的进程比物理内存所能容纳的要多得多。
(6) 进程表。它把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的PID、进程的状态、命令字符串和其他一些ps命令输出的各类信息。操作系统通过PID对进程进行管理。进程表的长度是有限制的,所以系统能够支持的同时运行的进程数也是有限制的。早期的UNIX系统只能同时运行256个进程。最新的实现版本已大幅度放宽这一限制,可以同时运行的进程数可能只与用于建立进程表项的内存容量有关,而没有具体的数字限制了。
(7) 查看进程命令:ps -ef
(8) 进程调度。Linux内核用进程调度器来决定下一个时间片应该分配给哪个进程。它的判断依据是进程的优先级。优先级高的进程运行得更为频繁。在Linux中,进程的运行时间不可能超过分配给它们的时间片,它们采用的是抢占式多任务处理,所以进程的挂起和继续运行无需彼此之间的协作。操作系统根据进程的nice值来决定它的优先级,一个进程的nice值默认为0并将根据这个程序的表现而不断变化。长期不间断运行的程序的优先级一般会比较低,而(例如)暂停来等待输入的程序会得到奖励。可以用nice命令设置进程的nice值,使用renice命令调整它的值。nice命令是将进程的nice值增加10,从而降低该进程的优先级。可以用 ps -l 查看正在运行的进程的nice值。

3 启动新进程


3.1 启动新进程的三种方法:

方法1:通过库函数system
可以在一个程序的内部启动另一个程序,从而创建一个新进程。
#include <stdlib.h>
int system(const char *string);
system函数的作用是,运行以字符串参数的形式传递给它的命令并等待该命令的完成。命令的执行情况如同在shell中执行如下的命令:
$ sh -c string
如果无法启动shell来运行这个命令,system函数将返回错误代码127;如果是其他错误,则返回-1。否则,system函数将返回该命令的退出码。
注意:
(1) system函数很有用,但它也有局限性,因为程序必须等待由system函数启动的进程结束之后才能继续,因此我们不能立刻执行其他任务。
(2) 通过 system("ps ax &"); 可以把shell启动的程序放到后台执行,这样对system函数的调用将在shell命令结束后立刻返回。但是这样依然存在问题,就是无法控制输出的行为。如果想要用好进程,我们就需要能够对它们的行为做更细致的控制。
(3) 一般来说,使用system函数远非启动其他进程的理想手段,因为它必须使用一个shell来启动需要的程序。由于在启动程序之前需要先启动一个shell,而且对shell的安装情况以及使用的环境的依赖也很大,所以使用system函数的效率不高。因此,应该优先考虑其他方法。

方法2:exec函数簇 —— 替换进程映像
exec系列函数由一组相关的函数组成,它们在进程的启动方式和程序参数的表达方式上各有不同。exec函数可以把当前进程替换为一个新进程,新进程由path或file参数指定。你可以使用exec函数将程序的执行从一个程序切换到另一个程序。
注意:
(1) exec函数比system函数更有效,因为在新的程序启动后,原来的程序就不再运行了。
(2) 新进程的PID、PPID和nice值与原先的完全一样。事实上,这里发生的一切其实就是,运行中的程序开始执行exec调用中指定的新的可执行文件中的代码。
(3) 对于由exec函数启动的进程来说,它的参数表和环境加在一起的总长度是有限制的。上限由ARG_MAX给出,在Linux上它是128KB。POSIX规范要求ARG_MAX至少要有4KB。
(4) 一般情况下,exec函数是不会返回的,除非发生了错误。出现错误时,exec函数将返回-1,并且会设置错误变量errno。(需要包含头文件 errno.h)
(5) 由exec启动的新进程继承了原进程的许多特性。(这里需要参考其他资料)

函数原型:
#include <unistd.h>
execl、execlp 和 execle 的参数个数是可变的,参数以一个空指针结束。execv 和 execvp 的第二个参数是一个字符串数组。这两种情况,新程序在启动时会把在argv数组中指定的参数传递给main函数。(传递的参数从 argv[1] 开始)


(1) 可变参数
int execl(const char *path, const char *arg0, ..., (char *)0);
int execlp(const char *file, const char *arg0, ..., (char *)0);
int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);

(2) 字符串数组
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[]);


execl的例子:在prog1程序中创建进程prog2,并向prog2传递参数。
#include <unistd.h>
int execl(const char *path, const char *arg0, ..., (char *)0);

// prog1.c#include <stdio.h>#include <unistd.h>#include <stdlib.h>int main(){printf("program 1, to exec program 2\n");int res = execl("prog2", "prog2", "hello from prog1", "more info", 0);printf("res = %d\n", res);printf("done\n");// never be printedexit(0);}// prog2.c#include <stdio.h>#include <unistd.h>#include <stdlib.h>int main(int argc, char** argv){printf("program 2\n");printf("%s\n", argv[0]);printf("%s\n", argv[1]);printf("%s\n", argv[2]);exit(0);}

执行:
./prog1
输出:
program1, to exec program 2
program 2
prog2
hello form prog1
more info


方法3:fork—— 复制进程映像
要想让进程同时执行多个函数,我们可以使用线程或从原程序中创建一个完全分离的进程,后者就像init的做法一样,而不像exec调用那样用新程序替换当前执行的进程。我们可以通过调用fork创建一个新进程。这个系统调用复制当前进程,在进程表中创建一个新的表项,新表项中的许多属性与当前进程是相同的。
注意:
(1) 新进程几乎与原进程一模一样,执行的代码也完全相同,但新进程有自己的数据空间、环境和文件描述符。
(2) fork 和 exec 函数结合在一起使用就是创建新进程所需要的一切了。
函数原型:
#include <sys/types.h>
#include <unistd.h>
pid_t fork();
在父进程中的fork调用返回的是新的子进程的PID。新进程将继续执行,就像原进程一样,不同之处在于,子进程中的fork调用返回的是0。父子进程可以通过这一点来判断究竟谁是父进程,谁是子进程。
如果fork失败,它将返回-1。失败通常是因为父进程所拥有的子进程数目超过了规定的限制(CHILD_MAX),此时errno将被设为EAGAIN。如果是因为进程表里没有足够的空间用于创建新的表单或虚拟内存不足,errno变量将被设为ENOMEM。

3.2 等待一个进程

当用fork启动一个子进程时,子进程就有了它自己的生命周期并将独立运行。有时,我们希望知道一个子进程何时结束。例如,有时,父进程在子进程之前结束,由于子进程还在继续运行,所以得到的输出结果有点儿乱。我们可以通过在父进程中调用wait函数,让父进程等待子进程的结束。
函数原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *stat_loc);
wait系统调用将暂停父进程直到它的子进程结束为止。这个调用返回子进程的PID,它通常是已经结束运行的子进程的PID。状态信息允许父进程了解子进程的退出状态,即子进程的main函数返回的值或子进程中exit函数的退出码。如果stat_loc不是空指针,状态信息将被写入它所指向的位置。
// fork_wait.c#include <unistd.h>#include <sys/types.h>#include <sys/wait.h>#include <stdio.h>#include <stdlib.h>int main(){pid_t pid;char *message;int n;int exit_code;printf("fork program starting\n");pid = fork();switch(pid){case -1:perror("fork failed");exit(1);case 0:message = "child";n = 5;exit_code = 37;break;default:message = "parent";n = 3;exit_code = 0;break;}for(; n>0; n--) {puts(message);sleep(1);}if(pid != 0){int stat_val;pid_t child_pid;child_pid = wait(&stat_val);printf("child has finished: PID = %d\n", child_pid);if(WIFEXITED(stat_val))printf("child exited with code %d\n", WEXITSTATUS(stat_val));elseprintf("child terminated abnormally\n");}exit(exit_code);}



父进程(从fork调用中获得一个非零的返回值)用wait系统调用将自己的执行挂起,直到子进程的状态信息出现为止。这将发生在子进程调用exit的时候。我们将子进程的退出码设置为37。父进程然后继续运行,通过测试wait调用的返回值来判断子进程是否正常终止。如果是,就从状态信息中提取出子进程的退出码。

问题:嵌套子进程的情况
http://topic.csdn.net/u/20111007/00/320af831-aede-440e-b379-3fd6d76d8a6c.html

3.3 僵尸进程

用fork来创建进程确实很有用,但你必须清楚子进程的运行情况。子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常终止或父进程调用wait才告结束。因此,进程表中代表子进程的表项不会立刻释放。
虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来,以备父进程今后的wait调用使用。这时它将成为一个死(defunct)进程或僵尸(zombie)进程。

当子进程为僵尸进程时(即子进程在父进程之前结束),如果此时父进程异常终止,子进程将自动把PID为1的进程(即init)作为自己的父进程。子进程现在是一个不再运行的僵尸进程,但因为其父进程异常终止,所以它由init进程接管。僵尸进程将一直保留在进程表中直到被init进程发现并释放。进程表越大,这一过程就越慢。所以,应该尽量避免产生僵尸进程,因为在init清理它们之前,它们将一直消耗系统的资源。

还有另一个系统调用可用来等待子进程的结束。—— waitpid 函数
可以用它来等待某个特定进程的结束。
函数原型:
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *stat_loc, int options);
参数1:pid参数指定需要等待的子进程的PID。如果它的值为-1,waitpid将返回任一子进程的信息。与wait一样。
参数2:如果stat_loc不是空指针,waitpid将把状态信息写到它所指向的位置。
参数3:option参数可用来改变waitpid的行为,其中最有用的一个选项是WNOHANG,它的作用是防止waitpid调用将调用者的执行挂起。这样,你可以用这个选项来检查是否有子进程已经结束。如果没有,程序将继续执行。其他的选项和wait调用的选项相同。因此,如果想让父进程周期性地检查某个特定的子进程是否已终止,就可以使用如下的调用方式:
waitpid(child_pid, (int *) 0, WNOHANG);
如果子进程没有结束或以外终止,它就返回0,否则返回child_pid。
如果waitpid失败,它将返回-1并设置errno。失败的情况包括:没有子进程(errno设置为ECHILD)、调用被某个信号中断(EINTR)或选项参数无效(EINVAL)。

3.4 输入和输出重定向

已打开的文件描述符将在fork和exec调用之后保留下来,我们可以利用对进程这方面知识的理解来改变程序的行为。

// upper.c#include <stdio.h>#include <stdlib.h>#include <ctype.h>// toupperint main(){int ch;while ((ch = getchar) != EOF){putchar(toupper(ch));}exit(0);}// useupper.c#include <unistd.h>#include <stdio.h>#include <stdlib.h>int main(int argc, char *argv[]){char *filename;if (argc != 2){fprintf(stderr, "usage: useupper file\n");exit(1);}filename = argv[1];if (!freopen(filename, "r", stdin)){fprintf(stderr, "could not redirect stdin from file %s\n", filename);exit(2);}execl("./upper", "upper", 0);perror("could not exec ./upper");exit(3);}

$ ./useupper file.txt

useupper程序用freopen函数先关闭标准输入,然后将文件流stdin与程序参数给定的文件名关联起来。接下来,它调用execl用upper程序替换掉正在运行的进程代码。注意:因为已打开的文件描述符会在execl调用之后保留下来,所以upper程序的运行情况和它在shell提示符下的运行情况完全一样:$ ./upper < file.txt。

3.5 线程(略)





原创粉丝点击