Linux:进程概念

冯诺依曼体系结构

冯诺依曼体系结构是现代计算机硬件体系结构,包括输入设备、存储器(内存)、输出设备(显示器)、运算器和控制器,注意:中央处理器(CPU)包含运算器和控制器等
(1)不考虑缓存情况,CPU能且只能对内存进行读写,不能访问外设,因此输入数据后放到存储器中进行缓冲
(2)硬件行为决定软件行为,所有设备都是围绕存储器工作的。
例如qq聊天过程:
输入设备输入数据,CPU读取数据首先从内存中去读,如果内存中没有,就去辅助存储器(例如硬盘,比如qq发文件)读,把硬盘的数据读入到内存,然后才会被CPU读取到。然后发送数据(发送设备是网卡,网卡什么时候从内存中取数据,是程序控制的),到达对端电脑,对方通过网卡接收数据,放到内存中,CPU进行处理,放回内存,控制显示器从内存中拿数据显示出来。
系统调用和库函数:操作系统会通过驱动系统来管理软硬件资源,它不会完全暴露给上层,对上层以接口对用户提供服务,这个接口称为系统调用接口,而系统调用接口成本比较大,因此库函数对系统调用接口进行了二次封装。

进程
  • 进程的概念
    就是一个正在执行的程序,操作系统将运行中的程序描述起来,通过描述来实现对程序的运行调度,这个描述信息就是操作系统调度一个程序运行的实体,因此在操作系统中进程就是运行中程序的描述pcb—进程控制块;
    为什么要有PCB?
    因为操作系统要管理进程,要管理进程就要先描述,再组织,要描述就要有PCB
    那么进程和程序的区别是什么?
    (1)程序是放到磁盘的可执行文件,进程是指程序运行的实例;
    (2)进程是动态的,程序是静态的;
    (3)进程是程序的执行,通常进程不可以在计算机之间迁移,而程序通常对应着文件,静态可以控制;
    (4)进程是暂时的,程序是长久的;
    (5)进程是一个状态变化的过程,程序可以长久保存;
    那么在Linux操作系统下的PCB就是task_struct(一个结构体),它的内容分类包含:
    标识符(PID):描述本进程的唯一标识符、状态、优先级(交互式程序一般优先级要求是最高的)、程序计数器(程序将被执行的下一个指令的地址)、内存指针
    上下文数据(程序上次正在处理的数据)、I/O状态信息、记账信息(一个进程在CPU上的运行时间)、其他信息,即程序上次正在处理的数据。
    CPU的分时机制:每个程序在CPU上运行都有一个时间片,时间片运行完毕则调度切换,时间片就是程序在CPU上运行的这段时间。
  • 查看进程
    /proc目录保存进程信息
    通过ps命令查看,它的选项包括:-aux(信息更详细)/-ef(查看所有进程信息)
    例如:
[Daisy@localhost ~]$ ps -ef

显示所有进程信息
例如要打印第一行和匹配到loop字符串,使用:

[Daisy@localhost ~]$ ps -ef | head -n 1 && ps -ef | grep loop
UID         PID   PPID  C STIME TTY          TIME CMD
Daisy      6422   5467  0 16:48 pts/4    00:00:00 grep --color=auto loop

可以看到显示出了第一行并且匹配到了loop字符串,&&表示相与的意思

  • 通过系统调用获取进程标识符
    使用接口getpid()来获取进程标识符
    例如我们先写一个Makefile文件,然后生成了一个10-12可执行文件,./进行运行成为一个进程,通过getpid()查看他的进程标识符,例如vim 10-12.c:
#include <stdio.h>
#include <unistd.h>
int main()
{

        printf("pid:%d\n",getpid());
        return 0;
}

make后./运行10-12文件,结果是

[Daisy@localhost LinuxCode]$ ./10-12
pid:8579

发现他的pid是8579,然后使用ps -aux | grep 10-12可以查看进程标识符也是8579
通过getppid()获取父进程id。
例如:

[Daisy@localhost LinuxCode]$ ./10-12
ppid:8068

使用ps axj | head -n1 && ps axj | grep 10-12

[Daisy@localhost LinuxCode]$ ps axj | head -n1 && ps axj | grep 10-12
  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
  8068   8692   8691   8068 pts/2      8691 R+    1000   0:00 grep --color=auto 10-12

发现父进程ID也是8068,而且进程不管是退出还是重新运行,父进程ID并不发生变化,因为它是bash

[Daisy@localhost LinuxCode]$ ps -aux | grep 8068
Daisy      8068  0.0  0.3 116692  3412 pts/2    Ss   18:40   0:00 -bash

