进程和信号是Linux操作环境的基础部分,控制着Linux和其它类UNIX系统的几乎所有活动。
UNIX标准对进程的定义:其中运行着一个或者多个线程的地址空间,以及这些线程需要的系统资源。Linux系统的进程是非常轻量级的。
每个Linux进程包含以下部分:
Linux系统使用一个被称为进程表的结构来存放当前加载到内存的所有进程的信息。这些信息包括:进程ID、进程状态、进程命令字符串以及其它一些ps命令输出的信息。操作系统通过PID对进程进行管理,早起的UNIX系统只能同时运行256个进程。
注意:这些状态也适用于线程。进程中的线程通常处于不同的状态。
通过ps的STAT列,可以查看进程的状态,其代码如下表:
| STAT代码 | 说明 |
| S | 睡眠。通常是在等待某个事件的发生,如信号、输入、Time slot |
| R | 运行。严格来说应是“可运行”,即在运行队列中,处于正在执行或即将运行状态 |
| D |
不可中断的睡眠(Uninterruptible sleep)。通常是在等待输入或输出(网络、磁盘、其它外设的IO)完成 处于此状态的进程,无法处理信号(无法被信号唤醒,只能被它所等待的东西唤醒,或超时,如果在睡眠前设置了超时的话),即使是kill -9 也无法处理 不允许中断的原因是,保护系统数据一致,防止数据读取错误 |
| T |
停止。通常被Shell作业控制所停止,或者进程正处于调试器的控制下 在Terminal中键入Ctrl + Z会导致当前前台进程停止,暂停运行。此时输入bg,则让该进程继续在后台运行 |
| Z |
死(Defunct)进程或者僵尸(Zombie)进程 子进程死亡后,处于僵尸状态,其父进程负责收集其退出码等信息并完全释放它 |
| N | 低优先级任务(nice) |
| W | 分页。不适用于2.6+内核 |
| s | 进程是Session leader |
| + | 进程属于前台进程组 |
| l | 进程是多线程的 |
| < | 高优先级任务 |
一般而言,每个Linux进程都是由另外一个被称为父进程的进程所启动的,前者相对的被称为子进程。在Linux系统启动时,它运行一个PID为1的init进程,可以把该进程看做操作系统的进程管理器,它是所有进程的祖先进程。
启动进程并等待其结束,是Linux中最基本的进程管理任务。应用程序可以通过fork、exec、wait等系统调用完成这些任务。
每个进程被分配以非常短暂的时间片,在时间片范围内进程代码被CPU执行,由于CPU非常快、时间片又非常短,你会感觉到多个程序同时运行的假象。Linux内核使用进程调度器来决定下一个时间片应该分配给哪个进程,其判断的依据是进程的优先级,优先级高的进程获得的时间片更多。在Linux中进程的运行时间不可能超过分配给它的时间片,Linux使用的是抢占式处理,因此进程的挂起、继续运行不需要彼此之间的协作。
进程调用exit() 系统调用可以让自身退出,进程占用的内存将被释放,并利用信号通知其父进程。
父进程可能先于子进程死亡,这种情况下,init进程可以成为子进程的养父。
子进程在死亡后,将处于僵尸状态。这种状态的进程不能被调度,并占据少量的系统资源,以保证其父进程可以访问其退出码等信息。父进程负责完全的释放子进程。
进程包含:虚拟地址空间、打开的系统对象的描述符、安全上下文、进程标识符、环境变量、最小-最大工作集大小,以及最少一个线程 —— 主线程。
线程是进程内部的一个可调度的实体(执行路径),进程内的所有线程共享地址空间、打开的描述符。每个线程维护自己的exception handlers、调度优先级、线程本地存储、线程标识符、线程上下文结构。线程可以具有自己的安全上下文。
线程上下文中包括的数据项:寄存器数据、内核栈、线程环境块、位于进程地址空间的用户栈。
可以在程序内部启动另外一个程序,从而创建新的进程,可以通过库函数system()完成:
#include <stdlib.h> /** * 运行指定的命令并等待其完成 * @param cmd 需要执行的命令 * @return 如果无法启动Shell返回127,其它错误返回-1,否则返回所执行命令的退出码 */ int system( const char *cmd );
上述函数的重大缺点是必须等待子进程的完成,并且依赖于Shell,因此使用的不多。
exec系列函数用于把当前进程替换为一个新的进程(新进程执行结束后不会返回原进程)。 新的程序启动后,原有的程序就不再运行了:
#include <unistd.h>
char **environ; // 该全局变量可用来设置传递到新程序的环境变量
// ***p函数通过搜索PATH环境变量来查找新的可执行文件的路径,如果目标程序不在PATH中,则path参数应当使用绝对路径
// ***e函数支持通过envp数组指定环境变量
// 下面三个函数支持变长参数列表,此列表以一个空指针结束
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[] );
//举例:
char * const ps_argv[] = { "ps", "ax", 0 };
char * const ps_envp[] = { "PATH=/bin:/usr/bin", "TERM=console", 0 };
execl("/bin/ps", "ps", "ax", 0);
execlp("ps", "ps", "ax", 0);
execle("/bin/ps", "ps", "ax", 0, ps_envp);
execv("/bin/ps", ps_argv);
execvp("ps", ps_argv);
execve("/bin/ps", ps_argv, ps_envp);
一般情况下,exec函数是不会返回的,除非发生错误,此时返回-1并设置errno。
exec启动的新进程保留了原进程的许多特性,特别是,原进程打开的文件描述符仍然有效,除非这些描述符的close on exec flag标记位被设置。任何在原进程中打开的目录流都会在新进程中被关闭。
系统调用fork允许创建以当前进程为模板,复制出一个新的进程。fork调用会在进程表中创建一个新的表项,其中很多属性都和原进程相同,包括所执行的代码。但新进程具有自己的数据空间、环境、文件描述符。fork函数的原型如下:
#include <sys/types.h> #include <unistd.h> pid_t fork( void );
fork()函数很巧妙,其具有“两次返回”的效果,实质上对应了父子进程的不同执行路径。对于父进程,fork()返回子进程的ID;而对于子进程,fork()总是返回0。通过这一特点,可以判断当前执行的代码是父进程还是子进程:
pid_t new_pid;
new_pid = fork();
switch ( new_pid )
{
case -1 : /* 错误 */
break;
case 0 : /* 子进程 */
break;
default : /* 父进程 */
break;
}
结合fork、exec函数,创建新进程的条件就完备了。
当fork启动一个子进程后,子进程就有了自己的生命周期,并将独立运行,有时候,需要知道子进程何时结束,可以在父进程中用wait系统调用:
#include <sys/types.h> #include <sys/wait.h> /** * 暂停父进程的执行,直到其子进程结束 * @param stat_loc 存放状态信息,用于了解子进程的退出状态(即子进程main函数的返回值或者exit函数的退出码) * WIFEXITED(stat_val) 如果子进程正常结束,则取值非0 * WEXITSTATUS(stat_val) 如果WIFEXITED非0,取值子进程的退出码 * WIFSIGNALED(stat_val) 如果子进程死于未捕获的信号,则取值非0 * WTERMSIG(stat_val) 如果WIFSIGNALED非0,取值目标信号的代码 * WIFSTOPPED(stat_val) 如果子进程意外终止,取值非0 * WSTOPSIG(stat_val) 如果WIFSTOPPED非0,返回信号代码 * * @return 子进程的PID */ pid_t wait( int *stat_loc ); /** * 等到某个特定的子进程结束 * @param pid 子进程的PID,如果指定为-1将返回任一子进程的信息 * @param stat_loc 如果不是空指针,则用来存放状态信息 * @param option 用于定制waitpid的行为 */ pid_t waitpid( pid_t pid, int *stat_loc, int options ); /** * 检查某个子进程是否结束,立即返回 * * @return 如果目标子进程没有结束或者意外终止,返回0;否则返回子进程PID * 如果函数调用失败,返回-1并设置errno */ waitpid(child_pid, (int *) 0, WNOHANG);
使用fork创建的子进程终止时,其与父进程的关联还会保持,直到父进程也正常终止或者父进程调用wait才会结束。在此之前,虽然子进程已经无事可做,但是其在进程表中的项不会被删除,其退出码需要被保存,以备后续父进程的wait调用,这种进程被称为僵尸进程(Zombie,也称为死defunct进程)。
如果父进程异常终止,子进程自动把PID=1的进程(init)作为自己的父进程,这类僵尸进程会一直保存在进程表中直到init发现并释放它。
在Linux中,会话(Session)通常是指Shell会话,即会话的概念和Shell是分不开的。所谓Shell是用户访问Linux系统的接口,是用户与内核之间的桥梁。
在大部分的Linux发行版中,BASH是默认的Shell实现。每当你:
时,一个关联的新会话会被自动创建。不管本地还是远程登录,用户都会得到一个与终端(Terminal)相关联的(Shell)进程,该进程作为Session Leader,会话的ID就是该进程的PID。
你也可以编程式的创建新的会话,调用pid_t setsid(void) 函数可以让当前进程(不管它是否为Shell)作为Session Leader,创建新的会话。如果当前进程已经是Session Leader则会出错。新的会话中只有一个进程,并且它没有关联终端,因此你需要对其输入、输出进行重定向。
当终端被挂断(Hangup),即:
时,Session Leader会接收到SIGHUP信号而退出。在Session Leader退出前,它会向所有子进程也发送SIGHUP信号,通常会导致会话中所有进程都结束掉。要想让某个子进程超越Session的生命周期而存活,你可以:
顾名思义,进程组(Process group)就是包含了1~N个进程的分组。进程组的主要作用是利于信号的分发——当信号发送给进程组时,其内部的所有进程都会接收到信号。
一个会话中可以包含1~N个进程组,进程组不允许跨会话迁移,进程没有资格创建属于其它会话的进程组,进程也不能加入属于其它会话的进程组。
一个Session有且只有一个终端,该终端称为控制终端(controlling terminal),改变Session关联的终端这一操作,只能由Session Leader完成。
终端的生命周期可能:
软中断信号(简称信号,signal)用来通知进程发生了异步事件(通常是某种错误)。信号是进程之间(包括用户进程之间、用户进程与内核进程之间)进行通信的一种简单方式,信号不会给目标进程发送任何数据。
使用信号并挂起程序(例如pause调用)是Linux程序设计的一个重要部分,这意味着程序不需要总是在执行,在一个无限循环中检查某个事件是否发生,相反,它可以等待事件的发生。这种机制对于只有一个CPU的多任务环境非常重要,进程共享一个处理器,繁忙的循环会对系统性能造成极大的影响。
尽管用户可以通过命令手工发出信号,但是内核是主要的信号发出者。以下场景下会目标进程会收到信号:
在进程的进程表项(内核通过进程表对进程进行管理,每个进程在进程表中占有一项)中,有一个信号域,信号域的每一个slot对应一种信号,对于同一种信号,进程无法知道在处理前来过多少个。
内核通过设置进程的信号域对应slot,来给进程发送信号。
当内核将信号发送给正在睡眠的进程时:
进程在以下场景下检查自己是否收到信号:
进程接收到信号后,可以:
根据POSIX标准,信号为进程而产生,但是仅仅其中一个线程可以接收信号并处理。至于哪个线程负责处理信号,取决于实现。
对于Linux:
对于pthreads:每个线程具有独立的信号栈设置(signal stack settings),但是新线程总是从父线程拷贝此设置
| 信号 | No. | 说明 |
| SIGHUP | 1 |
如果进程通过终端运行,而终端忽然关闭后,进程将收到该信号。HUP是hang up的简写 在终端被关闭时,交互式的Shell会重新发送SIGHUP信号给所有任务(Jobs),不管是运行中的还是挂起(Stopped)的。挂起的任务还会收到SIGCONT信号,确保它们会处理SIGHUP。要阻止向某个任务发送SIGHUP,可以对其调用disown命令 对于BASH等Shell,调用exit、logout时,是否向所有任务发送SIGHUP取决于Shell选项:shopt | grep huponexit ,该选项默认值是off,即不发送 |
| SIGINT | 2 | 进程被中断(interrupted),当通过终端按ctrl + c导致进程接收到该信号 |
| SIGQUIT | 3 | 与SIGINT类似,只是该信号是由ctrl + \,该信号会在终结进程时生成core dump(核心转储,即进程的内存映像,可以后续分析) |
| SIGILL | 4 | 非法(Illegal)指令。程序执行了CPU无法理解的机器码时将收到该信号 |
| SIGTRAP | 5 | 主要用于调试和程序跟踪 |
| SIGABRT | 6 | 程序调用abort()函数时触发,导致程序紧急停止 |
| SIGBUS | 7 | 尝试以错误的方式访问内存时触发 |
| SIGFPE | 8 | 程序中出现浮点数异常(floating point exception)时触发 |
| SIGKIL | 9 | 立即终止进程,该信号不能被忽略。可以由ctrl + c引发 |
| SIGUSR1 | 10 | 供编程人员使用 |
| SIGSEGV | 11 | 段错误,无效内存段访问,尝试越界访问内存(不是分配给当前进程的内存)时触发 |
| SIGUSR2 | 12 | 供编程人员使用 |
| SIGPIPE | 13 | 当进程通过管道机制,将信息输出给目标进程的输入时,目标进程挂掉,当前进程收到此信号 |
| SIGALRM | 14 | 进程调用alarm()函数,定时器到期后,系统通过该信号提示进程 |
| SIGTERM | 15 | 这是一个一般的、用于“礼貌的”终结进程的信号。与SIGKIL不同,该信号可能被阻塞、处理或者忽略 |
| SIGCHLD | 17 | 进程先前通过fork() 创建了子进程,这些子进程中的一个或者多个挂掉时,父进程收到此信号 |
| SIGCONT | 18 | 可以使由SIGSTOP导致休眠的进程恢复 |
| SIGSTOP | 19 | 如果系统发送该信号给进程,进程的状态将被保存,并且不再获得CPU周期 |
| SIGTSTP | 20 | Terminal SToP。本质上与SIGSTOP一样,该信号由终端操作ctrl + z导致 |
| SIGTTIN | 21 | 当后台运行的进程尝试从stdin读取数据时,系统发送该信号给它。目标进程的典型响应是进入暂停,一直到进入前台时,SIGCONT信号到达 |
| SIGTTOU | 22 | 类似于SIGTTIN,当后台进程尝试写数据到stdout时触发 |
| SIGURG | 23 | 带外数据(out-of-band,OOB)到达时,使用网络连接的进程接收到该信号。带外数据不使用与普通数据相同的通道。对于TCP协议,由于没有所谓带外通道,是通过URG位实现的。带外数据通常是一些紧急的重要数据 |
| SIGXCPU | 24 | 系统发送该信号到使用CPU到达限制的进程 |
| SIGXFSZ | 25 | 系统发送该信号到尝试创建超过尺寸限制的文件的进程 |
| SIGVTALRM | 26 | 与SIGALRM类似,但不通过真实时间计时,而是通过目标进程使用的的CPU时间计时 |
| SIGPROF | 27 | 与SIGVTALRM类似,但是计时除了目标进程使用的CPU时间,而包括为了目标进程服务的系统代码执行时间 |
| SIGIO | 29 | 亦即SIGPOLL。当有输入等待进程处理,或者输出通道可以供进程写入时,系统给进程发出该信号 |
| SIGPWR | 30 | 当切换到紧急备用电源时,进程接收到该信号 |
| SIGSYS | 31 | 未使用 |
通过signal可以设置信号的处理函数:
#include <signal.h> /** * 设置信号的处理函数 * @param sig 信号 * @param func 处理回调函数 * @return 返回先前的信号处理函数的指针,如果未定义信号处理函数返回SIG_ERR并设置errno为正数 * 如果给出一个无效的信号,或者尝试处理不可捕获、不可忽略的信号(例如SIGKILL),则将errno设置为EINVAL */ typedef void (*signal_handler_t )( int ); signal_handler_t signal( int sig, signal_handler_t ); //两个特殊的信号处理函数: // SIG_IGN 忽略信号 // SIG_DFL 恢复此信号的默认处理行为 #include <unistd.h> /** * 导致当前进程暂停执行,直到接收到一个信号 * 当暂停被一个信号中断时,返回-1并且设置errno为EINTR */ int pause( void );
信号处理函数中调用某些函数是不安全的,例如printf,最好是在信号处理函数中设置一个标记,然后在主程序中检查标记再调用某些函数。下面是信号处理函数的例子:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void ouch( int sig )
{
printf( "OUCH! - I got signal %d\n", sig );
//该信号处理函数恢复默认行为:停止程序
signal( SIGINT, SIG_DFL );
}
int main()
{
//进程启动后,设置信号的处理函数
signal( SIGINT, ouch );
while ( 1 )
{
//此循环会不停执行,除非接收到信号
printf( "Hello World!\n" );
sleep( 1 );
}
}
X/Open和UNIX规范推荐了更加健壮的信号编程接口:
#include <signal.h>
/**
* 指定接收到sig信号后采取的动作
* @param sig 处理的信号
* @param act 需要指向的动作
* @param oact 如果不为空,此函数调用前sig信号的处理动作被转储到该指针
* @return 如果成功,返回0,失败返回-1,如果给出的信号无效或者对不允许忽略或者
* 捕获的信号进行忽略或者捕获,则设置errno=EINVAL
*/
int sigaction( int sig, const struct sigaction *act, struct sigaction *oact );
typedef void (*sa_handler_t)( int );
struct sigaction
{
sa_handler_t sa_handler; //信号处理函数,包括SIG_DFL、SIG_IGN
//一个信号集,在sa_handler被调用之前,此信号集中的信号不会传递给进程,
//可以防止信号处理函数尚未执行完毕就接收到新信号并重入信号处理函数的情况
//信号处理函数在指向过程中,可能被新的信号中断而再次调用,这不仅仅是递归调用
//的问题,更牵涉到可重入(安全的进入和再次指向)的问题
sigset_t sa_mask;
//标记位:
//SA_RESETHAND 表示处理函数调用后(入口第一句后),即重置默认处理函数(SIG_DFL)
//SA_NOCLDSTOP 子进程停止后不产生SIGCHLD信号
//SA_RESTART 重启可中断函数而不是给出EINTR错误。许多系统调用是可中断的,也就是
// 说接收到信号后系统调用会返回一个错误并设置errno=EINTR
// 以表示该调用因为信号而返回。设置该标记后,调用将重启而不是被信号中断
//SA_NODEFER 捕获到信号时不将其加入到信号屏蔽掩码中。通常的做法:为防止同一信号不
// 断到达,新接收到的信号会被加入到掩码中,直到处理函数指向完毕
int sa_flags;
};
//向信号集中增加一个信号
int sigaddset( sigset_t *set, int signo );
//创建空白信号集
int sigemptyset( sigset_t *set );
//创建包含所有已定义信号的信号集
int sigfillset( sigset_t *set );
//从信号集中删除指定的信号
int sigdelset( sigset_t *set, int signo );
//判断信号是否为信号集的成员,如果是返回1否则返回0,如果给定的信号无效返回-1并设置EINVAL
int sigismember( sigset_t *set, int signo );
/**
* 根据how指定的方式修改进程的信号屏蔽掩码
* @param how
* SIG_BLOCK 将set加入到进程的掩码
* SIG_SETMASK 将进程的掩码设置为set
* SIG_UNBLOCK 从进程的掩码中删除set
* @param set 新的信号屏蔽掩码,如果设置为空,仅仅是把当前信号屏蔽掩码保存到oset
* @param oset 原先的信号屏蔽掩码
* @return 如果成功返回0;如果how无效返回-1并设置errno=EINVAL
*
*/
int sigprocmask( int how, const sigset_t *set, sigset_t *oset );
/**
* 如果一个信号被进程阻塞,就不会传递给进程而停留在待处理状态。
* 该函数可以查看阻塞的信号中那些处于待处理状态
*/
int sigpending( sigset_t *set );
/**
* 挂起进程自己,等待信号集中某个信号到达
* 如果接收到的信号终止了程序,该调用不会返回;否则返回-1并设置errno=EINTR
*/
int sigsuspend( const sigset_t *sigmask );
下面是对前一个信号处理例子的改写:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
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 = SA_RESETHAND;
sigaction( SIGINT, &act, 0 );
while ( 1 )
{
printf( "Hello World!\n" );
sleep( 1 );
}
}
信号处理函数中会遇到可重入问题,下表列出可以被信号处理函数安全调用的函数,他们本身是可重入、或者本身不会再生成信号:
进程可以调用kill函数来向包括其自身在内的进程发送一个信号。如果进程没有发送目标信号的权限,对kill的调用就会失败,失败的常见原因是目标进程是由另外一个用户所拥有。下面的函数可以用来发送信号:
#include <sys/types.h> #include <signal.h> /** * 将信号发送给指定进程,如果成功返回0,失败返回-1并设置errno,errno可以为以下值: * EINVAL 给定的信号无效 * EPERM 发送进程的权限不够,一般只能发送给同一用户的进程,超级用户可以发送信号给所有进程 * ESRCH 目标进程不存在 * */ int kill( pid_t pid, int sig ); #include <unistd.h> /** * 在延迟seconds秒以后,发送一个SIGALRM信号 * 每个进程只能有一个闹钟,因此后续的调用将导致重新计时并返回上一次闹钟设置的剩余秒数 * 如果seconds设置为0则取消闹钟请求 */ unsigned int alarm( unsigned int seconds );
Leave a Reply to Alex Cancel reply