4.Linux系统进程

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;
}

结果:
在这打印父进程的id和子进程的id里插入图片描述另一个终端监视子进程的状态变化

子进程大状态变为僵尸状态

僵尸进程的危害

进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于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内核中一样的编址方式,给每一个变量、每一行代码都进行了编制,所以,程序在编译的时候,每个字段已经有了一个虚拟地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值