使用kill -9 来杀掉这个进程,这时进程结束

  • 通过系统调用创建进程fork

例如:
vim 10-12.c,编写代码

#include <stdio.h>
#include <unistd.h>
int main()
{
        printf("before!\n");
        fork();
        printf("after!\n");
        printf("I am a process:pid:%d,ppid:%d\n",getpid(),getppid());
}

make后然后./进行运行,结果是:

[Daisy@localhost LinuxCode]$ ./10-12
before!
after!
I am a process:pid:9065,ppid:8809
[Daisy@localhost LinuxCode]$ after!
I am a process:pid:9066,ppid:1

证明fork可以创建子进程,在创建子进程之后,父子进程代码共享(子进程因为拷贝了父进程PCB里边的很多数据,因此与父进程内存指针以及程序计数器都相等,所以运行的代码以及运行的位置都一样),数据各自私有一份(进程运行时具有独立性,因为父子进程是两个进程,保持独立性),但是父子进程谁先运行不一定(取决于操作系统的调度系统来决定的),但是可以稍加控制,例如:


#include <stdio.h>
#include <unistd.h>
int main()
{
        printf("before!\n");
        fork();
        printf("after!\n");
        printf("I am a process:pid:%d,ppid:%d\n",getpid(),getppid());
        sleep(1);
}

运行结果是:

[Daisy@localhost LinuxCode]$ ./10-12
before!
after!
I am a process:pid:9135,ppid:8809
after!
I am a process:pid:9136,ppid:9135

可以看出父进程先运行
总结:fork有两个返回值,给父进程返回子进程的pid,给子进程返回0(返回值若为-1,说明创建子进程失败),例如:

#include <stdio.h>
#include <unistd.h>
int main()
{
        pid_t id=fork();
        if(id<0)
        {}
        else if(id==0)//child
        {
                printf("I am a child : pid:%d ppid:%d\n",getpid(),getppid());
        }
        else
                printf("I am a parent : pid:%d ppid:%d\n",getpid(),getppid());
        sleep(1);
}

结果是

[Daisy@localhost LinuxCode]$ ./10-12
I am a parent : pid:9252 ppid:8809
I am a child : pid:9253 ppid:9252
  • 进程状态
    1、R运行状态:并不是进程一定在运行中,而是进程要么是在运行中,要么在运行队列中
    2、S睡眠状态:表示进程在等待事件完成,又叫做可中断睡眠
    3、D磁盘休眠状态:又叫不可中断睡眠状态,进程通常等待IO的结束
    4、T停止状态
    5、X死亡状态
  • 僵尸进程
    即处于僵死状态的进程,表示进程已经退出,但是资源没有完全释放
    僵尸进程的产生:子进程先于父进程退出,为了保存退出原因,因此资源没有完全被释放,因此在子进程退出时,操作系统会通知父进程,让父进程获取子进程的退出原因,然后释放子进程的所有资源,但是如果当前父进程并没有关注子进程的退出状态,子进程称为僵死状态,这就是僵尸进程
    例如:
[Daisy@localhost LinuxCode]$ touch test.c
[Daisy@localhost LinuxCode]$ vim Makefile
[Daisy@localhost LinuxCode]$ vim test.c

编译test.c代码

#include <stdio.h>
#include <stdlib.h>
int main()
{
        pid_t id=fork();
        if(id<0)
        {
                perror("fork");
                return 1;
        }
        else if(id>0)
        {
                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;
}

编译在另一个终端下启动监控,
当前终端如图:
在这里插入图片描述
在这里插入图片描述
另个终端:
在这里插入图片描述
发现 < defunct >那一行状态信息是Z+,就是僵尸进程。
僵尸进程的危害:资源泄漏
如何避免僵尸进程产生:进程等待
注意:僵尸进程就算是使用kill -9都无法杀死。僵尸状态处理:退出父进程

  • 孤儿进程
    即父进程先于子进程退出,子进程没有从父进程获取自身的退出状态,子进程就成为了孤儿进程,所有的孤儿进程都被一号(init)进程所收养,父进程成为一号进程(init/systemd进程)(孤儿进程退出后不会成为僵尸进程)
    例如:
#include <stdio.h>
#include <unistd.h>
int main()
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
    }
    else if(id==0)
    {
        printf("child pid = %d,ppid=%d\n",getpid(),getppid());
        sleep(8);
        printf("child pid = %d,ppid=%d\n",getpid(),getppid());
    }
    else
    {
        sleep(3);
        printf("father pid =%d,ppid=%d\n",getpid(),getppid());
    }
    return 0;
}

