Linux进程概念

冯诺依曼体系结构

在这里

操作系统(Operator System)

在这里
设计OS目的:
对下:1、与硬件交互,管理好软硬件资源。
校长:操作系统
辅导员:驱动程序
硬件:学生
校长提出决策,辅导员执行决策。
对上:2、为用户程序(应用程序)提供一个良好的执行环境。
操作系统管理好软硬资源后,操作系统就能为用户提供良好的运行环境,例如提供了系统接口。
例如:用户需要执行printf(“hello”);语句,向显示器写入hello字符串,那么printf()函数的操作必须贯穿操作系统,通过调用系统接口,并且得到操作系统的允许,才能访问软硬件资源。
系统接口与用户层接口
系统接口:Linux操作系统由c语言写,所以叫c函数;
用户层操作接口:在系统接口之上,对系统接口封装的函数库(c库,java库),shell的外壳,部分指令。
注意:用户能通过用户操作接口进行指令操作,开发操作,管理操作。库函数里如果软件需要对硬件方面的操作,那么在语言层面上会调用系统接口。

进程

基本概念

课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。

什么是进程?

结论:进程= 程序 + 操作系统维护进程的相关的数据结构。
曾经我们启动的程序,先从硬盘上把程序的代码+数据加载到内存里,被刚刚加载到内存的叫程序文件内容,这个程序文件内容就好比学生,校长要管理好学生,先对学生描述,然后通过数据结构把每一位学生组织起来。所以操作系统给每个加载到内存的程序都创建了进程控制块(PCB),PCB是操作系统里的统称名词,在Linux操作系统下叫task_struct的结构体名。PCB对程序进行描述,还有相关的数据结构,操作系统只需要对task_struct操作就能达到对程序的管理。
在这里插入图片描述tast_struct包含了进程内部的所有属性信息!进程就好比链表节点,为了管理好,就要有链表的数据结构。
占在os的角度, 由双链表数据结构组织进程,CPU有一个运行队列,CPU要处理程序时,只需要让进程的链表头链接到 运行队列即可。
在这里插入图片描述结论:有了进程控制块,所有的进程管理任务与进程对应的程序毫无关系!!与进程对应的内核创建的该进程的PCB强相关。

描述进程-PCB

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
  • 课本上称之为PCB(process control block),Linux操作系统下的PCB是: task_struct
task_struct-PCB的一种
  • 在Linux中描述进程的结构体叫做task_struct。
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类

task_struct数据结构庞大而复杂,但它可以分成一些功能组成部分:

1.标示符: 描述本进程的唯一标示符,用来区别其他进程。
例如进程pid,获取进程pid,我们可以调用getpid(),该函数包含在unistd.h文件里,该文件不属于c标准库。

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>

int main()
{
  printf("%d\n",getpid());//获取当前进程pid
  printf("%d\n",getppid());//获取父进程pid
  return 0;
}

注意:我们执行的程序都是bash的子进程。

2. 状态: 任务状态,退出代码,退出信号等。
ps:进程状态在下面会将。
当程序执行最后一条代码时,return n;n为退出码,该退出码会写到进程信息里。在这里插入图片描述echo $? 为获取环境变量,该变量保存输出最近执行的程序的退出码。

也就task_struct 当前具有:

task_struct{
	pid_t pid;// 进程pid
	pid_t ppid;// 父进程pid
	int code,exit_code; 退出码 
	int status;// 进程转态
}

3. 优先级: 相对于其他进程的优先级。
进程都能被CPU调用,但是优先级能决定进程谁先被执行。

4. 程序计数器: 程序中即将被执行的下一条指令的地址。
有一个pc指针,指向下一条指令,程序在运行的时候会不断的修改pc指针。
5.内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
可通过该指针找到对应实体;
6. I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
文件描述符。关于i/o
7.记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
记账信息给操作系统的调度模块(算法)使用,能较为均衡的调度每个进程,获取CPU资源。
8.上下文数据进程执行时处理器的寄存器中的数据。
CPU里有一套寄存器,用来保存每个进程的临时数据,CPU规定在一个时间片后会切换下一个进程,为了保护上下文数据,CPU的寄存器的数据会保存到PCB里,当切换回来时,再把上下文数据恢复。通过CPU的来回切换,用户能感受到多个进程同时在运行。

