Linux进程概念
1.进程的理解
1.1 进程的概念
基本概念:
操作系统理论概念:程序的一个执行实例,正在执行的程序等
Linux内核观点:担当分配系统资源(CPU时间,内存)的实体
- 核心理解:进程=可执行程序+该进程对应的内核数据结构
进程控制块(PCB)的理解:
- 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合
- 操作系统理论上称之为
PCB(process control block)
,Linux操作系统下的PCB是:task_struct
- 凡是提到进程,必须首先想到进程task_struct(PCB)
- PCB是操作系统描述进程的一个统称。当可执行程序加载到内存,是运行了一个进程。实际的大小要比文件本身要大。操做系统管理进程要先对进程进行描述,会添加一些属性(所以比本身文件大),属性包括描述信息+内容代码数据等
- 操作系统允许多个进程同时允许,为了方便管理,操作系统还会将进程组织起来,一般是组织成一个双向链表的数据结构,如下图:
1.2 task_struct与PCB的区别
- PCB是操作系统描述进程的一个统称
- task_struct是Linux下描述进程的结构体,是Linux内核的一种数据结构,它会被装载到内存里并且包含进程的信息
1.3 task_struct的内容分类(了解)
task_ struct内容分类:
标示符: 描述本进程的唯一标示符,用来区别其他进程
状态: 任务状态,退出代码,退出信号等
优先级: 相对于其他进程的优先级
程序计数器: 程序中即将被执行的下一条指令的地址
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
其他信息
1.4 组织进程和查看进程的方法
查看进程:进程的信息可以通过 /proc 系统文件夹
查看
如果要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹
大多数进程信息也可以使用top和ps这些用户级工具获取
单独使用ps命令,会显示所有进程信息:
ps aux
ps命令与grep命令搭配使用,即可只显示某一进程的信息:
ps aux | head -1 && ps aux | grep proc | grep -v grep
- 可以使用命令
kill -9 pid
杀死进程- proc文件:内存文件系统—当前系统实时的进程信息
- 每一个进程在系统中,存在一个唯一的标识符PID
1.5 进程标识符的获取方法
方法一:通过getpid()、getppid()函数获取
通过系统调用获取进程标示符:
子进程
标识符(PID
):getpid()父进程
标识符(PPID
):getppid()
举例代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
方法二:通过fork()函数获取
前提知识:
- fork是一个系统调用级别的函数,其功能就是创建一个子进程
- fork执行一次,有两个返回值,
父进程返回子进程PID
,子进程返回0
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
举例代码:打印父子进程pid:
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
pid_t id=fork();
//理论上:id:0 子进程 id>0 父进程
//实际上:子进程、父进程各自得到一个id进行条件判断
//父进程得到的变量id是子进程pid,子进程得到的id是0
if(id==0)
{
//child
while(1)
{
printf("我是子进程,我的pid:%d,我的父进程是:%d\n",getpid(),getppid());
sleep(1);
}
}
else
{
//parent
printf("我是父进程,我的pid:%d,我的父进程是:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
奇怪的现象:
- 这里遇到了一个奇怪的问题:这里的if和else同时执行了,进行了两次printf打印,并没有受到条件的约束,为什么呢?
- 首先我们思考,C语言上if 和else if可以同时执行嘛?当然不可以。有没有可能两个以上死循环?当然不可以
- 但是这里却发现Linux下可以!
出现这个现象的原因:
fork之后,父进程和子进程会共享代码,也就是写时拷贝了代码
(这就解释了为什么printf会打印两次的原因)
fork之后,父进程和子进程返回值不同,可以通过不同的返回值,进行判断让父子执行不同的代码块
(这就解释了为什么条件约束失效)
1.6 fork()函数有两个返回值的原因剖析
重点剖析:fork()为什么给父进程返回子进程pid,给子进程返回0?
父子关系:父亲—儿子:1:n(n>=1)
原因:
- 父进程必须要有标识子进程的方法:fork()之后,给父进程返回子进程的pid!
- 子进程最重要的是要知道自己被创建成功了,因为子进程找父进程成本非常低—使用getppid()
重点剖析:fork()之后父子进程谁先执行
- 通常情况下,我们总是用 sleep 等操作来保证另一个进程先执行,但父子进程谁先执行并不是不可预测的
- 从linux内核2.6.32开始,在默认情况下,父进程将成为fork之后优先调度的对象
- 采取这种策略的原因很简单:fork是父进程发起的调用,因此fork之后,父进程在CPU中处于活跃的状态,并且其内存管理信息也被置于硬件内存单元的转译后备缓冲器(TLB),所以先调度父进程无论从减少上下文切换、CPU让出等方面都可以提高性能
- linux内核从2.6.24开始,内核采用完全公平调度(CFS),用户创建的普通进程,都采用CFS调度策略。对于CFS调度策略,内核提供的控制选项默认是0,表示父进程优先获得调度,如果该值被改为1,子进程会优先获得调度。但POSIX标准和linux都没有保证会优先调度父进程,因此在应用中,我们不能对父子进程的执行顺序做任何假设
- 如果确实需要父子进程的某一特定执行顺序,那么还是得需要进程间的同步手段
所以:从理论上,优先调度父进程。从使用上,不可预测。
结论:进程调度顺序由操作系统内核和调度算法共同决定
重点剖析:为什么fork()会返回两次?
前提分析:
原因分析:
- 当程序执行到下面的语句:
pid=fork(); 由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回
。 因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。 fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
- 在父进程中,fork返回新创建子进程的进程PID
- 在子进程中,fork返回0
- 如果出现错误,fork返回一个负值
- 我们可以通过fork返回的值来判断当前进程是子进程还是父进程,其实就相当于链表,进程形成了链表,父进程的fork函数返回的值指向子进程的进程PID, 因为子进程没有子进程,所以其fork函数返回的值为0
- 调用fork之后,数据、堆、栈有两份,代码仍然为一份但是这个代码段成为两个进程的共享代码段都从fork函数中返回。当父子进程有一个想要修改数据或者堆栈时,两个进程真正分裂
- 子进程代码是从fork处开始执行的, 为什么不是从#include处开始复制代码的?这是因为fork是把进程当前的情况拷贝一份,执行fork时,进程已经执行完了当前情况; fork只拷贝下一个要执行的代码到新的进程
2.进程状态
2.1 Linux的进程状态
Linux系统的进制状态分类:
运行状态(R,running)
:并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
睡眠状态(S,sleeping)
: 意味着进程在等待事件完成,这里的睡眠有时候也叫做可中断睡眠 interruptible sleep
磁盘休眠状态(D,Disk sleep)
:有时候也叫不可中断睡眠状态(uninterruptible sleep)
,在这个状态的进程通常会等待IO的结束
暂停状态(T,stopped)
:可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行
死亡状态(X,dead)
:这个状态只是一个返回状态,你不会在任务列表里看到这个状态
僵尸状态(Z,zombie)
:是一个比较特殊的状态。当进程退出并且父进程,没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
杀死进程的方法:kill -9 进程PID
。ctrl+c只能杀前台进程,kill命令可以杀后台进程
Linux内核进程状态源代码:
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 */---僵尸 };
进程状态间的切换,如下图:
2.2 进程阻塞与进程挂起的理解
进程阻塞:
1.一个进程使用资源的时候,可不仅仅是在申请CPU资源
2.进程可能申请更多的其他资源:磁盘,网卡,显卡等
3.如果我们申请CPU资源,暂时无法得到满足,是需要排队的,即进入运行队列
进程挂起:
1.挂起进程在操作系统中可以定义为暂时被淘汰出内存的进程,机器的资源是有限的,在资源不足的情况下,操作系统对在内存中的程序进行合理的安排,其中有的进程被暂时调离出内存,当条件允许的时候,会被操作系统再次调回内存,重新进入等待被执行的状态,表现为系统在一定的时间没有任何动作
2.3 进程状态查看方法
进程状态的查看:ps aux
/ ps axj
//常用的进程状态查看方法
ps aux|head -1 && ps aux|grep 进程PID
ps ajx|head -1 && ps ajx|grep 进程PID
- 系统原生的用法不好用,如下图:
- ps aux:显示所有进程 有效用户ID或名字
- ps ajx:显示所有进程 PPID、PID、PGID、SID、COMMAND等
ps a //显示现行终端机下的所有程序,包括其他用户的程序
ps u //以用户为主的格式来显示程序状况。
ps x //显示所有程序,不以终端机来区分
ps aux //显示有效用户ID或名字
ps ajx //显示PPID,PID,PGID,SID,UID,COMMAND等
ps awx //显示完全程序参数
2.4 进程状态:运行状态R
测试方法:使用命令top,查看进程状态
效果:
这里我们可以看到一些进程状态时R,这些标识为R的进程就是正在运行或者进入运行队列的进程
2.5 进程状态:睡眠状态S
测试代码:
#include<stdio.h>
int main()
{
printf("Process is running...\n");
printf("Process PID:%d\n",getpid());
sleep(1000);
return 0;
}
效果:
- 这里我们看到子进程8130的运行状态就是S状态
- 一个进程处于浅度睡眠状态(sleeping),意味着该进程正在等待某件事情的完成,处于浅度睡眠状态的进程随时可以被唤醒,也可以被杀掉(这里的睡眠有时候也可叫做可中断睡眠(interruptible sleep))
2.6 进程状态:深度睡眠状态D
- 一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束
- 例如,某一进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)
2.7 进程状态:暂停状态T
在Linux当中,我们可以通过发送SIGSTOP信号
使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行
例如:
我们再对该进程发送SIGCONT信号,该进程就继续运行了:
2.8 进程状态:死亡状态X
- 死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)
2.9 进程状态:僵尸状态Z*
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
举例代码:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
printf("I am running...\n");
pid_t id=fork();
if(id==0)
{
//child
int count=3;
while(count)
{
printf("I am chile,pid:%d,ppid:%d,count:%d\n",getpid(),getppid(),--count);
sleep(1);
}
printf("child quit...\n");
exit(1);
}
else if(id>0)
{
//father
while(1)
{
printf("I am father,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
从上图我们就看到了子进程24337的状态由S+变成了Z+,进入僵尸状态的进程我们称它为僵尸进程!
僵尸进程对于我们的操作系统来说是有极大危害的!
僵尸进程危害:
1.进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态
2.维护退出状态本身要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,
Z状态一直不退出,PCB一直都要维护
3.那一个父进程创建了很多子进程,就是
不回收
,就会造成内存资源的浪费
,因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!4.
导致内存泄漏
【常见问题:1.频繁GC(垃圾回收机制),发生GC的时候,所有进程都必须等待,GC频率越高,就感觉系统越卡顿。2.内存不足引发的程序运行崩溃】
2.10 特殊的进程状态:孤儿状态
- 若子进程先退出而父进程没有对子进程的退出信息进行读取,那么我们称该进程为僵尸进程
- 但若是父进程先退出,那么将来子进程进入僵尸状态时就没有父进程对其进行处理,此时该子进程就称之为"孤儿进程"
- 孤儿进程被1号init进程领养,当然要有init进程回收,1号init就是操作系统
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}else
{//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
3.进程优先级
3.1 进程优先级基本概念
cpu资源分配的先后顺序,就是指进程的优先权(priority)
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
核心问题:
1.什么是优先级?什么是权限?
优先级是进程获取资源的先后顺序
权限是能还是不能获取资源
2.为什么存在优先级?
排队的本质叫做确定优先级,资源总是不够的,需要优先分配给重要的进程
3.查看进程优先级方法?
//Linux Unix系统下,用ps -l命令监控运行的进程的详细信息
ps -l
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值
3.2 对于PRI和NI的理解
- PRI代表进程的优先级(priority),通俗点说就是进程被CPU执行的先后顺序,该值越小进程的优先级别越高,默认是80
- NI代表的是nice值,其表示进程可被执行的优先级的修正数值
- PRI值越小越快被执行,当加入nice值后,将会使得PRI变为:PRI(new) = PRI(old) + NI
- 若NI值为负值,那么该进程的PRI将变小,即其优先级会变高
- 调整进程优先级,在Linux下,就是调整进程的nice值
NI的取值范围:-20~19,40个级别
PRI的取值范围:60~99,每次设置优先级都会被恢复为默认的80
- Linux不允许进程无节制的设置优先级
注:需要强调一点的是,进程的nice值不是进程的优先级,,他们不是一个概念,但是进程nice值会影响到进程的优先级变化。 可以理解nice值是进程优先级的修正数据
3.3 查看和修改进程优先级命令
- 查看进程优先级的命令:
top
- top命令用于显示系统运行的进程信息,作用类似于windows中的任务管理器,只不过top不是图形化的,而是显示实时文本信息
- 修改进程优先级的命令:
修改进入top后按“r”–>输入进程PID–>输入nice值
- 注:每次设置优先级,这个PRI(old)优先级都会恢复为80
修改步骤:
- 输入top,查看进程优先级
- 输入r,进入修改功能
- 输入要修改的PID并输入nice值,这里以PID 3155举例,我们改nice为19
- 最后我们来看看3155进程的NI就变成了19
这里就可以很清晰的看到了,我们原来的3155进程PR是20,NI修改为19后,PR就变成了39=20+19