【Linux】进程概念

🌇个人主页平凡的小苏

📚学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风翻盘。

🛸C++专栏:Linux内功修炼
家人们更新不易,你们的👍点赞👍和⭐关注⭐真的对我真重要,各位路 过的友友麻烦多多点赞关注。欢迎你们的私信提问,感谢你们的转发! 关注我,关注我,关注我,你们将会看到更多的优质内容!!

一、冯诺依曼体系结构

我们常见的计算机,如笔记本。我们不常见的计算机,如服务器,大部分都遵守冯诺依曼体系。

我们所认识的计算机,都是有一个个硬件组成

  1. 输入设备:键盘、鼠标摄像头...
  2. 输出设备:显示器、播放器硬件
  3. 运算器:对我们的数据进行计算任务
  4. 控制器:对我们计算硬件进行一定的控制

关于冯诺依曼,注意几点:

  • 这里的存储器指的是内存

  • 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备)

  • 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。

  • 一句话,所有设备都只能直接和内存打交道

二、操作系统

1.为什么要有操作系统

  • 与硬件交互,管理所有的软硬件资源
  • 为用户程序(应用程序)提供一个良好的执行环境

2.操作如何提供给用户服务

  • 操作系统里面,里面会有各种数据,可是,操作系统不信任任何用户!

    操作系统为了保证自己的数据安全,也为了保证给用户提供服务,操作系统以接口的方式给用户提供调用的入口,来获取操作系统内部的数据!

三、进程

3.1、基本概念

  • 课本概念:程序的一个执行实例,正在执行的程序等

  • 内核观点:担当分配系统资源(CPU时间,内存)的实体。

3.2、内核分类

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。

  • 状态: 任务状态,退出代码,退出信号等。

  • 优先级: 相对于其他进程的优先级。

  • 程序计数器: 程序中即将被执行的下一条指令的地址。

  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针

  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。

  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

  • 其他信息

3.3、查看进程

进程的信息可以通过 /proc 系统文件夹查看

大多数进程信息同样可以使用top和ps这些用户级工具来获取

ps axj | head -1 && ps ajx | grep test

3.4、系统调用获取进程标识符

  • 进程id(PID)

  • 父进程id(PPID)

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
    printf("我是子进程,pid: %d\n", getpid());
    printf("我是父进程,ppid: %d\n", getppid());
    return 0;
}

3.5、通过系统调用初识fork

3.5.1通过man手册了解fork

fork的返回值是一个int,fork是给父进程返回子进程的pid,而子进程则是返回0,要是fork失败的话那就是返回-1.

3.5.2、为什么父进程返回子pid,子进程返回0?

这是为了区分不同的执行流,执行不同的代码块

3.5.3、函数是如何返回两次的,如何理解?

在我们创建子进程的时候,父子之间的代码是共享的,数据会独自开辟空间,当我们想要使用数据时,会发生写实拷贝

