Linux系统进程(进程状态、进程地址空间、写实拷贝)
基本概念
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
描述进程-PCB
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block), Linux操作系统下的PCB是: task_struct结构体
task_struct - PCB的一种
在Linux中描述进程的结构体叫做task_struct。
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct含有的内容分类
其实就是task_struct结构体含有的成员属性表征的意思
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据: 进程执行时处理器的寄存器中的数据。
I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
这样OS对进程的创建和退出本质就是对链表的管理
常见进程相关命令及头文件
PID,指当前进程ID
ps -aux
ps -e 或 ps -A:显示所有进程。
ps -u 用户名:显示指定用户的进程。
ps -ef:显示所有进程的详细信息,包括进程ID、父进程ID、CPU使用率等。
ps -aux:显示所有进程的详细信息,包括用户、CPU使用率、内存使用情况等。
杀死进程
kill -9 进程号
-9是参数,这个命令相当于crtl + c终止进程
头文件
#include<sys/types.h>
#include<unistd.h>
获取进程的调用接口
pid_t getpid(void);//获取当前进程的PID
pid_t getppid(void);//获取当前进程的父进程PID
将系统当前运行的所有进程显示在文件系统当中
获取进程pid
头文件(由操作系统提供)
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);//获取当前进程的PID
pid_t getppid(void);
pid_t是操作系统提供的一种数据类型,本质是无符号整数类型
完整代码
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
while(1);
pid_t id=getpid();//获取自己的进程
printf("corrent ID is:%d\t ppid:",id,getppid());
sleep(1);//睡觉1秒
}
创建子进程-fork函数
通过代码方式创建子进程
头文件和函数
#include<unistd.h>
pid_t fork(void);
fork函数返回值,创建失败返回-1,
创建成功
a.给父进程返回子进程的pid,
b.给子进程返回0,
也就是说,fork创建成功会返回两个值
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main()
{
printf("I am parent process:pid:%d\n",getpid());
pid_t ret=fork();//创建一个进程,使变成两个进程,fork上面代码是父进程,下面代码是子进程和fu
printf("ret:%d pid:%d ppid:%d\n",ret,getpid(),getppid());
sleep(1);//睡觉1秒
}
从上图结果可知,fork函数创建后,形成两个进程,
父进程打印"ret:2973 pid:2972 ppid:2082",其中2973是子进程的ID
子进程打印"ret:0 pid:2973 ppid:2972",其中2972是父进程的id,2973是子进程自己的id,ret是0是因为fork创建成功会给子进程返回一个0
fork函数之后的代码是共享的
#include<stdio.h>
#include<unistd.h>
int main()
{
pid_t id=fork();
if(id<0)
{
//创建子进程失败
perror("fork");
return 1;
}
else if(id==0)
{
//子进程
while(1)
printf("I am child,pid:%d ppid:%d",getpid(),getppid());
}
else
{
//父进程
while(1)
printf("I am father,pid:%d ppid:%d",getpid(),getppid());
}
printf("you can see me!\n");
sleep(1);
return 0;
}
这段代码中,子进程和父进程的代码都会被执行,也就说,两个进程的while死循环都会被执行
这是因为fork之后,有两个不同的执行流,id变量在父进程里面是子进程的pid ,而在子进程里是0,根据这样的情况,可以通过if让两个进程执行不同的代码,所以父进程执行了else这部分的代码,而子进程执行了else if(id==0)这部分的代码
一个父进程可以有多个子进程,而一个子进程只有一个父进程
操作系统和cpu运行某一个进程,本质从task_struct形成的队列中挑选一个task_struct来执行该进程的代码。进程调度,变成了在task_struct形成的队列中选择一个进程的过程
id为啥会有两个返回值?
fork()
{
//创建子进程的逻辑
return id;
}
1.因为fork函数内部,有个return id,等父进程执行到return id时,子进程已经被创建出来并且可能排在进程调度队列中等待调度,这时父进程执行完return id,该id就是子进程的id,id被更新
2.id变量是前后保存了两个不同的值,而不是一个变量同时保存了不同的值
linux进程的状态
新建:
运行:task_struct结构体在运行队列中排队,就叫做运行态
阻塞:等待非cpu资源就绪,该进程所处的状态就是阻塞状态,等待的队列称为阻塞队列
挂起:当内存不足的时候,os适当的置换进程的代码和数据到磁盘,此时cpu只有进程的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 * 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): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。R对应运行态。
S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。S对应阻塞态。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),深度睡眠,在这个状态的进程通常会等待IO的结束。
T停(暂停)状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
进程可以分为前台进程和后台进程(&,意思是在后台运行)
僵尸进程
是什么
个人理解:一个进程已经退出,但是不允许被os释放,处于一个被检测的状态
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
创建维持30秒僵尸进程的例子
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id > 0){ //parent
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
}else{
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
结果:
另一个终端监视子进程的状态变化
子进程大状态变为僵尸状态
僵尸进程的危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护,而此时的PCB一直保存在内存中,占用内存,造成内存泄漏
一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费。因为数据结构对象本身就要占用内存,如同C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间的
孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”,此时的孤儿进程被1号init进程领养,要有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.Myfather id is: %d", getpid(), getppid());
sleep(10);
}
else
{ // parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
进程优先级
cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能
UID:代表执行者的身份
PID:代表这个进程的代号
PPID:代表这个进程是由哪个进程发展衍生而来的,即父进程的代号
PRI:代表这个进程可被执行的优先级,值越小越快被执行
NI:代表这个进程的nice值
寄存器的临时数据叫进程的上下文,上下文数据不可以被丢弃
其他概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发
环境变量
系统命令可以直接运行,自己写的程序必须带路径
常见环境变量
PATH:指定命令的搜索路径
HOME:指定用户的主工作目录(即用户登陆到linux系统时默认的目录)
SHELL:当前Shell,它的值通常是/bin/bash
查看环境变量路径
echo $PATH
将个人的可执行程序路径放到PATH路径下,就可以只写命令不带路径就能运行
esport PATH=$PATH:个人可执行文件所在路径
export PATH=$PATH:/root/code/PCB_Study/study02
子进程的环境变量都是从父进程来。默认,所有环境变量都会被子进程继承
环境变量具有全局属性
进程地址空间
进程task_struct结构体里有个mm_struct* mm变量,这个变量是个指针,指向一个进程地址结构体
其实堆和栈区之间还有块共享区,后面讲进程通信方式的共享内存方式在细讲
代码区指的是正文代码,比如main函数的地址
进程地址空间是一种内核数据结构,里面有各个区域的start和end指针来划分哪里是栈区、哪里是堆区、全局区、代码区
下图是进程地址空间数据结构的部分成员变量
通过在一个范围定义start和end,就把一块大区域划分成了不同区域
在函数内使用static修饰变量,会将该变量属性变为全局属性,但是其他函数不会识别到该变量,意味着,即使该变量被static修饰,能访问它的依然只有原函数。
字面常量
在编译器中能被直接编译的
int main()
{
"hello";//字面常量
10;//字面常量
'a';
char*str="helloworld";//该字符串处于字符常量区,所有的字符常量都在代码区,该字符常量区是只读区
}
写实拷贝策略
写实拷贝策略是什么,这里先引出需要写实拷贝的场景
定义一个全局变量g_val,子进程修改这个全局变量的值,父进程不修改,两个进程打印这个变量的值、地址
#include <iostream>
#include <unistd.h>
using namespace std;
int g_val = 100;
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
int cnt = 0;
while (1)
{
cout << "__________________________________________________" << endl;
cout << "I am child, my pid is " << getpid() << " and g_val is " << g_val << endl;
cout << "子进程的g_val虚拟地址是" << &g_val << endl;
cout << "__________________________________________________" << endl;
sleep(1);
cnt++;
if (cnt == 5)
{
g_val = 200;
}
}
}
else if (id > 0)
{
// parent
while (1)
{
cout << "__________________________________________________" << endl;
cout << "I am parent, my pid is " << getpid() << " and g_val is " << g_val << endl;
cout << "父进程的g_val虚拟地址是" << &g_val << endl;
cout << "__________________________________________________" << endl;
sleep(1);
}
}
}
以下是程序打印结果图
这究竟是怎么回事,为什么两个变量的地址相同,却能在两个进程中有不同的值?
这就是写实拷贝发生的场景
可以看到5秒后,gal的值已经被子进程更改为200,但是父进程的gal依然是100,这是因为,创建了父子进城后,子进程复制了父进程的地址空间,连g_val的虚拟地址也被复制给了子进程,因此和父子进程的g_val虚拟地址一样,但是当子进程修改了g_val的值后,操作系统重新给g_val分配了一块空间,虽然虚拟地址依然指向和父进程相同的地址,但是映射到的物理地址是不同的,这种策略叫写实拷贝
下图是详细过程
在子进程修改g_val变量前,父子进程的g_val虚拟地址相同(假设为0x11),父进程的g_val经过父进程的页表映射,映射到物理地址上(假设为2222);子进程的g_val同理,该变量虚拟地址映射到的物理地址和父进程映射的相同
子进程修改g_val变量后,子进程的g_val虚拟地址虽然还是0x11,但是页表上会形成新的映射,映射到新的物理地址上,这就是全局变量在两个进程的虚拟地址相同,但是值却可以不同的原因
进程地址空间的独立性
扩展
源文件在编译成可执行程序存到磁盘的时候(只是编译好,没有运行、没有加载到内存中),请问这个程序内部有地址吗?
答案是有的,编译好的程序文件,内部已经有地址了,进程地址空间不仅是操作系统内部要遵守,编译器也要遵守,并且采用和linux内核中一样的编址方式,给每一个变量、每一行代码都进行了编制,所以,程序在编译的时候,每个字段已经有了一个虚拟地址