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

时间:2022-08-28 21:48:18

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

分类: 《Linux程序设计》笔记 918人阅读 评论(2)收藏 举报 linuxsignalunixlinux内核编程struct


进程的基本概念

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

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

每个进程都会被分配一个唯一的数字编号,称为进程标识符或PID,它通常是一个范围从232768的正整数。当进程被启动时,系统将按顺序选择下一个未被使用的数字作为它的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-execvfork-exec,日常编程中则常用前者。

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

[cpp] view plaincopyprint?
  1. #include <unistd.h>  
  2.      
  3. extern char **environ;  
  4. int execl(const char *path, const char *arg0, ..., (char *)0);  
  5. int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);  
  6. int execlp(const char *file, const char *arg0, ..., (char *)0);  
  7. int execv(const char *path, const char *argv[]);  
  8. int execve(const char *path, const char *argv[], const char *envp[]);  
  9. int execvp(const char *file, const char *argv[]);  

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

[cpp] view plaincopyprint?
  1. #include <unistd.h>  
  2. #include <stdio.h>  
  3.     
  4. int main()  
  5. {  
  6.         printf("Running ps with execlp /n");  
  7.         sleep(3);  
  8.         execl("/bin/ps""ps""-f", 0);       // 语句 0  
  9.         printf("Done./n");  
  10.    
  11.         return 0;  
  12. }  

我们使用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启动一个子进程时,子进程就有了它自己的生命周期并将独立运行。我们可以通过在父进程中调用waitwaitpid函数来等待子进程的结束。

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

 

信号

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

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

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

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

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

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

[cpp] view plaincopyprint?
  1. #include <signal.h>  
  2. int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);   

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

[cpp] view plaincopyprint?
  1. #include <stdio.h>  
  2. #include <unistd.h>  
  3. #include <signal.h>  
  4.    
  5. void ouch(int sig){  
  6.         printf("OUCH! - I got signal %d/n", sig);  
  7. }  
  8.    
  9. int main()  
  10. {  
  11.         struct sigaction act;  
  12.    
  13.         act.sa_handler = ouch;  
  14.         sigemptyset(&act.sa_mask);  
  15.         act.sa_flags = 0;  
  16.    
  17.         sigaction(SIGINT, &act, 0);  
  18.    
  19.         while(1){  
  20.                 printf("hello. /n");  
  21.                 sleep(1);  
  22.         }  
  23.    
  24.          return 0;  
  25. }   

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

[cpp] view plaincopyprint?
  1. void (*) (int) sa_handler        // function, SIG_DFL or SIG_IGN  
  2. sigset_t sa_mask              // signals to block in sa_handler  
  3. int sa_flags                  // signal action modifiers  

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