此代码首先让父进程进入3秒的休眠期,然后子进程打印出第一句后,进入8秒的休眠期,此时子进程的父进程id是父进程的进程id,三秒之后父进程退出(孤儿进程前提),8秒之后,子进程退出时,它的父进程id已经变成了1,也就是孤儿进程会被一号金城所收养
代码运行结果就是

[Daisy@localhost ~]$ ./file
child pid = 11338,ppid=11337
father pid =11337,ppid=11151
[Daisy@localhost ~]$ child pid = 11338,ppid=1

注意:孤儿进程在系统后台运行

  • 进程优先级
    即CPU资源分配的先后顺序
    例如:
[Daisy@localhost LinuxCode]$ ps -l
F S   UID    PID   PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000   9961   9957  0  80   0 - 29173 do_wai pts/1    00:00:00 bash
0 R  1000  10370   9961  0  80   0 - 37235 -      pts/1    00:00:00 ps

UID代表执行者身份、PID代表进程代号、PPID父进程的代号、PRI代表这个进程可执行的优先级,值越小越早执行、NI代表这个进程的nice值
PRI与NI:PRI即进程的优先级,NI就是nice值,表示进程可被执行的优先级的修改数值,nice的取值范围是==-20至19==,共40个级别
命令top然后按“r” 输入进程PID号,然后 输入新的nice值(必须在root下才能修改)
注意:程序分为CPU密集型和IO密集型,其中IO密集型不需要修改优先级;并且一般不建议修改优先级

  • 其他概念
    竞争性:系统进程数目众多,而CPU资源只有少量甚至只有一个,因此进程之间具有竞争属性。
    动态性:进程的实质是一次程序执行的过程,有创建、撤销等状态的变化。而程序是一个静态的实体。
    结构性:进程拥有代码段、数据段、PCB(进程控制块,进程存在的唯一标志)。也正是因为有结构性,进程才可以做到独立地运行。
    独立性:多个进程运行,要独享各种资源,多进程运行期间互不干扰。
    并行:多个进程在多个CPU下分别,然后同时进行运行,这个必须有多个 CPU 才行
    并发:在一个时间段内多个进程同时推进,依赖多进程间的切换(进程切换会保留自身的硬件上下文信息,在下一次恢复再拿出来)(操作系统可以运行多个进程,但是不能在同一时刻多个进程运行)
环境变量

环境变量是系统提供的一种变量,设置系统运行环境参数信息的变量,让系统运行环境的配置更加灵活方便,并且可以通过一个环境变量向进程传递参数,在系统当中通常具有全局属性,它和进程没有直接关系。

  • 环境变量相关命令
    (1)echo:显示某个环境变量
    (2)export:设置新的环境变量
    (3)env:显示所有环境变量,例如:
    在这里插入图片描述
    可以看出env命令显示所有环境变量
    (4)unset:清除环境变量
  • 常见环境变量
    (1)PATH:在shell中帮用户查找命令,它可以不用带路径来进行命令的执行,当不想用路径来执行命令时,可以将自己的路径放在环境变量后,但是要记得备份(使用 export PATH=$ PATH: 路径名这个命令来进行操作),例如:
[Daisy@localhost LinuxCode]$ ls
Linux1  Makefile  test
[Daisy@localhost LinuxCode]$ pwd
/home/Daisy/LinuxCode
[Daisy@localhost LinuxCode]$ export PATH=$PATH:/home/Daisy/LinuxCode/test
[Daisy@localhost LinuxCode]$ echo $PATH
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/Daisy/.local/bin:/home/Daisy/bin:/home/Daisy/LinuxCode/test

执行export PATH=$PATH:/home/Daisy/LinuxCode/test命令后,通过echo $PATH查看环境变量发现在其后确实跟上了/home/Daisy/LinuxCode/test路径,这时就可以不用带路径来执行命令,也可以将PATH清空,使用export PATH=’'命令即可(清空后任何命令就无法执行,重新启动虚拟机(xshell)即可)。
(2)HOME
它的作用是执行当前用户登录所处的主工作目录,也就是默认的目录,例如当用户名是Daisy时,pwd发现我的主工作目录是/home/Daisy,echo $HOME发现也是/home/Daisy

[Daisy@localhost ~]$ clear
[Daisy@localhost ~]$ pwd
/home/Daisy
[Daisy@localhost ~]$ echo $HOME
/home/Daisy

然后切换到root用户下,执行上述操作,得

[root@localhost ~]# pwd
/root
[root@localhost ~]# echo $HOME
/root

发现他们的主工作目录是/root,表明HOME环境变量是执行用户的主工作目录。
Linux也可以指定本地环境变量,例如:

