《Linux程序设计》学习笔记11——进程和信号


进程的基本概念

UNIX98 规范和 UNIX95 规范把进程定义为“一个其中运行着一个或多个线程的地址空间和这些线程所需要的系统资源。

实际上,正在运行的程序或进程由程序代码、数据、变量(占用着系统内存)、打开的文件(文件描述符)和环境组成。一般来说, Linux 系统会在进程之间共享程序代码和系统函数库,所以在任何时刻内存中都只有程序的一份拷贝。

每个进程都会被分配一个唯一的数字编号,称为进程标识符或 PID ,它通常是一个范围从 2 32768 的正整数。当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的 PID ,当数字已经回绕一圈时,新的 PID 重新从 2 开始。数字 1 为特殊进程 init 保留,它负责管理其他的进程。所有其他的系统进程要么是由 init 进程启动,要么由被 init 进程启动的其他进程启动。

在许多 Linux 系统上,目录 /proc 中有一组特殊的文件,这些文件的特殊之处在于它们允许你“窥视”这在运行的进程的内部情况,就好像这些进程是目录中的文件一样。这在 学习笔记 03 /proc 文件系统部分提到过。

Linux 进程表就像一个数据结构,它把当前加载在内存中的所有进程的有关信息保存在一个表中,其中包括进程的 PID 、进程的状态、命令字符串和其他一些 ps 命令输出的各类信息。操作系统通过进程的 PID 对它们进行管理,这些 PID 是进程表的索引。进程表的长度是有限制的,所有系统能够支持的同时运行的进程数也是有限制的。早期的 UNIX 系统只能同时运行 256 个进程。最新的实现版本已大幅度放宽这一限制,可以同时运行的进程数可能只与用于建立进程表项的内存容量有关,而没有具体的数字限制了。

我们可以使用 ps 命令 查看当前正在运行的进程。默认情况下, ps 程序只显示与终端、主控台、串行口或伪终端(比如 pts/0 )保持连接的进程的信息。其他进程在运行时不需要通过终端与用户通信,它们通常是一些系统进程, Linux 用它们来管理共享的资源。我们可以使用 ps 命令的 -a 选项 查看所有的进程,用 -f 选项 显示进程完整的信息。 ps 命令的详细资料请查阅手册。

 

进程调度

在一台单处理器计算机上,同一时间只能有一个进程可以运行,其他进程处于等待运行状态。每个进程轮到的运行时间(时间片)是相当短暂的,这就给人一种多个程序在同时运行的印象。

Linux 内核用进程调度器来决定下一个时间片应该分配给哪个进程。它的判断依据是进程的优先级,优先级高的进程运行得更为频繁。在 Linux 中,进程的运行时间不可能超过分配给它们的时间片,它们采用的是抢占式多任务 处理,所以进程的挂起和仅需运行无需彼此之间的协作。

在一个如 Linux 这样的多任务系统中,多个程序可能会竞争使用同一个资源,在这种情况下,我们认为,执行短期的突发性工作并暂停运行以等待输入的程序,要比持续占用处理器以进行计算或不断轮询系统以查看是否有新的输入到达的程序要更好 。我们称表现良好的程序为 nice 程序 。一个进程的 nice 值默认为 0 并将根据这个程序的表现而不断变化。我们可以使用 nice 命令设置进程的 nice 值,使用 renice 命令调整它的值。可以使用 ps 命令的 -f -l 详细查看这在运行的进程的 nice 值( NI 栏)。

如果你对进程调度感兴趣,可以去参阅《操作系统》或《 Linux 内核》相关的书籍。

 

启动新进程

在《精通 UNIX 环境下 C 语言编程及项目实践》的 学习笔记 04 中曾提过有三种执行新进程的方法。

一种就是直接调用库函数 system 来实现。然而一般来说,使用 system 函数远非启动其他进程的理想手段,因为它必须用一个 shell 来启动需要的程序。由于在启动程序之前需要先启动一个 shell ,而且对 shell 的安装情况及使用的环境的依赖也很大,所以使用 system 函数的效率不高。

另两种方式则是 fork-exec vfork-exec ,日常编程中则常用前者。

Exec 函数系列有六个函数,具体定义如下:

#include extern char **environ; int execl(const char *path, const char *arg0, ..., (char *)0); int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]); int execlp(const char *file, const char *arg0, ..., (char *)0); int execv(const char *path, const char *argv[]); int execve(const char *path, const char *argv[], const char *envp[]); int execvp(const char *file, const char *argv[]);

