Linux-进程概念

一、冯诺依曼体系结构

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

存储器指的就是内存。

输入设备有键盘、摄像头、话筒、磁盘、网卡、鼠标等。

输出设备有显示器、播放器、磁盘、网卡等。

运算器 主要是对输入的数据进行计算任务,计算任务分为算数运算和逻辑运算两类。

控制器是对计算硬件流程进行一定的控制。

注:有些设备是纯输入设备,有些设备是纯输出设备,也有既是输入也是输出的设备,例如磁盘和网卡。

这5个设备都是独立的个体,各个硬件单元必须用"线"链接起来,"线"有两类1.系统总线2.IO总线。

冯诺依曼体系中中央处理器(CPU)只能对存储器(内存)进行读写,不能访问外设(输入设备和输出设备)。

原因:因为CPU运行时间是纳秒级别的,内存运行时间是微秒级别的,硬盘运行时间是毫秒级别的(1s=1000毫秒=1000000微秒=1000000000微秒),根据木桶原理CPU和硬盘的运行效率差距太大如果CPU直接访问外设会导致CPU等待时间过长,总体时间效率低下。而将硬盘中的数据提前存储到内存中,因为内存还会细分为一级二级三级缓存,因此CPU和内存之间时间级别差距不是特别大,CPU从内存中读取和处理数据的时间效率会显著提升。因此CPU不会直接从外设读取数据而是从内存中读取数据。

冯诺依曼体系结构中数据处理的流程

一个程序要运行,必须要先加载到内存中运行。

代码也是数据CPU要进行处理时代码也必须先加载到内存里。

总结:所有设备都只能直接和内存打交道。

二、操作系统(Operator System)

1.操作系统的结构

操作系统为什么要做管理部分有对系统调用接口用户操作接口的说明。

系统怎么进行管理部分有对操作系统驱动程序底层硬件的说明。

2.操作系统是什么?

操作系统本质上是一款对软硬件进行管理的软件。

3.操作系统为什么要做管理?

操作系统帮助用户,管理好下面的软硬件资源。

操作系统为了给用户提供一个良好(稳定、高效、安全)的运行环境。

系统调用接口和用户操作接口的说明

操作系统里面会有各种重要的数据,由于用户之中可能坏人,可能会随意修改操作系统中重要的数据,因此操作系统并不信任任何用户。操作系统为了保证自身的安全,也为了保证能够给用户提供服务,操作系统以接口的方式给用户提供调用的入口,用于获取操作系统内部的数据,操作系统的接口是操作系统自身提供的。对操作系统自身内部函数的调用称为系统调用
所有访问操作系统的行为都只能通过系统调用完成,就如同银行并不相信客户,但为了让客户依就可以在银行进行存取钱借贷等操作,就在银行柜台前只开了一个小小的窗口,而玻璃窗都是防弹玻璃防止不法行为的发生。
在系统调用的使用上,功能比较基础,对用户的要求也相对较高。所以一些技术高超的开发者对操作系统接口进行了适度封装,从而形成库、指令、shell外壳等更有利于上层用户的使用或开发者进行二次开发。

4.操作系统怎么进行管理?

操作系统是通过分析软硬件的数据信息对软硬件进行管理的。

对操作系统、驱动程序、底层硬件的说明

操作系统具体的管理方式:先描述再组织

1.用类或结构体描述每个软硬件数据信息(共同属性)。

2.操作系统从驱动程序获取到类描述生成的所有对象的数据。

3.操作系统用链表结构或其他高效的数据结构将这些数据对象组织起来,每个数据对象会有特定的编号。

4.最后操作系统对组织后的结构进行管理(增删查改等操作)。

三、进程

1.概念

一个已经加载到内存的程序叫做进程(任务)。

一个操作系统,不仅仅能只运行一个进程,可以同时运行多个进程。

2.对进程的描述

操作系统管理进程同样也是用的先描述再组织的方式。

任何一个进程在加载到内存的时候在形成真正的进程时操作系统要先创建一个描述进程属性的结构体对象(PCB)。

PCB:process ctrl block - - - 进程控制块,是进程属性的集合。

进程=内核PCB数据结构对象+代码和数据

操作系统对进程的管理:本质上就是对PCB组织后形成的链表结构进行管理

Linux操作系统中的PCB是task struct。

task struct内容分类

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

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

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

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

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

上下文数据:进程执行时处理器中寄存器的数据。

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

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

其他

3.查看进程

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

ls /proc

第二种查看进程的方式:ps指令

ps ajx :查看所有进程。

ps ajx | head -1 :查看进程的第一行。