在这里插入图片描述
9.其他task_struct结构体信息

查看进程

进程的信息可以通过 /proc 系统文件夹查看 ,进程创建时,操作系统会以当前pid为目录名,把进程的相关信息保存到该目录下,进程销毁该目录也销毁。
在这里插入图片描述
cwd:当前进程工作目录路径。
exe:启动执行程序的文件路径。
注意:这些文件不属于磁盘文件,而是内存文件。
大多数进程信息同样可以使用top和ps这些用户级工具来获取。

fork初识

fork是用来创建子进程。创建子进程那么系统上就多了一个进程,
1、默认情况下,子进程会“继承”父进程代码和数据,原因很简单,父进程的代码和数据是向磁盘上获取的,而子进程没有。
2、子进程内核数据结构task_struct也会以父进程为模版,初始化子进程task_struct.

如何理解父子继承?

继承代码:代码只有一份,并且代码是不可修改的。
继承数据:进程之间是具有独立性的,默认情况下数据是共享的,但是如果要进程对数据的修改,那么需要通过“写时拷贝”来完成进程数据的独立性。

通过fork()创建进程

头文件: <unistd.h>
参数:无参
返回值:失败返回<0;成功时,给子进程返回0,给父进程返回子进程pid

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	int ret = fork();
	if(ret < 0)
	{
		perror("fork");
		return 1;
	}
	else if(ret == 0)
	{ 	//child
		printf("I am child : %d!, ret: %d\n", 	getpid(), ret);
	}	
	else
	{ 	//father
		printf("I am father : %d!, ret: %d\n", getpid(), ret);
	}
	sleep(1);
	return 0;
}

如何理解fork()函数有两个返回值?

pid_t fork()
{
	// 创建进程代码
	// ……
	// ……
	// ……
	return xxx;

父进程创建了新进程,新进程继承了父进程代码,那么父子进程都会往下执行,自然就有了两个返回值,至于为什么给子进程返回0,给父进程返回子进程pid,一开始我还以为是return getpid(),结果不是我想的那样;父进程创建子进程目的就是为了让子进程干活,父进程与子进程是1:n的关系,父进程要获取子进程pid达到能控制子进程,而子进程只需要通过getppid即可获得。

如何理解两个返回值的设置?

返回值由同一块空间接收,数据是共享的,但是进程具有独立性,那么谁先写入,那么谁就要“写时拷贝”。

进程状态

进程状态的意义:方便操作系统快速判断进程,完成特定的功能,比如调度,本质是一种分类。

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

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

一个进程处于运行状态时不一定正在占用CPU资源,一个进程在满足被CPU调度的条件下会被放在run_queue里,在队列里准备给CPU调度,这就叫进程处于运行状态。

状态演示:

int main()
{
	while(true)
	{};//死循环
	return 0;
}

在这里插入图片描述

浅度睡眠状态-S(可中断睡眠)

当我们完成某种任务时,任务条件不具备,需要进程进行某种等待,可能等待网卡,磁盘,显示器,等外设资源时还有sleep命令,这时候进程处于S状态。
ps:千万不要以为进程只会等待CPU资源。
我们把运行状态的进程从run_queue放到等待队列中,就叫做挂起(阻塞)。
从等待队列,放到运行队列,然后被CPU调度就叫做唤醒进程。

等待队列与运行队列有什么区别?

运行队列:等待着CPU的资源。
等待队列:等待着外设资源,或者是被某种方式被限制到等待队列里(例如:sleep() )。当进程等到某种资源时,就会被放到运行队列里,然后等待CPU调度,访问某种资源。
所以:进程在某种队列里就处于某种状态。
在这里插入图片描述状态演示:

int main()
{
	while(true)
	{
		printf("hello world\n");
	};//死循环
	return 0;
}

在这里插入图片描述大部分情况处于S状态,是因为大部分时间在等待IO设备准备就绪,当IO准备就绪后,进程放到运行队列被CPU调度然后往外设写入。

深度睡眠状态-D(不可中断睡眠)

一个进程处于深度睡眠状态(disk sleep),表示该进程不会被杀掉,即便是操作系统也不行,只有该进程自动唤醒才可以恢复。该状态有时候也叫不可中断睡眠状态(uninterruptible sleep),处于这个状态的进程通常会等待IO的结束。
有一种场景: 进程要求从内存中写入数据到磁盘,此时进程处于休眠状态,操作系统发现进程什么事都不干浪费资源,操作把进程杀掉,那么磁盘完成工作以后会向进程汇报,但是进程已死,万一磁盘写入失败,那么失败的结果没人知道及处理,所以就有了D状态,一个D状态一个爷,如果系统中存在大量的爷进程那么系统会宕机。

暂停状态-T

在Linux当中,我们可以通过发送SIGSTOP信号使进程进入暂停状态(stopped),发送SIGCONT信号可以让处于暂停状态的进程继续运行。

暂停状态与等待状态的区别?

等待状态期间还会有部分数据可以被修改。
暂停状态不会有数据被修改。

状态演示:

一个程序正在运行, 我们给该进程发送 19信号。
kill -19 进程pid
程序继续运行
kill -18 进程pid

注意:完成暂停及继续操作后进程会被切换到后台,我们需要kill -9 pid 才能杀死进程。
关于信号部分,进程信号章节会讲解。

僵尸状态&死亡状态

死亡状态:只是一个回收状态,当一个进程的退出信息被读取后,该进程所申请的资源进程相关的内核数据结构+你的代码和数据就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态(dead)。
僵尸状态
当进程被某种因素终止后,会先进入僵尸状态,该进程不会立马被释放,而是供操作系统或是其父进程进行辨别退出原因。

所以说进程的死亡顺序是先进入僵尸状态然后死亡状态。

状态演示:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
	int ret = fork();
	if(ret < 0)
	{
		perror("fork");
		return 1;
	}
	else if(ret == 0)
	{ 	//child
		while(true)
		{
		 	printf("I am child : %d!, ret: %d\n", 	getpid(), ret);
		    sleep(1);
		}
	}	
	else
	{ 	//father
		while(true)
		{
			printf("I am father : %d!, ret: %d\n", getpid(), ret);
			sleep(1);
		}
	}
	return 0;
}