当我们在程序中直接调用 exec 函数时,指定运行的程序将替换当前的程序,看下面的一个简单程序 pexec.c

#include #include int main() { printf("Running ps with execlp /n"); sleep(3); execl("/bin/ps", "ps", "-f", 0); // 语句 0 printf("Done./n"); return 0; }

我们使用 make pexec & ./pexec & 运行程序,在语句 0 执行前使用 ps 命令查看当前的进程列表,你将发现 pexec 进程存在于列表中;但在语句 0 执行的结果中却找不到 pexec 进程,实际上当 execl 函数执行时,新启动的 ps 进程已经把 pexec 进程替换掉了。

注意 :对于 exec 函数启动的进程来说,它的参数表和环境加在一起的总长度是有限制的。上限由 ARG_MAX 给出,在 Linux 系统上它是 128K 字节。其他系统可能会设置一个非常有限的长度,这有可能会导致出现问题, POSIX 规范要求 ARG_MAX 至少要有 4096 个字节。

提示 :在原进程中已打开的文件描述符在新进程中仍将保持打开,除非它们的“执行时关闭标志”( close on exec flag )被置位。任何在原进程中已经打开的目录流将在新进程中被关闭。

我们可以通过调用 fork 创建一个新进程。通过与 exec 函数配合,我们可以实现多进程编程的目的。当用 fork 启动一个子进程时,子进程就有了它自己的生命周期并将独立运行。我们可以通过在父进程中调用 wait waitpid 函数来等待子进程的结束。

fork 来创建进程确实很有用,但必须清楚子进程的运行情况。子进程终止时,它与父进程之间的关联还会保持,直到父进程也正常地终止或父进程调用 wait 才结束。因此,进程表中代表子进程的表项不会立刻释放。虽然子进程已经不再运行,但它仍然存在于系统中,因为它的退出码还需要保存起来以备父进程今后的 wait 调用使用。这时它将成为一个死进程( defunct )或僵尸进程( zombie 。关于僵尸进程的详细介绍同样在《精通 UNIX 环境下 C 语言编程及项目实践》的 学习笔记 04 中有所描述。

 

信号

信号是 UNIX Linux 系统响应某些条件而产生的一个事件,接收到信号的进程会相应地采取一些行动。信号的名称在头文件 signal.h 中定义,每个都以 SIG 开头。

我们可以使用 signal 函数 处理信号,信号的处理方式可以是 SIG_IGN (忽略信号)、 SIG_DEF (默认方式)或者自行定义处理方式。关于如何使用 signal 处理信号在《精通 UNIX 环境下 C 语言编程及项目实践》的 学习笔记 05 中有所描述。

注意 :在信号处理程序中,调用如 printf 这样的函数是不安全的。一个有用的技巧是,在信号处理程序中设置一个标志,然后在主程序中检查该标志,如需要就打印一条信息。书中的表 11-6 列出了可以在信号处理程序中被安全调用的函数。

进程可以通过调用 kill 函数 向包括它本身在内的其他进程发送一个信号。如果程序没有发送该信号的权限,对 kill 函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。

提示 不推荐使用 signal 接口 ,应该使用定义更清晰、执行更可靠的函数 sigaction ,在所有的新程序中都应该使用这个函数。

X/Open UNIX 规范推荐了一个更新和更健壮的信号编程接口: sigaction ,定义如下

#include int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);

下面是一个简单的例程,它用 sigaction 来截获 SIGINT 信号:

#include #include #include void ouch(int sig){ printf("OUCH! - I got signal %d/n", sig); } int main() { struct sigaction act; act.sa_handler = ouch; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGINT, &act, 0); while(1){ printf("hello. /n"); sleep(1); } return 0; }

sigaction 函数的调用方式与 signal 函数差不多。 sigaction 结构定义在文件 signal.h 中,它的作用是定义在接受到参数 sig 指定的信号后应该采取的行动。该结构应该至少包括以下几个成员:

void (*) (int) sa_handler // function, SIG_DFL or SIG_IGN sigset_t sa_mask // signals to block in sa_handler int sa_flags // signal action modifiers

其中, sa_mask 字段指定了一个信号集,在调用 sa_handler 所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字 中。这是一组将被阻塞且不会传递给该进程的信号,在使用 signal 函数时,可能会出现有些信号在处理函数中还未运行结束时就被接收到,设置信号屏蔽字可以防止这种现象的发生。头文件 signal.h 中有一组函数用来操作信号集,它们分别是 sigaddset sigemptyset sigfillset sigdelset 等。

你可能感兴趣的:(《Linux程序设计》笔记)