cpp 复制代码
[xxl@VM-0-2-centos ~]$ ps ajx | head -1
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND

ps ajx | head -1 && ps ajx | grep test :同时执行两条指令,第一条是查看进程的第一行,另一条是在显示的进程中过滤出含有test关键字的进程。

注:grep --color=auto test这条进程的出现是因为grep过滤进程中也有test关键字,使用grep -v grep可以屏蔽这条进程。

ps ajx | head -1 && ps ajx | grep test | grep -v grep:过滤出含有test的进程,并且过滤掉含有grep的进程。

杀死进程(强制干掉进程)

kill -9 进程PID(进程编号)

4.通过系统调用接口查看进程的PID和PPID

PID为进程id

PPID为父进程id

getpid() 函数能查看进程id,getppid() 能查看父进程id,这两个函数所需要的头文件都是**#include<sys/types.h>。**

cpp 复制代码
[xxl@VM-0-2-centos test]$ cat text.c
#include<stdio.h>
#include<sys/types.h>
int main()
{
    printf("当前进程的id:%d,父进程的id:%d\n",getpid(),getppid());
    return 0;
}
[xxl@VM-0-2-centos test]$ ./text
当前进程的id:13885,父进程的id:8185

5.通过系统调用创建进程--fork

创建进程的方法有两种

  1. ./ 运行可执行程序--属于指令级别的创建进程。

2.fork指令--属于代码层面创建子进程。

fork创建子进程

运行man fork认识fork。

fork有两个返回值。

父子进程代码共享,但数据各自私有一份,并且是以写时拷贝的方式私有的。

fork之后通常用if进行分流。

执行text.c

cpp 复制代码
[xxl@VM-0-2-centos test]$ cat text.c
#include<stdio.h>
#include<sys/types.h>

int main()
{
    printf("hello world\n");
    fork();
    printf("hello Linux\n");
    return 0;
}
[xxl@VM-0-2-centos test]$ ./text
hello world
hello Linux
hello Linux

执行test.c

cpp 复制代码
[xxl@VM-0-2-centos test]$ cat test.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    printf("当前进程pid:%d   ppid:%d\n",getpid(),getppid());
    pid_t id=fork();	//使用fork创建子进程
    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
    {
    }
    return 0;
}
[xxl@VM-0-2-centos test]$ ./test        //执行test
当前进程pid:16053   ppid:8185
我是父进程,pid:16053    ppid:8185
我是子进程,pid:16054    ppid:16053
我是父进程,pid:16053    ppid:8185
我是子进程,pid:16054    ppid:16053
我是父进程,pid:16053    ppid:8185
我是子进程,pid:16054    ppid:16053

原本test只是一条进程,在调用fork后产生了一条子进程。

对fork的介绍

fork通过复制调用的进程创建一条新进程。新进程称为子进程,子进程就是父进程的一份副本。fork会返回两次,会给子进程返回0,给父进程返回子进程的PID。

fork的4个问题

1.为什么fork要给子进程返回0给父进程返回子进程的PID?

返回不同的返回值是为了区分,让不同的执行流执行不同的代码块,一般而言fork之后的代码父子进程共享,因为一个父进程可能会有多个子进程但通常一个子进程只会有一个父进程,给父进程返回子进程的PID是为了让父进程精确的找到并控制对应的子进程。

2.fork函数里面到底在干什么?

fork函数里面在创建子进程。

3.fork函数是怎么做到返回两次的?

在fork函数中创建完子进程后,父子进程就都可以被CPU调用了,而且return返回函数也属于父子共享的代码,因此父子进程都会返回一个pid_t类型的值,因此fork函数会返回两次。

4.一个变量里面为什么会有不同的内容?例如执行test时id既能等于0进入打印"我是子进程"的代码块,也能大于0进入打印"我是父进程"的代码块。

fork在返回时发生了写时拷贝,子进程识别id时读取到的是新开辟的内存数据,父进程识别id时读取的是旧的内存空间里面的数据。

所有进程都是bash进程的子进程。

四、进程状态

运行态(runing):又称为R状态,凡是在运行队列里的进程都处于运行态

一个CPU里面会有一个运行队列(runqueue),所有在运行队列里排队或执行的进程都处于运行态。

一个进程只要是把自己放到CPU上开始运行了,并不是一直到执行完毕才把自己放下来。例如有一个进程里有一个死循环while(1),但其他进程依旧可以正常运行。

因为每一个进程都会有一个叫做时间片的概念。例如时间片是10ms,如果这个进程运行满10ms后就会被放到进程尾部进行排队。

在一个时间段内CPU需要保证所有的进程代码都会被执行。--并发执行