父子进程不断的打印,父子当前处于S或者R状态,我们给子进程发送9号信号杀死子进程,

在这里插入图片描述
子进程进入僵尸状态,父进程没有对子进程由任何的处理操作例如等待操作,这个后面会学,所以子进程一直处于僵尸状态。

僵尸进程的危害:

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?是的!
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说, Z状态一直不退出, PCB一直都要维护?是的!
  • 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构
  • 对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
  • 内存泄漏?是的!
孤儿进程

父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”,孤儿进程被1号init进程领养,当然要有init进程回收喽。

进程优先级

基本概念

cpu资源分配的先后顺序,就是指进程的优先权(priority)。
优先权高的进程有优先执行权利。配置进程优先权对多任务环境的linux很有用,可以改善系统性能。
还可以把进程运行到指定的CPU上,这样一来,把不重要的进程安排到某个CPU,可以大大改善系统整体性能。

为什么要有优先级?

资源少,让优先级高的享受更多的资源。

查看系统进程

在这里插入图片描述
我们很容易注意到其中的几个重要信息,有下:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的nice值

PRI & NI

PRI也还是比较好理解的,即进程的优先级,或者通俗点说就是程序被CPU执行的先后顺序,此值越小进程的优先级别越高。
那NI呢?就是我们所要说的nice值了,其表示进程可被执行的优先级的修正数值PRI值越小越快被执行,那么加入nice值后,将会使得PRI变为:PRI(new)=80+nice
这样,当nice值为负值的时候,那么该程序将会优先级值将变小,即其优先级会变高,则其越快被执行
所以,调整进程优先级,在Linux下,就是调整进程nice值,nice其取值范围是-20至19,一共40个级别。

nice值为什么是一个相对较小的一个范围?

优先级再怎么设置,也只能是一种相对的优先级,不能出现绝对的优先级,否则会出现很严重的进程“饥饿问题”。也就是部分进程优先级低,没有或者极少数能获取CPU资源。
调度器:较为均衡的让每个进程享受到CPU资源。例如:A、B、C进程,在一个时间内,分别获取CPU资源的次数,50,40,30。