[Daisy@localhost ~]$ myenv=1000
[Daisy@localhost ~]$ echo $myenv
1000

如果要将它导成环境变量,使用export myenv即可,然后env命令来查看一下,发现存在了env这个环境变量,如图:
在这里插入图片描述
可以使用unset命令来清楚掉,然后使用env查看一下,例如:

[Daisy@localhost ~]$ unset myenv
[Daisy@localhost ~]$ env

在这里插入图片描述
这时就发现没有了env这个环境变量。
(3)SHELL:当前Shell,值通常是/bin/bash

  • 通过代码获取环境变量
    (1)命令行第三个参数
    例如touch myenv.c文件,然后编写Makefile规则,例如Makefile中为:
myenv:myenv.c
        gcc $^ -o $@
.PHONY:clean
clean:
        rm myenv

然后vim myenv.c,例如:

#include <stdio.h>
int main(int argc,char* argv[],char* env[])//argv表示命令行参数,它指向可执行程序的名称,env放环境变量
{
        int i=0;
        for(;env[i];i++)
        {
                printf("%d:%s\n",i,env[i]);
        }
        return 0;
}

然后make一下,然后./myenv,得到
在这里插入图片描述
这时打印出的进程myenv是一个环境变量,进程环境变量创建到了当前目录下,因为环境变量有一个属性是当前目录下。因此从上述可以看出环境变量的组织方式就是一个字符数组,然后存储环境变量。
(2)通过第三方指针environ获取
例如touch myenviron.c,然后vim myenviron.c:

#include <stdio.h>
int main(int argc,char* argv[])
{
        extern char* *environ;
        int i=0;
        for(;environ[i];i++)
        {
                printf("%s\n",environ[i]);
        }
        return 0;
}

由于环境变量的组织方式如图:

在这里插入图片描述
因此在声明environ时,它是一个二级指针。,最后vim Makefile,然后make后./运行myenviron,得到
在这里插入图片描述
可以看出获取到了环境变量。

  • 通过系统调用获取或者设置环境变量
    使用getenv函数,例如重新vim myenv.c:
#include <stdio.h>
#include <stdlib.h>
int main()
{
        printf("%s\n",getenv("PATH"));
        return 0;
}

将上一次的myenv文件通过make clean删除,然后重新make,./运行myenv,得到的是PATH环境变量的内容,得到:

[Daisy@localhost LinuxCode]$ ./myenv
/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/Daisy/.local/bin:/home/Daisy/bin

可以看出得到了PATH环境变量的内容

  • 环境变量通常具有全局属性
    例如重新vim myenv.c,例如:
#include <stdio.h>
#include <stdlib.h>
int main()
{
        printf("%s\n",getenv("MYENV"));
        return 0;
}

然后先将上一次的myenv make clean,重新make,./运行myenv,得到

[Daisy@localhost LinuxCode]$ ./myenv
段错误(吐核)

发现出现错误,因为此时并没有MYENV这个环境变量,那我们创建一个MYENV环境变量,先MYENV=1000,但是这时他只是一个本地变量,然后export导出环境变量,然后通过env查看一下是否有MYENV这个环境变量例如:

[Daisy@localhost LinuxCode]$ MYENV=1000
[Daisy@localhost LinuxCode]$ echo $MYENV
1000
[Daisy@localhost LinuxCode]$ export MYENV
[Daisy@localhost LinuxCode]$ env

结果发现如图
在这里插入图片描述
的确有MYENV这个环境变量,此时重新make,./运行myenv,得到

[Daisy@localhost LinuxCode]$ make
gcc myenv.c -o myenv
[Daisy@localhost LinuxCode]$ ./myenv
1000

说明环境变量是可以被子进程继承下去的,因为没有用export时,运行myenv程序没有得到MYENV的内容1000,在export导出环境变量后,可以得到MYENV的内容1000,因为这时MYENV已经成为了环境变量,系统环境变量列表中存在,因此他能够被子进程继承,因此它具有全局属性。

程序地址空间

例如:
编译这段代码:

#include <stdio.h>
#include <stdlib.h>
int g_val=100;
int main()
{
        pid_t id=fork();
        if(id==0)
        {
                sleep(1);
                printf("child: %d,%p\n",g_val,&g_val);
        }
        else
        {
                sleep(2);
                printf("father:%d,%p\n",g_val,&g_val);
        }
        return 0;
}

make之后运行得到:

child: 100,0x601044
father:100,0x601044

发现父子进程的g_val的值和地址都是一样的,因为子进程按照父进程为模板,父子进程没有对变量进行任何修改,那么如果将代码改为

