`
isiqi
  • 浏览: 16491853 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

进程与信号(二)

阅读更多

进程调度

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来标识的,并且使用父进程来设置要输出的信息数目。

分享到:
评论

相关推荐

    实验 Linux进程通信的参考答案

    在 Linux 中,信号是一种异步通信机制,允许一个进程向另一个进程发送信号,从而通知其执行某些操作。信号可以分为两类:内核信号和用户信号。内核信号是由操作系统内核生成的,而用户信号是由用户进程生成的。 在...

    Linux环境进程间通信 信号灯

    但与互斥锁不同的是,二值信号灯强调的是共享资源的状态,而非特定进程对资源的锁定状态。 - **计算信号灯:** 信号灯的值可以取任意非负值(受限于内核限制),用于表示资源的数量或状态。 #### 二、Linux信号灯 ...

    基于Linux的实现进程的信号量互斥申请

    3. **互斥锁的概念与实现**:在多进程环境中,互斥锁是一种通过信号量实现的机制,确保同一时间只有一个进程访问特定资源。当一个进程获得锁后,其他试图获取该锁的进程将被阻塞,直到锁被释放。 4. **PV操作**:...

    进程间通信之信号 sinal ) 完整代码

    进程间通信之信号 sinal ) 唯一的异步通信方式 七种进程间通信方式: 一 无名管道( pipe ) 二 有名管道( fifo ) 三 共享内存 shared memory 四 信号 sinal 五 消息队列 message queue ) 六 信号量 ...

    操作系统实验进程的软中断通信

     (1)正确应用系统调用signal()建立进程与信号(异步事件)之间的联系,理解信号机制;  (2)正确应用系统调用getppid()、kill(),进步理解广义同步的含义。 5[思考问题]  (1)为什么说系统调用signal()...

    Linux多进程通信-信号量,共享内存示例

    互斥量是二进制信号量,用于保护临界区,确保同一时间只有一个进程可以访问共享资源。信号量集则可以表示更复杂的计数,允许多个进程同时访问但控制访问数量。在代码中,通常会使用`sem_t`结构体来创建和操作信号量...

    《计算机操作系统》 进程的软中断通信

    第二个问题是,若子进程向父进程发送信号,父进程接到信号后可以缺省操作、或忽视信号、或执行一函数,各是什么含义?答案是,缺省操作是进程捕捉到信号之后不作任何指定的工作而忽略中断信号的影响。执行一函数是...

    Java进程信号量机制的实验程序

    Java进程信号量机制是多线程编程中一种有效的同步工具,它源于操作系统中的同步原语,用于管理和控制对共享资源的访问。在Java中,信号量由`java.util.concurrent.Semaphore`类实现,它提供了两种类型:可重用的二...

    操作系统实验二:进程、线程之间的同步

    1。生产者消费者问题(信号量+mutex) 参考教材中的生产者消费者算法,创建5个进程,其中两个进程为生产者进程,...编写一个写者优先解决读者写者问题的程序,其中读者和写者均是多个进程,用信号量作为同步互斥机制。

    操作系统实验二 进程通信机制的应用

    操作系统实验二“进程通信机制的应用”旨在通过实践深入理解进程并发执行的概念,以及掌握进程和线程的创建、控制及通信方法。实验报告要求学生在Linux环境下,运用系统调用进行进程和线程的编程与调试,从而更好地...

    linux 网络编程 进程通信 信号量

    二、进程通信 在多进程环境下,进程间的通信是必不可少的。Linux提供了多种进程通信方式: 1. **管道(Pipes)**:无名管道是半双工的,数据只能单向流动,适用于父子进程之间的简单通信。 2. **命名管道(Named ...

    Linux进程间通信-信号通信信号发送实例.pdf

    第二个实例展示了如何使用`sigqueue`函数向当前进程发送带有附加信息的信号。这个例子中定义了一个信号处理函数`SignHandlerNew`,当信号被接收时,会打印出信号号和附加信息。`sigaction`函数用于设置信号处理函数...

    操作系统进程控制实验报告.doc

    每个进程都有信号机制检验是否有信号到达,如果有,捕获信号后,根据系统默认处理或者用户自定义的防方法处理信号,当信号处理完后,在返回原来的程序继续执行。 6. 进程创建和控制: 进程创建和控制是操作系统中...

    L16-进程同步与信号量1

    【进程同步与信号量】是操作系统中的核心概念,主要用于解决多进程协作中可能出现的问题,确保系统资源的有序分配和使用。在本节中,我们主要探讨了生产者-消费者问题和信号量机制。 首先,生产者-消费者问题是操作...

    编写daemon进程,检测信号实现功能

    当检测到SIGHUP(远程挂断)信号时,该进程获得系统运行时间(读取/proc/uptime文件,该文件第一列为系统启动到现在的时间(以秒为单位),第二列为系统空闲的时间(以秒为单位)),将其追加写入日志文件。...

    进程与线程的生动形象理解

    ### 进程与线程的生动形象理解 #### 一、引言 进程与线程作为操作系统中的核心概念,对于理解和开发高效的软件至关重要。本文旨在通过一个生动的类比来帮助读者更好地理解进程与线程的基本概念及其差异,并进一步...

    进程同步与互斥

    进程同步与互斥是操作系统中的核心概念,它们在多任务环境下确保了程序的正确执行和资源的有效利用。这里我们将深入探讨这两个概念,并结合C语言的实现进行讲解。 首先,我们来理解什么是进程同步。在多任务操作...

    进程并发与同步

    **同步控制解析**:为了实现父子进程间的同步,可以在子进程的结束处调用`exit()`向父进程发送一个终止信号;父进程则使用`wait()`等待子进程结束。这种方法简单有效,确保了父进程只有在所有子进程完成后才继续...

Global site tag (gtag.js) - Google Analytics