基本概念
进程(Process):是操作系统进行资源分配的最小单位。一个进程是一个程序的一次执行过程。
进程和程序的区别
程序首先以文件的形式保存在磁盘当中 (双击之后)加载到了内存中 由cpu加载开始运行
这个进程被加载到内存之后除了原本的代码之外还多了一堆的数据
多出来的数据就是操作系统对于进程的描述 而我们将这一堆数据称为PCB(Process Control Block) 进程控制块
进程等于程序加上PCB
PCB
当我们使用ps指令的时候 我们可以看到系统中存在着大量的进程
每个进程都对应着一个PCB
每个PCB对应着一个进程的数据 它们之间使用双链表组织起来 我们可以通过PCB来找到并管理每个进程
这样子我们就把操作系统对于进程的管理转化为了对于双链表的增删查改
Linux下的PCB是test_struct结构体
task_struct是什么
进程控制块(PCB)的作用是描述进程 而Linux系统是使用c语言写出来的
我们在c语言中一般是使用一个结构体去描述一个对象 这个在linux描述进程的结构体我们就把它叫做task_struct
task_struct是PCB
task_struct一般会被储存在内存中
task_struct里面有什么
标示符: 描述本进程的唯一标示符 用来区别其他进程
状态: 任务状态 退出代码 退出信号等
优先级: 相对于其他进程的优先级
程序计数器(pc): 程序中即将被执行的下一条指令的地址
内存指针: 包括程序代码和进程相关数据的指针 还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据
I/O状态信息: 包括显示的I/O请求 分配给进程的I/O设备和被进程使用的文件列表
记账信息: 可能包括处理器时间总和 使用的时钟总和 时间限制 记账号等
其他信息
查看进程
我们可以通过两种方式来查看进程
通过系统目录查看
通过ps指令查看
通过系统目录查看进程
在根目录下有一个proc目录
这个目录中含有大量的进程信息
这些文件中有些是用数字表示的 而这些数字实际上就是程序的pid
通过ps指令查看进程
但是我们一般查看进程的时候使用的是这样子的语句
ps axj | head -1 && ps axj | grep xxx
这段命令分为两部分&& 连接的两个命令 如果前面执行成功了就会执行后面的语句
ps axj | head -1
这段命令的意思打出 ps axj的第一行
ps axj | grep xxx
这段命令的意思是打出所有包含xxx的进程
获取程序的pid和ppid
pid : pid是程序的标识符
ppid: ppid是当前进程的父进程的标识符
getpid() 在 unistd.h 头文件里面
int main()
{
while(1)
{
printf("I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
return 0;
}
通过fork函数创建进程
fork是一个系统调用级别的函数 其功能就是创建一个子进程
我们可以通过如下的代码来验证它
int main()
{
int ret = fork();
while(1)
{
printf("I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
return 0;
}
如何理解fork创建进程
从创建层面 不论是使用指令 跑代码还是使用fork创建进程 在操作系统眼中都没有区别
从继承层面 fork出来的子进程它本身没有代码和数据 所以说它是拷贝的父进程的代码和数据(写时拷贝)
代码和数据是全部拷贝过来吗?
对于代码来说 是的 但是一般创建进程之前的代码是用不到的 因为已经运行到创建进程(上下文)
对于数据来说父子进程的数据也就是PCB大部分是共享的 但是我们也要考虑修改的情况
因为进程具有独立性 这里就要用到一个写时拷贝的技术
关于父子进程
可以通过pid的返回值来区分父子进程 从而达到同时进行两个任务的目的
fork函数的返回值:
如果子进程创建失败返回-1
如果子进程创建成功 在父进程中返回子进程的pid 在子进程中返回0
int main()
{
int ret = fork();
if(ret > 0)
{
printf("you can see me!!!\n");
}
else
{
printf("oops! you can see me!!!\n");
}
sleep(1);
printf("hello world\n");
sleep(1);
}
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
pid_t id = fork(); // int
if(id == 0)
{
//child
while(true)
{
cout << "I am child, pid: " << getpid() << ", ppid:" << getppid() << endl;
sleep(1);
}
}
else if(id > 0)
{
//father
while(true)
{
cout << "I am parent, pid: " << getpid() << ", ppid:" << getppid() << endl;
sleep(2);
}
}
else
{
//TODU
}
//cout << "hello proc: " << getpid() << " hello parent: " << getppid() << "ret: " << id << endl;
sleep(1);
return 0;
}
这就是两个进程同时运行的结果
在做业务的时候我们可以将里面的代码换成业务逻辑就可以了
进程的状态
在进程从创建到消亡的这段时间会存在不同的状态 这些状态值储存在PCB当中 操作系统会根据PCB中的状态值来决定这个进程是否运行 结束
常见的进程状态有以下几点
在我们的Linux源码中对于状态有着如下的定义
/*
* 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 *task_state_array[] =
{
"R (running)", /* 0*/
"S (sleeping)", /* 1*/
"D (disk sleep)", /* 2*/
"T (stopped)", /* 4*/
"T (tracing stop)", /* 8*/
"Z (zombie)", /* 16*/
"X (dead)" /* 32*/
};
运行状态 R (run)
一个进程处于运行状态(running) 并不意味着进程一定处于运行当中,运行状态表明一个进程要么在运行中 要么在运行队列里 可以同时存在多个R状态的进程
浅度睡眠状态 S (sleep)
我们让这个进程休眠30秒
此时我们可以使用 kill -9 + pid 结束进程
深度睡眠状态 D (disk sleep)
一个进程处于深度睡眠状态(disk sleep)表示该进程不会被杀掉 即便是操作系统也不行,只有该进程自动唤醒才可以恢复 该状态有时候也叫不可中断睡眠状态(uninterruptible sleep) 处于这个状态的进程通常会等待IO的结束
例如 某一进程要求对磁盘进行写入操作 那么在磁盘进行写入期间 该进程就处于深度睡眠状态 是不会被杀掉的 因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答(磁盘休眠状态)
暂停状态-T (stop)
在Linux当中 可以通过发送SIGSTOP信号使进程进入暂停状态(stopped)
发送SIGCONT信号 该进程就继续运行
僵尸状态 Z(zombie)
当一个进程将要退出的时候 在系统层面 该进程曾经申请的资源并不是立即被释放 而是要暂时存储一段时间 以供操作系统或是其父进程进行读取 如果退出信息一直未被读取 则相关数据是不会被释放掉的 一个进程若是正在等待其退出信息被读取 那么我们称该进程处于僵尸状态(zombie)
僵尸状态的存在是必要的
因为如果该进程的申请的资源在其运行结束之后立即释放那么我们便不知道该进程完成的如何
要知道它完成的如何我们必须要它的调用者来接受它的返回值 在了解完毕之后才可以让这个进程终止
我们在写c语言代码的时候通常在最后加上一个返回值
int main()
{
// ..
return 0;
}
这个return的值实际上是返回给操作系统的
我们可以使用 echo $? 命令来查询上次命令的返回值
死亡状态 X (dead)
我们一般不能查询到进程的死亡状态 因为在进程死亡的那一瞬间该进程的所有资源将会被释放 该进程也不存在了
僵尸进程
处于僵尸状态的进程就叫做僵尸进程
我们可以制造一个僵尸进程 具体操作就是让父进程一直运行 然后子进程退出 这样子的话子进程就一直无法将返回值传递给父进程 那么子进程就会一直处于僵尸状态
代码如下
int main()
{
pid_t id = fork(); // int
if(id == 0)
{
//child
cout << "I am child, pid: " << getpid() << ", ppid:" << getppid() << endl;
}
else if(id > 0)
{
//parent
while(true)
{
cout << "I am parent, pid: " << getpid() << ", ppid:" << getppid() << endl;
sleep(2);
}
}
else
{
//TODU
}
return 0;
}
可以看到子进程的状态就变成了Z 僵尸状态
僵尸进程的危害
僵尸进程的退出状态必须一直维持下去 因为它要告诉其父进程相应的退出信息 可是父进程一直不读取 那么子进程也就一直处于僵尸状态
僵尸进程的退出信息被保存在task_struct(PCB)中 僵尸状态一直不退出 那么PCB就一直需要进行维护
若是一个父进程创建了很多子进程 但都不进行回收 那么就会造成资源浪费 因为数据结构对象本身就要占用内存
僵尸进程申请的资源无法进行回收 那么僵尸进程越多 实际可用的资源就越少 也就是说 僵尸进程会导致内存泄漏
孤儿进程
在Linux中的进程大多是父子关系 万一 一个进程的子进程还没有结束但是他的父进程结束了 那么就称这个进程为孤儿进程
如果没有父进程来管理这个子进程那么他就会一直占用资源 所以说为了防止这种情况 我们设计了让1号init进程来领养这个进程
代码如下
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
while(true)
{
cout << "I am child, pid: " << getpid() << ", ppid:" << getppid() << endl;
sleep(3);
}
}
else if(id > 0)
{
//parent
cout << "I am parent, pid: " << getpid() << ", ppid:" << getppid() << endl;
}
else
{
//TODU
}
return 0;
}
子进程的父进程变为了 1 (init进程)