进程调度
ps输出的一个特点就是ps命令实体本身:
1357 pts/2 R 0:00 ps -ax
这表明进程1357处于运行状态(R)并且正在执行命令ps -ax。所以进程是在其自身的输出中被描述的。状态指示器只是表明程序已经准备好运行,并不一定是在实际运行。在单处理器的计算机上,每次只能运行一个进程,而其他的进程必须依次等待。这些轮序,就是所谓的时间片,非常短,从而给我们一种感觉,所有的程序是在同时运行的。状态R只是表示程序并没有等待其他的进程完成或是等待输入或输出来结束。这就是为什么我们会在ps输出中看到两个这样的进程。(另一个通常会看到的标识为运行的进程就是X显示服务器)
Linux内核使用一个进程调度器来决定哪一个进程将会接受下一个时间片。他是通过使用进程优先级(我们在第4章讨论了优先级)来做到的。具有高优先级的进程会具有更高的运行频率,而其他的,例如低优先级的后台任务,就具有较低的运行频率。在Linux中,进程不能超过分配给他们的时间片。老的系统,例如Windows 3.x,通常需要进程显示放弃,从而其他的进程可以重新运行。
在多任务系统中,例如Linux,多个程序也许会竞争同一个资源,那些执行大量任务并且暂停等待输入的程序被认为要比独占处理器来连续的计算一些值或是连续的查询系统来查看是否有新的输入可用的方式要好得多。从术语来说,我们称之为nice程序,而且从常识来说,这个"niceness"是可以度量的。操作系统依据一个"nice"值以及程序的行为来确定一个进程的优先级,其默认值为0。长时间运行而没有暂停的程序通常会具有较低的优先级。例如,程序暂停等待满足输入。这有助于保持程序与用户进行交互;当其等待用户的某些输入时,系统会增加其优先级,这样当他满足重新运行的条件时,他就具有一个较高的优先级。我们可以使用nice程序来设置进程的nice值,并且使用renice来重新调整进程的nice值。nice命令会为一个进程的nice值增加10,从而为其指定一个较低的优先级。我们可以使用ps命令的-l或是-f选项来查看活动进程的nice值。我们所感兴趣的值显示在NI列中。
$ ps -l
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
000 S 500 1259 1254 0 75 0 - 710 wait4 pts/2 00:00:00 bash
000 S 500 1262 1251 0 75 0 - 714 wait4 pts/1 00:00:00 bash
000 S 500 1313 1262 0 75 0 - 2762 schedu pts/1 00:00:00 emacs
000 S 500 1362 1262 2 80 0 - 789 schedu pts/1 00:00:00 oclock
000 R 500 1363 1262 0 81 0 - 782 - pts/1 00:00:00 ps
从这里我们可以看出oclock程序以一个默认的nice值在运行。如果他是由下面的命令来启动的
$ nice oclock &
那么他就已经被分配了一个+10的nice值。如果我们用下面的命令来进行调整
$ renice 10 1362
1362: old priority 0, new priority 10
那么oclock就会更少的运行频率。我们可以再次使用ps命令来查看修改的nice值:
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
000 S 500 1259 1254 0 75 0 - 710 wait4 pts/2 00:00:00 bash
000 S 500 1262 1251 0 75 0 - 714 wait4 pts/1 00:00:00 bash
000 S 500 1313 1262 0 75 0 - 2762 schedu pts/1 00:00:00 emacs
000 S 500 1362 1262 0 90 10 - 789 schedu pts/1 00:00:00 oclock
000 R 500 1365 1262 0 81 0 - 782 - pts/1 00:00:00 ps
状态列现在包含N来表明nice值已经由默认值进行修改。ps输出的PPID域表明父进程ID,使得PID进程启动的进程,如果这个进程不再运行,为init(PID 1)。
Linux调度器依据优先级来决定允许哪个进程运行。当然,各个实现会有所不同,但是高优先级具有更高的运行频率。在某些情况下,如果高优先级进程已经准备运行,低优先级进程根本就不会运行。
启动一个新进程
我们可以使得一个程序由另一个进程的内部来运行,从而通过使用system库函数来创建一个新的进程。
#include <stdlib.h>
int system (const char *string);
system函数运行作为字符串传递给他的命令并且等待其结束。这个命令的运行与下面命令的运行结果相同:
$ sh -c string
如果shell并没有启动来运行这个命令,system就会返回127,如果发生了其他错误则会返回-1。否则system返回这个命令的退出代码。
试验--system
我们可以使用system来编写一个程序为我们运行ps命令。尽管这个程序并不是十分有用,我们会在后面的例子中看到如何来开发这个技术。在这个例子中我们并不十分严格的检测system调用是否适用于这种情况。
#include <stdlib.h>
#include <stdio.h>
int main()
{
printf(“Running ps with system/n”);
system(“ps -ax”);
printf(“Done./n”);
exit(0);
}
当我们编译并运行这个程序时,system1.c,我们会得下面的输出:
$ ./system1
Running ps with system
PID TTY STAT TIME COMMAND
1 ? S 0:05 init
2 ? SW 0:00 [keventd]
...
1262 pts/1 S 0:00 /bin/bash
1273 pts/2 S 0:00 su -
1274 pts/2 S 0:00 -bash
1463 pts/1 S 0:00 oclock -transparent -geometry 135x135-10+40
1465 pts/1 S 0:01 emacs Makefile
1480 pts/1 S 0:00 ./system1
1481 pts/1 R 0:00 ps -ax
Done.
因为system1函数使用一个shell启动所要求的程序,我们可以通过修改system1.c中的函数调用将其放在后台运行:
system(“ps -ax &”);
当我们编译运行这个版本的程序时,我们会得到下面的输出:
$ ./system2
Running ps with system
PID TTY STAT TIME COMMAND
1 ? S 0:05 init
2 ? SW 0:00 [keventd]
...
Done.
$ 1246 ? S 0:00 kdeinit: klipper -icon klipper -miniicon klipper
1274 pts/2 S 0:00 -bash
1463 pts/1 S 0:00 oclock -transparent -geometry 135x135-10+40
1465 pts/1 S 0:01 emacs Makefile
1484 pts/1 R 0:00 ps -ax
工作原理
在第一个例子中,程序使用"ps -ax"字符串来调用system,这会运行ps程序。当ps命令已经完成时,我们的程序会这个调用返回到system。system程序十分有用,但却十分有限。因为我们的程序必须等待直到system调用所启动的进程结束,而我们不得进行其他的任务。
在第二个例子中,system调用在shell命令结束时立即返回。因为他要求在后台运行一个程序,当ps程序启动时shell就会立即返回,就如同我们在shell提示符下输入下面的命令一样:
$ ps -ax &
system2程序在ps命令有机会完成其所有的的输出之前输出Done.并退出。ps命令会在system2退出之后继续产生输出。这种进程的行为会使得用户十分迷惑。要更好的利用进程,我们需要更好的控制其动作。下面我们来看一下进程的底层接口,exec。
注:通常而言,system并不启动其他进程的一个完美方法,因为他使用一个shell来调用所要求的程序。这样的效率并不高,因为shell是在程序启动之前启动的,而且十分依赖于shell的安装与所用的环境。在这一节,我们将会看到调用程序的一个更好的方法,其使用总是优先于system调用。
替换一个进程映像
有一个以exec开头的相当函数族。他们的不同在于他们启动进程与表过程序参数的方式。一个exec函数使用由path与file参数所指定的新进程来替换当前的进程。
#include <unistd.h>
char **environ;
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[]);
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,execlp,execle带有多个参数,并以空指针结束。execv与execvp的第二个参数是一个字符串数组。在这两种情况下,出现在argv数组中的指定参数传递给main,并启动一个新程序。这些函数通常都是使用execve来实现的,尽管并没有要求以这种方式实现。
函数为以p为后缀的函数与其他函数的不同在于他们会查找PATH环境变量来查找新的程序可执行文件。如果可执行文件并不在这个路径中,就需要将一个包含目录的绝对文件名作为参数传递给函数。
全局变量environ可以为新程序环境传递一个值。相对应的,execle与execve的另一个参数可以传递一个字符串数组用作新程序环境。
如果我们希望使用exec函数来启动ps程序,我们需要在6个exec函数族中作出选择,如下面的代码段所示:
#include <unistd.h>
/* Example of an argument list */
/* Note that we need a program name for argv[0] */
char *const ps_argv[] = {"ps","-ax",0};
/* Example evnironment, not terribly useful */
char *const ps_envp[] = {"PATH=/bin:/usr/bin","TERM=console",0};
/* Possible calls to exec functions */
execl("/bin/ps","ps","-ax",0);
execlp("ps","ps","-ax",0); /* assumes ps in /bin */
execle("/bin/ps","ps","-ax",0,ps_envp); /* passes own environment */
execv("/bin/ps",ps_argv);
execvp("ps",ps_argv);
execve("/bin/ps",ps_argv,ps_envp);
试验--execlp
下面我们来修改我们的例子来使用execlp调用。
#include <unistd.h>
#include <stdio.h>
int main()
{
printf(“Running ps with execlp/n”);
execlp(“ps”, “ps”, “-ax”, 0);
printf(“Done./n”);
exit(0);
}
当我们运行这个程序,pexec.c,我们会是到通常的ps的输出,但是根本没有Done.信息。我们还要注意到,在输出并没有名为pexec的进程。
$ ./pexec
Running ps with execlp
PID TTY STAT TIME COMMAND
1 ? S 0:05 init
2 ? SW 0:00 [keventd]
...
1262 pts/1 S 0:00 /bin/bash
1273 pts/2 S 0:00 su -
1274 pts/2 S 0:00 -bash
1463 pts/1 S 0:00 oclock -transparent -geometry 135x135-10+40
1465 pts/1 S 0:01 emacs Makefile
1514 pts/1 R 0:00 ps –ax
工作原理
程序首先输出其第一条信息然后调用execlp,他会在PATH环境变量所指定的目录中查找一个名为ps的程序。然后他执行这个程序来替换我们的pexec程序,就如同我们输入下面的shell命令一样
$ ps -ax
当ps结束时,我们得到一个新的shell提示符。我们并没有返回到pexec,所以第二条信息根本就不会输出。新进程的PID与原始进程相同,同时具有相同的父进程PID与nice值。事实上,所发生的一切就是正在运行的程序已经开始由exec调用中所指定的新的可执行文件来执行新代码。
在参数列表的组合尺寸与由exec函数所启动的进程环境是有限制的。这是由ARG_MAX来指定的,而在Linux系统上这个限制为128KB。其他的系统也许会设置更为宽松的限制,然而这也许会导致问题。POSIX规范表明ARG_MAX至少应为4096B。
通常情况下exec函数并不会返回,除非发生错误,在这种情况下会设置错误变量errno,而exec函数会返回-1。
由exec所启动的新进程会继承原进程的许多特性。特别是,打开的文件描述符会在新进程中保持打开状态,除非设置了关闭选项。原始进程中打开的目录流会关闭。
复制一个进程映像
要使进程同时执行多个函数,我们或者使用线程,或者是在一个程序的内部创建一个完全独立的进程,就如init所做的那样,而不是替换当前的执行线程,就如exec那样。
我们可以通过调用fork来创建一个新进程。这个系统调用会复制当前进程,在进程表中创建一个实体,而且与当前进程具有相同的属性。新进程与原始进程几乎是相同的,执行相同的代码,但是具有其自己的数据空间,环境与文件描述符。与exec函数组合,fork就是我们创建新进程所需要的全部内容。
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
正如我们在图11-2中所看到的,父进程中的fork调用会返回新的子进程的PID。新进程会继续执行,就如原始进程一样,所不同的是子进程中的fork调用返回0。这可以使得父进程与子进程彼此区分。
如果fork失败则会返回-1。这通常是由于父进程可以拥有的子进程的数量(CHILD_MAX)所引起的,在这种情况下,errno会被设置为EAGAIN。如果在进程表中没有足够的空间,或是没有足够的虚拟内存,errno变量会被设置为ENOMEM。
使用fork的通常的代码片段如下所示:
pid_t new_pid;
new_pid = fork();
switch(new_pid) {
case -1 : /* Error */
break;
case 0 : /* We are child */
break;
default : /* We are parent */
break;
}
试验--fork
下面我们来看一个简单的例子,fork1.c。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid;
char *message;
int n;
printf(“fork program starting/n”);
pid = fork();
switch(pid)
{
case -1:
perror(“fork failed”);
exit(1);
case 0:
message = “This is the child”;
n = 5;
break;
default:
message = “This is the parent”;
n = 3;
break;
}
for(; n > 0; n--) {
puts(message);
sleep(1);
}
exit(0);
}
这个程序会运行两个进程。一个子进程会被创建,并且输出一条信息五次。原始的进程只输出三次。父进程会在子进程输出其全部的信息之前结束,所以下一个shell提示符会与输出混合出现一起。
$ ./fork1
fork program starting
This is the parent
This is the child
This is the parent
This is the child
This is the parent
This is the child
$ This is the child
This is the child
工作原理
当fork被调用后,程序分为两个独立的进程。父进程是通过由fork返回的非0来标识的,并且使用父进程来设置要输出的信息数目。