代码例子

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("begin: 我是一个进程, pid: %d, ppid: %d\n", getpid(), getppid());

    sleep(5);
    pid_t id = fork();

    //printf("我是后续的代码\n");
    //sleep(1);
    if(id == 0)
    {
        // 子进程
        while(1)
        {
            printf("我是子进程, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else if(id > 0)
    {
        //父进程
        while(1)
        {
            printf("我是父进程,pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        //error
    }
    return 0;
}

四、进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在Linux内核里,进程有时候也叫做任务)

R运行状态

并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{   
    while(1)
    {
        ;
    }
}

图中可以看书,我们只写循环语句,循环里面不做io操作是可以看到R状态的,因为cpu的速度是很快的,如果我们进行io操作,那它基本是处于睡眠状态(阻塞状态);这里主要演示R状态

S睡眠状态

意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{   
    while(1)
    {
        printf("i am a process: pid: %d\n", getpid());
        sleep(1);
    }
}

D磁盘休眠状态

有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。

T停止状态

可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

X死亡状态

这个状态只是一个返回状态,你不会在任务列表里看到这个状态

Z僵尸状态

  • 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程

  • 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。

  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 500;
        while(cnt)
        {
            printf("i am child, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            cnt--;
            sleep(1);
        }
        exit(0);
    }
    else
    {
        int cnt = 5;
        //father
        while(cnt--)
        {
            printf("i am father, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }

        //父进程目前并没有针对子进程干任何事事情
    }
}

僵尸进程的危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!

  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?是的!

  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!

孤儿进程

  • 父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?

  • 父进程先退出,子进程就称之为"孤儿进程"

  • 孤儿进程被1号init进程领养,当然要有init进程回收喽。

五、进程优先级

概念

  • cpu资源分配的先后顺序,就是指进程的优先权(priority)。

  • 优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可能改善系统性能。

  • 还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能

查看进程优先级命令

ps -al

我们很容易注意到其中的几个重要信息,有下:

UID : 代表执行者的身份

PID : 代表这个进程的代号

PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号

PRI :代表这个进程可被执行的优先级,其值越小越早被执行

NI :代表这个进程的nice值

PRI VS NI

  • PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高

  • 那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值

  • PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=PRI(old)+nice

  • 这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行

  • 所以,调整进程优先级,在Linux下,就是调整进程nice值

  • nice其取值范围是**-20至19**,一共40个级别

更改进程优先级命令

  • top命令

  • 进入top后按"r"-->输入进程PID-->输入nice值

六、环境变量

基本概念

  • 环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数

  • 如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪里,但是照样可以链接成功,生成可执行程序,原因就是有相关环境变量帮助编译器进行查找。

  • 环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性

常见的环境变量

  • PATH : 指定命令的搜索路径

  • HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)

  • SHELL : 当前Shell,它的值通常是/bin/bash。

测试PATH

c 复制代码
#include <stdio.h>
int main()
{
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    printf("hello world\n");
    return 0;
}

注意:为什么我们执行系统命令不用带路径,而执行可执行程序需要带./呢?这是因为环境变量的因素

系统命令大多都在红色方框的路径中,所以在执行指令时候,shell会在PATH中寻找,找到就会执行指令了。如果我们把上面的路径放入环境变量,我们在执行可执行程序的时候也可以不带./了

添加环境变量命令

PATH=$PATH: + 路径

注意:不要PATH+路径,因为这样会导致覆盖掉系统的环境变量,导致系统的命令识别不了,如果不小心这样做了,解决办法,重启xshell服务器就自动恢复成默认的样子了。

环境变量的相关命令

  • echo: 显示某个环境变量值

  • export: 设置一个新的环境变量

  • env: 显示所有环境变量

  • unset: 清除环境变量

  • set: 显示本地定义的shell变量和环境变量

通过系统调用获取环境变量

c 复制代码
#include <stdio.h>
#include <unistd.h> 
#include <stdlib.h>

int main()
{
    printf("%s\n",getenv("PATH"));    
    return 0;
}

命令行的三个参数

先来看前面两个参数的作用

c 复制代码
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[], char *env[])
{
    if(argc != 2) 
    {
        printf("Usage: %s -[a|b|c|d]\n", argv[0]);
        return 0;
    }
    if(strcmp(argv[1], "--help")==0)
    {
        printf("Usage: %s -[a|b|c|d]\n", argv[0]);
    }
    else if(strcmp(argv[1], "-a") == 0)
    {
        printf("功能1\n");
    }
    else if(strcmp(argv[1], "-b") == 0)
    {
        printf("功能2\n");
    }
    else if(strcmp(argv[1], "-c") == 0)
    {
        printf("功能3\n");
    }
    else if(strcmp(argv[1], "-d") == 0)
    {
        printf("功能4\n");
    }
    else
    {
        printf("default功能\n");
    } 
    return 0;
}

为什么我们执行命令可以带不同的选项呢?由图可知,它是由main函数中的前面两个参数来做到的,为指令、工具、软件提供支持的!!!我们在语言层面中很少用到,是因为不是在命令行中使用

第三个参数:获取系统中全部的环境变量

c 复制代码
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[], char *env[])
{

    int i = 0;
    for(; env[i]; i++)
    {
        printf("%s\n", env[i]);
    }
    
    return 0;
}

七、进程地址空间

1、虚拟地址

先看一段父子进程共存的程序,由子进程对全局变量grobal_val进行修改:

c 复制代码
#include <stdio.h>
#include <unistd.h>
int grobal_val=10;
int main()
{
    pid_t id=fork();
    if(id==0)
    {
        int cnt=0;
        while(1)
        {
            printf("子进程:pid=%d,ppid=%d | grobal_val=%d,&grobal_val=%p\n",getpid(),getppid(),grobal_val,&grobal_val);
            sleep(1);
            ++cnt;
            if(cnt==10)
            {
                grobal_val=200;
                printf("子进程已更改全局变量grobal_val\n");
            }
        }
    }
    else if(id>0)
    {
        while(1)
        {
            printf("父进程:pid=%d,ppid=%d | grobal_val=%d,&grobal_val=%p\n",getpid(),getppid(),grobal_val,&grobal_val);
            sleep(1);
        }
    }
    else 
    {
        printf("fork error\n");
        return 1;
    }
    return 0;
}

父子进程谁先执行不确定,由系统进行调度。

当子进程将全局变量grobal_val由10改为200,我们可以看到,父子进程的grobal_val的地址相同,但是父子进程从这个地址中获取的值却并不相同!

从同一块物理地址中取出的值是相同的,所以这个程序取出的地址(指针)并不是物理地址,而是虚拟地址(线性地址、逻辑地址)。注:逻辑地址指可执行程序编译完成后内部函数、变量的地址。逻辑地址有两种表示方法,一种是各个区域地址递增,另一种是每个区域的地址都从零偏移量开始(这种是比较老的表示方式)。

Linux中的逻辑地址是第一种表示方式,所以Linux中逻辑地址就是虚拟地址。

之前学习的C/C++内存区域,是一块虚拟内存空间,每个进程有它自己的虚拟内存空间,即进程地址空间。所以上面的代码用fork创建子进程,因为子进程是父进程的拷贝,父子进程的grobal_val虽然虚拟地址一样,但会被映射到不同的物理地址上。

grobal_val未被改变时,父子进程映射同一块grobal_val的物理地址,一旦父子进程的一方对共享数据进行修改,由于进程的独立性 ,操作系统会在物理内存中再开辟一块空间 ,并拷贝原数据 ,提出修改的进程的页表映射关系将会被改变 ,然后再让进程对数据进行修改 ,所以我们看到父子进程的数据并不一样。这种技术称为写时拷贝,对不同进程的数据进行分离。

2、进程地址空间的理解

1、进程它自己会认为它独占CPU资源,但其实并不是。因为进程以时间片轮转的形式占用CPU资源,时间一到,马上从运行状态进入休眠状态,实质上是通过虚拟地址空间,让进程认为它独占CPU资源。

2、进程地址空间是操作系统给进程开辟的一块虚拟内存空间,这块空间用内核的一种数据结构来描述、组织。

操作系统给每个进程一块4GB的虚拟内存,进程每次想使用,按需申请即可,但不会全部给进程。(注意这里给的是虚拟内存,就像老板给员工画饼一样)

对Linux操作系统中进程的理解中提到过,进程使用进程控制块task_struct结构体进行管理,同样的,每个进程地址空间也需要被管理,管理进程地址空间的结构体叫mm_struct,task_struct中有一个指针指向自己的mm_struct。

mm_struct伪代码:

c 复制代码
struct mm_struct
{
	uint32_t code_start,code_end;
	uint32_t data_start,data_end;
	uint32_t heap_start,heap_end;
	uint32_t stack_start,stack_end;
	······//存储进程地址空间各区域的起始位置
};

3、为什么要通过虚拟地址映射的方式访问物理地址

1、直接访问物理内存是非常不安全的,例如越界操作、恶意进程读取等。

2、页表会拦截不合理的请求,可以保护物理内存,防止恶意进程的访问 。所以写代码出现野指针、内存越界等情况并不会造成操作系统的崩溃。

3、进程地址空间的存在,可以让进程和进程间的代码进行解耦(互不干扰),保证了进程独立性的特征。

4、进程和编译器均遵守进程地址空间这一套规则,编完即可使用。

编译器也遵守进程地址空间这一套规则:

我们的代码在磁盘时,程序的函数、变量等通过虚拟地址建立联系,满足程序间的互相跳转;

当程序由磁盘被加载到内存中时,就具备了物理地址。函数、变量等通过页表映射至虚拟地址。

根据可执行程序的虚拟地址初始化mm_struct结构体中每个虚拟内存中的边界。

当程序在CPU中跑起来时,CPU根据虚拟地址运行完程序后,通过页表映射至物理地址。

相关推荐
pk_xz12345627 分钟前
Shell 脚本中变量和字符串的入门介绍
linux·运维·服务器
小珑也要变强30 分钟前
Linux之sed命令详解
linux·运维·服务器
海绵波波10736 分钟前
Webserver(4.3)TCP通信实现
服务器·网络·tcp/ip
九河云2 小时前
AWS账号注册费用详解:新用户是否需要付费?
服务器·云计算·aws
Lary_Rock3 小时前
RK3576 LINUX RKNN SDK 测试
linux·运维·服务器
幺零九零零3 小时前
【计算机网络】TCP协议面试常考(一)
服务器·tcp/ip·计算机网络
云飞云共享云桌面4 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
Peter_chq5 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
一坨阿亮6 小时前
Linux 使用中的问题
linux·运维
dsywws7 小时前
Linux学习笔记之vim入门
linux·笔记·学习