#include <stdio.h>
#include <stdlib.h>
int g_val=100;
int main()
{
        pid_t id=fork();
        if(id==0)
        {
                sleep(1);
                g_val=200;
                printf("child: %d,%p\n",g_val,&g_val);
        }
        else
        {
                sleep(2);
                printf("father:%d,%p\n",g_val,&g_val);
        }
        return 0;
}

得到的结果是:

child: 200,0x601044
father:100,0x601044

发现父子进程的g_val的值发生改变,但是地址还是一样,所以可以得出父子进程输出的变量不是同一个变量,但是地址相同,所以这个地址绝对不是物理地址,在Linux下,这种地址叫做虚拟地址,操作系统必须负责将虚拟地址转化为物理地址;
因为直接访问物理内存,:进程之间内存访问,缺乏控制,内存的利用率比较低(每一个程序都要求一块连续的内存),操作系统为每一个运行中的程序都创建了一个虚拟地址空间,
因此可以来分析下图:
在这里插入图片描述
不是内存,叫做进程地址空间,刚刚打印出来的地址是地址空间上的地址,所以进程地址空间又叫虚拟地址空间
举一个例子:有一个富人,他具有10亿的财产,他有5个孩子(孩子之间并不知道别的孩子的存在),他对每个孩子都说是自己唯一的继承人,而每个孩子想要花费一小部分富人的财产,只要在承受范围内,富人都会给,站在每个孩子的角度认为自己都是唯一的继承人,拥有这10亿财产,他们不知道有其他孩子的存在,而在上帝视角这5个孩子是竞争的关系,虚拟地址空间就是这个道理,富人就相当于操作系统,10亿的财产相当于物理内存,而富人给每个孩子的承诺相当于虚拟地址空间,这5个孩子相当于5个进程。
有多少个进程就有多少个地址空间,地址空间是一个结构体,描述的是一个一个的区域
如图:
在这里插入图片描述
可以看出虚拟地址空间需要映射到物理内存,通过页表+MMU(内存管理单元)来进行映射到物理内存
总结:
为什么要有虚拟地址空间?
(1)保护物理内存
(2)保证进程是独占资源的
虚拟地址和物理地址的概念
CPU通过地址来访问内存中的单元,如果CPU没有MMU(内存管理单元)或者MMU没有启用,CPU在取指令或访问内存时发出的地址将直接传到CPU芯片的外部地址引脚上,直接被内存芯片接受,这叫做物理地址;
如图:
在这里插入图片描述
如果CPU启用了MMU,CPU核发出的地址将被MMU截获,从CPU到MMU的地址称为虚拟地址,MMU将这个地址翻译成另一个地址发到CPU芯片外部地址引脚上,也就是虚拟地址映射成物理地址,如图:
在这里插入图片描述
虚拟地址和物理地址地址的分离可以给进程带来便利性与安全性,虚拟地址和物理地址必须建立一一对应的关系,才可以进行正确的地址转换;记录对应关系最简单的办法就是将对应关系记录在一张表中,为了让翻译速度较快,这张表必须加载到内存中,不过,这种记录方式比较浪费,因此Linux采用了分页方式来记录对应关系,分页就是以更大尺寸的单位页来管理内存,在Linux中,通常每页大小是4kb,通过getconf PAGE_SIZE可以获取当前内存页大小。
32bit位分页机制下虚拟地址由32bit组成的,常规4kb分页,32bit的虚拟地址被分成3个域:目录(最高10位)、页表(中间10位)、偏移量(最低12位),如图:
在这里插入图片描述
总结如下:其实虚拟内存映射到物理内存有三种方式,即操作系统进行内存管理的三种方式:
分段式:
虚拟地址的组成方式:段号+段内地址
借助段表:物理段起始地址:获取到物理段起始地址后+段内地址,这种方式对程序员比较友好,将程序的地址根据使用性质不同进行分段管理;
分页式:虚拟地址的组成方式:页号+页内偏移 、
借助页表映射到物理内存块号,获取到物理内存块号之后*块大小+页内偏移,提高了内存的利用率;
段页式:将虚拟地址进行分段管理,段内采用分页式管理,虚拟地址的组成:段号+段内页号+页内偏移
借助段表进行找到段内页表的起始地址,借助段内页号找到页表项,根据页表项可以找到物理块号;
交换分区的作用:
虚拟地址是64位,也就是大小为2^64,而物理内存只有8G,也就是大小为2 ^35,当物理内存不够用时,就会将物理内存中的数据交换到磁盘的交换分区上存储(将对应页表的缺页中断位进行置位),空出的内存运行当前数据;
内存置换算法:最久未使用算法(LRU),最少未使用算法等;

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值