进程的基本状态
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)。
下面的状态在 fs/proc/array.c 文件中定义:
static const char* const task_state_array[] = {
"R (running)",/* 0 */
"S (sleeping)",/* 1 */
"D (disk sleep)",/* 2 */
"T (stopped)",/* 4 */
"t (tracing stop)",/* 8 */
"X (dead)",/* 16 */
"Z (zombie)",/* 32 */
};
其中,我分别进行一下简单的介绍:
①R(running)表示运行状态,但是这并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列中。
②S(sleeping)表示睡眠状态,意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
③D(disk sleep)表示磁盘休眠状态,有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束,这是一种特殊的S状态,不能被打断,仅能通过自己醒来。
④T(stopped)表示停止状态,可以通过发送SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
⑤t(tracing stop)表示追踪停止状态。
⑥X(dead)表示死亡状态。
⑦Z(zombie)表示僵尸状态,是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
进程的优先级
进程cpu资源分配就是指进程的优先权(priority)。优先权高的进程有优先执行权利。
配置进程优先权对多任务环境的linux很有用,可以改善系统性能。还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。
一、先看系统进程:
无论在linux或者unix系统中,用ps –l命令则会类似输出以下几个内容:我们很容易注意到其中的几个重要信息,有下:
①UID:代表执行者的身份
②PID:代表这个进程的代号
③PPID:代表这个进程由哪个进程发展衍生而来,即父进程的代号
④PRI:代表这个进程可被执行的优先级,其值越小越早被执行
⑤NI:代表这个进程的nice值
其实,除了NI,其余的几个概念都是比较容易被理解的,但是NI是用来干嘛的呢?其表示进程可被执行的优先级的修正数值。如前面所说,PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice。 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行。
但是呢,现在更需要强调的一点是,进程的nice值不是进程的优先级,他们不是一个概念,但是进程nice值会影响到进程的优先级变化 。
二、修改进程优先级的命令主要有两个: nice,renice
1、一开始执行程序就指定nice值:nice2.1、调整已存在进程的nice:renice
renice -5 -p 2243 ---->nice值被调整为-5;
renice 15 -p 2243 ---->nice值被调整为15;
参数:
-g <程序群组名称> 使用程序群组名称,修改所有属于该程序群组的程序的优先权。
-p <程序识别码> 改变该程序的优先权等级,此参数为预设值。
-u <用户名称> 指定用户名称,修改所有属于该用户的程序的优先权。
2.2、也可以用top命令更改已存在进程的nice:
1.top
2.#进入top后按“r”–>输入进程PID–>输入nice值
进程的创建执行
当进程执行时,它会被装载进虚拟内存,为程序变量分配空间,并把相关信息添到task_struct中。
进程内存布局分为四个不同的段:
• 文本段,包含程序的源指令。
•数据段,包含了静态变量。
•堆,动态内存分区区域。
•栈,动态增长与收缩的段,保存本地变量。
这里有两种创建进程的方法, fork()和execve()。它们都是系统调用,但它们的运行方式有点不同。
要创建一个子进程可以执行fork()系统调用。然后子进程会得到父进程中数据段,栈段和堆区域的一份拷贝。子进程独立可以修改这些内存段。但是文本段是父进程和子进程共享的内存段,不能被子进程修改。
如果使用execve()创建一个新进程。这个系统调用会销毁所有的内存段去重新创建一个新的内存段。然后,execve()需要一个可执行文件或者脚本作为参数,这和fork()有所不同。注意,execve()和fork()创建的进程都是运行进程的子进程。
孤儿进程:
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集操作。
验证孤儿进程:
代码:
然后我们运行代码:
然后再通过kill指令杀死父进程
然后我们可以得到:
剩余的子进程就会被1号进程领养了。
僵尸进程:
一个子进程在其父进程没有调用wait()或waitpid()的情况下退出。这个子进程就是僵尸进程。如果其父进程还存在而一直不调用wait,则该僵尸进程将无法回收,等到其父进程退出后该进程将被init回收。先简单介绍一下wait()和waitpid()两个函数:
函数作用:父进程用来获取已终止进程的退出状态,并彻底清除该进程
wait函数调用后的情况
1、若该进程的所有子进程都在运行,则wait阻塞
2、若该进程的一个进程结束了,则获取该结束进程的死亡信息,返回
3、若该进程没有子进程,则出错返回
验证僵尸进程:代码:
运行代码:
可以看到子进程的pid是2824,然后我们通过grep去找到这个子进程
我们可以看到子进程的状态变成了Z,即子进程变成了僵尸状态。
子进程的异步等待方式
上面呢我们提到了僵尸进程是是可以通过调用wait和waitpid函数来清理的,父进程可以通过阻塞等待子进程结束,也可以非阻塞的查询是否有子进程结束等待清理(轮询的方式)。这里呢我们仔细介绍一下wait和waitpid两个函数
wait函数
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status)
函数说明:
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
参数:
参数 statloc 是一个整形指针。如果status不是一个空指针,则终止进程的终止状态将存储在该指针所指向的内存单元中。如果不关心终止状态,可以将 status参数设置为NULL。
返回值:
如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno 中。
waitpid函数
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int* status,int options);
函数说明:
从本质上讲,系统调用waitpid和 wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。
参数:
(1)pid:从参数的名字pid和类型 pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。
①pid>0时,等待进程ID等于 pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
②pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
③pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
④pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
(2)ststus与wait()函数的参数基本相同。
(3)options:
①当options参数为0时,与wait功能相同,仍是阻塞式等待,不提供额外功能,如果为下列常量按位或则提供更多功能:
②WCONTINUED:若实现支持作业控制,那么由pid指定的任一子进程在暂停后已经继续,但状态尚未报告,则返回状态
③WNOHANG:若由pid指定的子进程并不是立即可用的,则waitpid不阻塞,即此时以非阻塞方式(轮询式访问的必要条件)等待子进程,并且返回0。
④WUNTRACED:若实现支持作业控制,而pid指定的任一子进程已经暂停,且其状态尚未报告,则返回其状态
返回值:
waitpid的返回值比wait稍微复杂一些,一共有3种情况:
①当正常返回的时候,waitpid返回收集到的子进程的进程ID;
②如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
③如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;
另外,waitpid提供了三个wait所没有的功能:
①waitpid可等待一个特定的进程
②waitpid提供了一个wait的非阻塞版本
③waitpid支持作业控制
SIGCHLD
我这里再提一个信号的概念,这个信号叫做SIGCHLD,当子进程退出时,它会向父进程发送SIGCHLD信号,该信号的默认处理方式为忽略,当父进程以阻塞方式等待时,它不能处理自己的工作。 我们可以自定义一个捕捉信号的函数handler。
验证子进程退出时会给父进程发送SIGCHLD信号
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
void myhandler(int sig)
{
printf("get a sig : %d, pid = %d\n",sig, getpid());
}
int main()
{
signal(SIGCHLD, myhandler);
pid_t id = fork();
if(id == 0)//child
{
printf("i am child, pid = %d\n", getpid());
exit(1);
}
else//father
{
waitpid(id, NULL, 0);
}
return 0;
}
运行后得到
我们可以看到17就是SIGCHLD的值
验证父进程等待子进程的异步方式
父进程自定义SIGCHLD信号的处理函数,并采用非阻塞方式等待,当子进程退出时,会向父进程发送信号,父进程会进行回收。父子进程互不干扰,继续执行各自任务。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
void myhandler(int sig)
{
printf("father catch, child quit\n");
pid_t id;
while((id = waitpid(-1, NULL, WNOHANG)) > 0)
{
printf("wait child success %d\n", id);
}
}
int main()
{
signal(SIGCHLD, myhandler);
pid_t id = fork();
if(id == 0)//child
{
printf("i am child, quit pid: %d\n", getpid());
exit(1);
}
else//father
{
while(1)
{
printf("father is running\n");
sleep(1);
}
}
return 0;
}
然后运行程序得到
我们可以看到父进程做自己的事情,而且子进程退出之后等待成功,父进程非阻塞等待,继续做自己的事情。