大量把进程从CPU上放上去,拿下来的动作叫做进程切换

阻塞状态:等待队列中的进程都处于阻塞状态。

一个CPU只有一个运行队列,但每个设备或进程都会有自己的等待队列,例如有一些进程需要从键盘中读取数据,但并没有人为在键盘中输入数据,那么这个进程就会进入等待队列,等待数据的输入。

挂起状态:将阻塞队列中进程的代码和数据放入磁盘中,只保留进程PCB对象(task_struct)进行排队。

当操作系统中内存资源严重不足时,为了给内存空间节省出一大片空间,会将阻塞状态下的进程的代码和数据放入磁盘中,只保留进程PCB对象(task_struct)进行排队,当进程准备好后再将代码和数据换入进入运行状态,这种状态就是挂起状态,全称阻塞挂起状态。

Linux进程状态

linux内核源代码

cpp 复制代码
/*
* 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状态(runing)运行态,并不意味着进程一定在运行中,进程也可能是在运行队列中等待。

查看运行态:

cpp 复制代码
#include<stdio.h>
int main()
{
    while(1);
    return 0;
}

执行while(1);死循环,并用指令**ps ajx \|head -1 \&\& ps ajx \|grep text**查看进程状态。

S状态(sleeping)浅度睡眠状态,对应着阻塞状态,表示该进程正在等待某种资源的状态。

查看浅度睡眠状态:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
int main()
{
    while(1)
    {
	    printf("hello world\n");
	    sleep(2);
    }
    return 0;
}

执行打印hello world死循环,并用指令**ps ajx \|head -1 \&\& ps ajx \|grep text**查看进程状态。

注:R+和S+都表示进程在后台运行

D状态(disk sleep)深度睡眠状态,也称磁盘状态 ,有时候也叫不可中断睡眠状态(uninterruptible sleep),表示该进程正在等待磁盘写入完毕期间,且该进程不能被任何人杀掉,操作系统也杀不掉该进程。如果系统中显示出现了一个D状态的进程就说明这个系统已经快崩溃了。D状态不会响应任何请求,除非D状态自己醒来或者拔掉电源线。

查看D状态:

**dd**命令,但可能会有风险。

T状态(stopped)停止状态,也类似于阻塞状态,与S状态不同的是T状态是被主动控制的,可能是在等待资源,也可能并没有等待资源。

查看T状态:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    printf("当前进程pid:%d   ppid:%d\n",getpid(),getppid());
    pid_t id=fork();	//使用fork创建子进程
    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
    {
    }
    return 0;
}

使用gdb调试打断点时,此时进程就处于T状态。或者是使用SIGSTOP**kill -19 进程编号** 指令停止进程,SIGCONT**kill -18 进程编号**指令启动进程。

X状态(dead)终止状态,这个状态只是一个返回状态,你不会再进程列表中看到这个状态。

Z状态(zombie)僵尸进程状态,进程一般退出的时候如果父进程没有主动回收子进程信息,子进程会一直让自己处于Z状态,进程相关资源尤其是PCB(task_struct)结构体不能被释放。僵尸进程在父进程退出的瞬间会被操作系统进程回收。

查看Z状态:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    printf("当前进程pid: %d,ppid: %d\n",getpid(),getppid());
    pid_t id=fork();
    if(id==0)
    {
	    int cnt=2;
	    while(cnt)
	    {
	        printf("我是子进程,pid: %d,ppid: %d,cnt: %d\n",getpid(),getppid(),cnt--);
	        sleep(3);
	    }
    }
    else
    {
	    while(1)
	    {
	        printf("我是父进程,pid: %d,ppid: %d\n",getpid(),getppid());
	        sleep(3);
	    }
    }
    return 0;
}

执行text后使用指令**while :; do ps ajx \| head -1 \&\& ps ajx \| grep text; sleep 3; done**循环显示过滤后的text进程就可以看到Z状态的进程了。

僵尸进程的危害:内存泄漏

孤儿进程

父子进程,当父进程先退出,子进程的父进程会被改成1号进程(操作系统进程),表示该进程被系统进程领养,因为这个孤儿进程未来也会退出,也需要被释放。
查看孤儿进程:

cpp 复制代码
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
    pid_t id=fork();
    if(id==0)
    {
	    while(1)
	    {
	        printf("我是子进程,pid: %d,ppid: %d\n",getpid(),getppid());
	        sleep(3);
	    }
    }
    else
    {
	    int cnt=1;
	    while(cnt)
	    {
	        printf("我是父进程,pid: %d,ppid: %d,cnt: %d\n",getpid(),getppid(),cnt--);
	        sleep(3);
	    }
	    printf("父进程已经退出\n");
    }
    return 0;
}

执行orphan可执行文件,然后使用指令**while :; do ps ajx \| head -1 \&\& ps ajx \| grep orphan\|grep -v grep; sleep 3; done**查看orphan的PPID变化,就可以发现orphan的PPID在父进程断开后就变成了1。

注:孤儿进程无法用Ctrl+C退出,不过可以使用kill指令退出。

五、进程优先级

1.基本概念

优先级是什么?

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

为什么会产生优先级?

因为资源是有限的,进程是多个的,注定了进程之间是竞争关系!--竞争性

操作系统必须保证各个进程良性竞争,所以设定了优先级。

如果有些进程长时间得不到CPU资源,该进程的代码就长时间无法得到推进--进程的饥饿问题。

优先级怎么实现的?

PRI表示优先级,NI表示优先级的修正数据。优先级是由PRI和NI组合实现的。

2.查看系统进程

在Linux操作系统中使用ps -l指令可以输出以下内容:

UID:用户id,指令ls -aln可以查看用户id。

PID:当前进程id。

PPID:父进程id。

PRI:进程优先级。

NI:进程优先级的修正数据。

3.PRI和NI

1.PRI表示的是进程的优先级,或者通俗来说就是程序被CPU执行的先后顺序,此值越小,进程的优先级越高。

2.NI就是所谓的nice值,可表示为进程可被执行的优先级的修正数值。

3.PRI的值越小,越快被执行,加入nice值之后,将会使得PRI(新)=PRI(旧)+NI。注:PRI(旧)的值一直等于80。

4.当NI的值为负值时,PRI会变小,程序的优先级会变高;当NI的值为正值时,PRI会变大,程序的优先级会变低。

5.因此调整优先级在Linux中就是调整NI值。

6.NI值的取值范围是-20,19,一共是40个等级。

查看和修改进程NI值的方法

输入**top**指令可以查看所有进程的优先级信息。

修改进程优先级的步骤:

1.切换成root用户,非root用户无法修改优先级。

2.输入**top** 指令后按"r"键-->输入需要修改的进程的PID-->输入nice值-20,19,如果超出范围则按照边界进行计算。

4.进程切换

操作系统中进程切换的方式:

CPU的运行队列结构中会有runing队列和一个waiting队列,runing队列里的进程在根据优先级顺序依次运行,过完一个时间片后会将进程继续按照优先级排到waiting队列中,新增的进程也会在waiting队列中进行排队,当runing里面所有的进程都运行完后运行队列会切换runing和waiting,再次按照优先级顺序运行进程。

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

CPU中的寄存器对于进程切换的作用

CPU中有很多寄存器

通用寄存器:eax、ebx、ecx、edx。

栈帧:ebp、esp、eip。

状态寄存器:status。

为了提高效率,会将进程的高频数据放入到寄存器中。CPU寄存器中保存的是进程运行时进程的临时数据(进程的上下文)。进程从CPU上离开时要将自己的上下文数据保存好甚至带走,保存的目的是为了当进程切换后再进入CPU时能够恢复进程自身的上下文数据。

因此进程在被切换时:

1.保存上下文数据。

2.恢复上下文数据。

3.进程的临时数据在保存时会被保存在进程的PCB中

额外知识

1.查看kill功能列表指令

cpp 复制代码
kill -l
cpp 复制代码
[xxl@VM-0-2-centos 913]$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	

2.间隔1s无限显示进程状态指令

cpp 复制代码
while :; do ps ajx | head -1 && ps ajx | grep test; sleep 1; done

3.进程之间的竞争性

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

4.进程的独立性

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

5.并行

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

6.并发

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

相关推荐
ShineWinsu1 小时前
对于Git:基础操作的超详细保姆级解析
linux·c++·git·面试·备份·管理·版本控制器
菜鸟学编程o1 小时前
Linux常见指令
linux·运维·服务器
linan1012 小时前
android调用C++通用方式
linux·架构·智能硬件
imaol13 小时前
网络通信-网络编程
linux·udp
道尔柯南4 小时前
【Linux】线程概念与控制
linux
wuminyu4 小时前
Markword在紧凑对象头上的实现原理剖析
java·linux·c语言·jvm·c++
酸菜。4 小时前
DW_apb_wdt总结
linux
虚无的纽扣4 小时前
【Linux】一篇文章带你吃透进程控制:fork、wait、exit到底在做什么
linux·ubuntu
raindayinrain4 小时前
Linux 网络编程--套接字选项
linux·网络·套接字选项