Linux进程
文章目录
基本概念
进程是程序的一个执行实例,是一个正在执行的程序。从内核来谈,进程是操作系统进行资源分配的最小单位。
那进程和程序的区别是什么呢?我来举个例子,我们对一段代码进行编译后就会生成一个可执行程序,这个可执行程序本质上是一个存在磁盘上的文件,当我们运行这个文件后,相当于我们把这个文件从磁盘上加载到了内存中,因为CPU只会处理内存中的信息,当这个文件加载到内存后,我们就称这个文件叫进程。而没有加载到内存前,我们称这个文件叫程序。
这么多的进程,我们操作系统是怎么进行管理的呢,这就是我们上个文章所说的先描述,后组织。
程序控制块PCB
PCB(Process Control Block) 程序控制块存放的是每一个进程的信息,它是一种数据结构,我们可以简单的理解为进程属性的集合。
操作系统对每一个进程进行了详细的描述形成了一个个PCB(先描述),然后以链表的形式组织起来(后组织)。操作系统只需要拿到链表的头指针就可以访问所有的进程,操作系统管理各个进程就转变为管理这个链表。
创建一个进程、关闭一个进程、找到某个进程、修改某个进程就是对这个链表的增删查改。
task_struct
进程控制块是描述一个进程的,Linux操作系统是用C语言编写出来的,那它的进程控制块必然是用结构体实现的。
task_struct
{
- 标识符:描述本进程的唯一标示符,用来区别其他进程。
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据:进程执行时处理器的寄存器中的数据。
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟总和,时间限制,记账号等。
- 其他信息
}
查看进程
我们分为两种方法:
方法1.通过系统目录
在根目录下,有一个名为proc
的系统文件
我们使用以下命令来查看进程信息,其中第一列的数字就是这些进程的PID,对应文件夹记录者各种信息。
[---@VM-8-4-centos /]$ ls /proc/
我们再使用以下命令来查看PID为1的进程信息。
[---@VM-8-4-centos /]$ ls /proc/1
方法2.使用命令ps
我们只单独使用ps命令的话会显示所有进程信息。
[---@VM-8-4-centos /]$ ps ajx
我们把ps和grep命令结合起来,可以显示我们想要看到的进程信息。
[---@VM-8-4-centos /]$ ps ajx | head -1 && ps ajx | grep 11
为什么不grep 1,因为包含数字1的进程信息太多了。
查看进程PID和PPID
-
PID: 进程的标识符
-
PPID: 当前进程的父进程的标识符
系统调用:getpid()
|getppid()
我们通过一段代码来查看进程的PID和PPID。
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(1)
{
cout << "process : PID->" << getpid() << ", PPID->" << getppid() << endl;
sleep(2);
}
return 0;
}
有了PID,我们可以通过我们刚刚学习到的命令ps来查询这个进程到底是不是存在的。
因为我刚刚重新运行了程序,所以PID更换了。
[---@VM-8-4-centos fork1]$ ps ajx | head -1 && ps ajx | grep 449
fork创建进程
函数调用:fork
: 创建一个子进程。
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
fork();
while(1)
{
cout << "process : PID->" << getpid() << ", PPID->" << getppid() << endl;
sleep(2);
}
return 0;
}
fork出来的子进程也有自己的PID,也能证明操作系统给每个进程都创建PCB。
那么,怎么才能让父子进程各自执行自己的代码呢?
fork的返回值
- 创建成功,父进程中返回子进程的PID,子进程中返回0。
- 创建失败,父进程中返回-1。
使用下面的代码,让我们更深入的体会一下。
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
pid_t id = fork();
if (id > 0)
{
while (1)
{
cout << "我是父进程, PID->" << getpid() << endl;
sleep(2);
}
}
else if(id == 0)
{
while (1)
{
cout << "我是子进程, PID->" << getpid() << endl;
sleep(2);
}
}
else
{
perror("fork fail");
}
return 0;
}
Linux进程状态
一般一个进程从被创建到消亡都会存在不同的状态,以下是kernel源码的定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
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 */
};
我们刚刚在PCB中提到了,PCB是保存进程各种信息的地方,所以状态信息也在PCB中,也就是task_struct
中。
我们可以使用以下命令来查看进程状态:
[---@VM-8-4-centos ~]$ ps ajx
或者使用
[---@VM-8-4-centos ~]$ ps aux
1 运行状态
R-运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
可被调度的进程都被放在一个运行队列中,当需要时在运行队列中选取运行。
2 浅度睡眠状态
S-睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep))。
我们通过一段代码进行理解:
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
cout << "process : PID->" << getpid() << endl;
sleep(100);
return 0;
}
我们运行后得到PID是8209:
[wsj@VM-8-4-centos day01]$ ps aux | head -1 && ps aux | grep 8209
我们发现sleep后,进程确实是浅度睡眠状态。
3 深度睡眠状态
(DISK SLEEP)D-深度睡眠状态:有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。
一个程序若要求磁盘进行写入操作,在磁盘写入这个过程中,进程就会进入D状态。该进程会等待磁盘的回复以作出应答。
4 暂停状态
T-暂停状态:在Linux中,我们可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
我们对刚才的程序进行发送SIGSTOP信号,我们可以发现它的状态从S变为了T
[wsj@VM-8-4-centos day01]$ kill -SIGSTOP 13251
我们再发送SIGCONT信号,则发现进程13251又变回了最开始的状态:
[wsj@VM-8-4-centos day01]$ kill -SIGCONT 13251
5 死亡状态
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
6 僵尸状态
Z-僵尸状态:僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。
当一个进程将要退出的时候 在系统层面 该进程曾经申请的资源并不是立即被释放 而是要暂时存储一段时间 以供操作系统或是其父进程进行读取 如果退出信息一直未被读取 则相关数据是不会被释放掉的 一个进程若是正在等待其退出信息被读取 那么我们称该进程处于僵尸状态(zombie)。
僵尸进程
什么是僵尸进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
例如:
int main()
{
pid_t id = fork();
if(id > 0)
{
while(1)
{
cout << "我是父进程,PID->" << getpid() << endl;
sleep(5);
}
}
else if(id == 0)
{
cout << "我是子进程, PID->" << getpid() << endl;
sleep(6);
exit(2);
}
else
{
perror("fork fail");
}
return 0;
}
运行代码6秒后子进程会直接退出。子进程会等待父进程读取退出状态代码,但父进程还在运行,没有读取子进程状态码,子进程进入僵尸状态Z。
僵尸进程的危害
- 因为子进程要告诉父进程退出信息,可父进程一直不读取,那么子进程就一直处于僵尸状态。
- 僵尸进程的退出信息被存在
task_struct
(PCB)中,僵尸进程不退出,PCB就一直需要进行维护。 - 如果父进程创建非常多子进程,但是不回收,就会造成资源浪费,因为数据结构对象本来就需要占用内存。
- 僵尸进程申请的资源若不进行回收,那僵尸进程越多,可用资源就越少。这就是内存泄露。
孤儿进程
什么是孤儿进程
子进程退出,父进程一直不读取子进程退出信息,我们称它为僵尸进程。
如果父进程先退出,但是子进程还未结束,我们就称它为孤儿进程。如果没有父进程回收这个子进程,这个子进程就一直占用资源造成内存泄露。为了避免这种情况,当出现孤儿进程时,孤儿进程就会被1号init
进程领养,当子进程进入僵尸状态时由init
进行回收。
我们通过一段代码体会一下:
int main()
{
pid_t id = fork();
if (id > 0)
{
int count = 0;
while (1)
{
cout << "我是父进程,PID->" << getpid() << endl;
sleep(1);
if (count == 5)
{
cout << "我是父进程,我要退出了" << endl;
exit(2);
}
count++;
}
}
else if (id == 0)
{
while (1)
{
cout << "我是子进程,PID->" << getpid() <<", PPID->"<< getppid()<< endl;
sleep(1);
}
}
else
{
perror("fork fail");
}
return 0;
}
如果需要关掉这个孤儿进程,我们只需要输入以下命令即可:
[---@VM-8-4-centos day01]$ kill -9 30470
这条命令的原理,我们在后面讲信号时会解释。
感谢您的阅读,欢迎指正。