用top命令更改已存在进程的nice:
1、top
2、进入top后按“r”–>输入进程PID–>输入nice值

其他概念

竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级。

独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰

并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行

并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。

环境变量

基本概念

环境变量本质就是 操作系统在内存/磁盘文件中开辟的空间,用来保存系统相关的数据。

环境变量的用途

为什么 ls 命令不用带路径?

因为有环境变量PATH,系统会通过该变量去查找 ls指令程序。
打印PATH环境变量:

echo $PATH

打印结果:

/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/BBQ

用冒号隔开的一条一条搜索路径。

常见环境变量

PATH : 指定命令的搜索路径

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

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

和环境变量相关的命令

1.echo: 显示某个环境变量值
2. export: 设置一个新的环境变量
3. env: 显示所有环境变量
4. unset: 清除环境变量
5. set: 显示本地定义的shell变量和环境变量

本地变量 & 环境变量

系统上还存在一种变量,是与本次登录(session)有关的变量,只在本次登录有效。
本地变量的定义:

myval = 12345
set | grep myval

env | grep myval
export myval
env | grep myval

export myval :导入变量myval到环境变量列表里。

环境变量的组织方式

在这里插入图片描述每个程序都会收到一张环境表,环境表是一个字符指针数组,每个指针指向一个以’\0’结尾的环境字符串。每个进程都有它所运行的的一个环境变量,环境变量一般是存放在内存的用户空间的一个环境变量表中,这个环境变量表是在进程生成时,从父进程的环境变量表中拷贝一份。

环境变量的获取方式

方式一、通过系统调用获取或设置环境变量(推荐写法)

 char *getenv(const char *name)
 // name:环境名
 // 返回值:环境名对应值
#include <stdio.h>
#include <stdlib.h>
int main()
{
	printf("%s\n", getenv("PATH"));
return 0;
}

相当于:echo $PATH

方式二、main函数的第三个参数
main函数的第三个参数接收的实际上就是环境变量表,我们可以通过main函数的第三个参数来获取系统的环境变量。

int main(int argc,char* argv[],char * envp[])

argc: 传入的选项个数
argv:传入的选项列表,末尾null
envp:传入环境变量列表,末尾null
例如:ls -a -l
argc=2,argv[]= “-a”, “-l”

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

方式三、通过第三方变量environ获取

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

libc中定义的全局变量environ指向环境变量表,environ没有包含在任何头文件中,所以在 使用时 要用extern声明。

环境变量通常是具有全局属性的

环境变量通常是具有全局属性,是因为环境变量可以被子进程继承下去。
我们在命令行上启动的进程,父进程都是bash,bash的父进程是操作系统。在bash被启动时,就把环境配置好,包含环境变量等等,例如通过配置文件/etc/bashrc。

验证父子进程会继承环境变量:

// code.c
#include<stdio.h>
#include<stdlib.h>
int main()
{
  printf("%s\n",getenv("my_val")) ;
  return 0;
}

在这里插入图片描述

进程地址空间

看如下代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int g_val = 100;
int main()
{
    //数据是各自私有一份(写时拷贝)
    if(fork() == 0){
        //child
        int cnt = 5;
        while(cnt){
            printf("I am child, times: %d, g_val = %d, &g_val = %p\n", cnt, g_val, &g_val);
            cnt--;
            sleep(1);
            if(cnt == 3){
                printf("##################child更改数据#########################\n");
                g_val = 200;
                printf("##################child更改数据done#########################\n");
            }
        }
    }
    else{
        //parent
        while(1){
            printf("I am father, g_val = %d, &g_val = %p\n", g_val, &g_val);
            sleep(1);
        }
    }
    return 0;
}

运行结果:
在这里插入图片描述
代码分析:全局变量g_val被写时拷贝,在物理内存中应该有两份g_val,可是child更改数据后,父子进程的g_val的值不一样,但地址却是一样的。
说明我们获取的地址并不是物理内存上的地址。
实际上,我们在语言层面上打印出来的地址都不是物理地址,而是虚拟地址。物理地址用户一概是看不到的,是由操作系统统一进行管理的。

进程虚拟地址空间初识

进程地址空间分布图:
在这里插入图片描述

每个进程都有一张进程地址空间,它是操作系统给进程画的一张大饼,让进程以为自己独占物理内存。
进程地址空间在内核中是一个数据结构类型 ,具体进程的地址空间变量 struct mm_struct{}。

mm_struct结构体区域的划分

大饼是可以通过数据的方式进行画大饼!生活中处处是大饼,银行,亿万富翁私生子,父母的代保管的压岁钱等等。

划分区域?

划分区域就是区域的开始到区域的结束。从高地址到低地址就像一把尺子,我们可以通过尺子的刻度来划分区域。
在这里插入图片描述虽然这里只有start 和 end 但是每个进程都认为 mm_struct 代表了整个内存 且所有的地址为0x0000……00 ~ 0xFFF……FF,且每个进程地址空间的划分是按照4GB空间划分的,也就是每个进程都认为自己拥有4GB。

什么是虚拟地址?

地址空间上进行区域划分时,对应的线性位置虚拟地址。
在这里插入图片描述

mm_struct结构体与物理内存进行关联

页表和MMU硬件的作用是将虚拟地址转换为物理地址。
在这里插入图片描述页表详细讲解

进程虚拟地址空间与页表的作用

注意:对软硬件的访问都必须贯穿操作系统。

1、通过添加一层软件层,完成有效的对进程操作内存进行风险管理,本质目的是为了,保护物理内存以及各个进程的数据安全。

添加软件层就是os的助理,帮助OS。
例如:如果没有进程虚拟空间与页表,那么程序之间就会对内存不发觉的乱用,还有就是页表其实是一种权限,例如我们的语言层定义const变量,该变量只能读,一但我们修改,就会被页表检测出来。

2、将内存申请和内存使用的概念,在时间上划分清楚,通过虚拟地址空间,来屏蔽底层申请内存的过程,达到进程读写内存和os进行内存管理操作,进行软件上面的分离。

例如:我们申请的1000字节有可能不会立马在物理内存中申请,而是在读写的时候才申请。在os的角度,如果空间立马给你,就意味着,整个系统会有一部分空间,本来可以被别使用的,现在却被闲置这。
注意:这种操作是基于缺页中断进行物理内存申请。
我们也不用担心os系统申请不到空间,os会通过内存管理算法来给进程想办法开辟空间。例如:把一些进程闲置的内存数据放到磁盘上,然后把空间让出来。

3、站在CPU和应用层的角度,进程统一可以看做统一使用4GB空间,而且每个空间区域的相对位置,是比较确定的。(统一的视角看待进程)

例如:CPU是如何知道我们进程的起始代码在哪里?只要找到入口就可以按顺序执行了。每个进程的代码都在内存里,CPU每次都要维护这起始入口去找,效率低。
那么有了进程地址空间后,CPU只需要有一个特定的地址,每次的都使用该地址去查找,从磁盘加载到内存,页表只需要获取到加载到内存的物理地址,那么CPU就能轻松找到。其他区域也类似。
在这里插入图片描述
总结:OS最终这样设计的目的,达到一个目标:每个进程都认为自己是独占系统资源的!
程序地址空间补充

最后解析:
在这里插入图片描述

为什么父子g_val是两个相同的地址,原因就是进程虚拟地址空间,在语言上我们用的都是虚拟地址,当创建子进程时,子进程以父进程为模版创建,所以此时,子进程的页表地址空间与父进程基本一样,当子进程对g_val 进行修改时,os检测到要进行写时拷贝,开辟新空间复制g_val值,改变子进程页表对g_val的物理映射。父子进程对g_val的虚拟地址从未改动过,所以打印的g_val的地址是一样的。
在这里插入图片描述
补充:
所有的只读数据,一般在物理内存只保留一份。原因:操作系统维护一份是成本最低的。

int main()
{
	const char* p="hello";
	const char* str="hello";
	printf("%p\n",p);
	printf("%p\n",str);
	return 0;
}

运行结果:打印同样的地址。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2023框框

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值