【Linux】11:进程概念

【Linux】11:进程概念
目录1.1 引出进程1.2 进程的基本概念1.3 理解进程1.3.1 描述进程1.3.2 组织进程二、查看进程2.1 通过ps命令查看进程2.2 杀掉进程2.2.1 使用ctrlc2.2.2 使用kill -9 pid2.3 通过ls / proc命令查看进程2.4 查看进程父进程的信息2.5 总结三、如何创建进程3.1 fork函数3.2 fork函数的返回值3.3 fork函数的三个问题3.3.1 为什么fork给父进程返回子进程pid给子进程返回03.3.2 什么一个函数会返回两次如何理解3.3.3 为什么一个变量即0又大于0导致if else同时成立3.4 总结1.1 引出进程首先我们来看一个问题操作系统能不能一次运行多个程序呢---------当然可以啦因为运行的程序有很多所以 OS 需要将这些运行的程序管理起来。我们将这些正在运行的程序称之为进程。注意是正在运行的程序叫进程而不是程序本身如何管理这些进程呢—— 先描述再组织操作系统会创建一个描述和控制该进程的结构体。这个结构体称之为进程控制块PCBProcessing Control Block里面包含了该进程几乎所有的属性信息同时通过进程控制块也可以找到该进程的代码和数据。在 Linux 中进程控制块就是 struct task_struct 结构体。描述好所有进程了还需要将所有进程的 PCB 给组织起来通过双链表的方式此时操作系统只需要拿到双链表的头指针就可以找到所有进程的 PCB。OS 把对进程的管理就转换成了对数据结构中 PCB 的管理即对双链表的增删查改操作。假设这里有一个可执行程序 test它存储在磁盘上就是一个普通文件当我们 ./test 运行此程序操作系统会做以下事情将该程序从磁盘加载到内存中并为该程序创建对应的进程申请进程控制块PCB。总结为什么要存在 PCB 呢—— 因为 OS 要对进程进行管理。目前对于进程的理解进程 程序代码 数据 内核申请的与该进程对应的数据结构PCB。1.2 进程的基本概念首先读者要知道的是什么是进程【课本概念】程序的一个执行实例正在执行的程序等【内核观点】担当分配系统资源CPU时间内存的实体对于课本中的观点大家可能会觉得难以理解为何正在执行的程序就是一个进程呢。我们可以在Windows下按[Ctrl shift ESC]打开任务管理器查看一下可以看到左上角的这个【进程】标志代表呢我们下面所运行的程序都是一个进程这也就表明了在一个操作系统中不仅只能运行一个进程还可以运行多个进程但是呢进程不仅仅可以像上面这样去理解。我们来思考一个问题程序是文件吗相信读者肯定很清楚文件是存放在磁盘中的磁盘呢则是属于外设。这一块我们在冯·诺依曼体系结构有讲得很清楚对于CPU来说它是只会和内存打交到的所以磁盘中的数据需要先加载到内存中才可以被执行那么当可执行文件被加载到内存中时该程序就成为了一个【进程】总结进程 程序代码 数据 内核申请的与该进程对应的数据结构PCB。1.3 理解进程在了解了进程的基本概念后我们来更加进一步地认识一下进程管理是如何实现的那肯定是---- 先描述进程----在组织进程1.3.1 描述进程在 Linux 中我们使用一个结构体去描述一个进程因为 Linux 的内核源代码都是使用C语言来写的而 结构体 则是C语言里面的一些知识不懂的同学可以先去了解一下。并且呢这个结构体还有名字的它叫做【进程控制块】----- PCB课本中的叫法呢是PCBProcess Control BlockLinux操作系统下的PCB是task_struct这个结构体呢就是组织了各种各样的属性才可以去很好地描述一个进程以下就是这个task_struct的所有结构信息:(后续我们会逐一讲解)标示符: 描述本进程的唯一标示符用来区别其他进程。状态: 任务状态退出代码退出信号等。优先级: 相对于其他进程的优先级。程序计数器: 程序中即将被执行的下一条指令的地址。内存指针: 包括程序代码和进程相关数据的指针还有和其他进程共享的内存块的指针上下文数据: 进程执行时处理器的寄存器中的数据[休学例子要加图CPU寄存器]。IO状态信息: 包括显示的I/O请求,分配给进程的IO设备和被进程使用的文件列表。记账信息: 可能包括处理器时间总和使用的时钟数总和时间限制记账号等。其他信息转换为代码形式的话就可以是下面这样struct PCB{进程的编号进程的状态进程的优先级...相关的指针信息};既然知道了如何去描述一个PCB结构体我们就要来知道操作系统对一个进程总共会做哪些事情为该进程创建对应的PCB对象将该进程的代码和数据加载到内存中所以很多教科书在介绍进程的时候只会说它在计算机内部是一个PCB对象其实对于一个进程来说应该是由操作系统为其创建出来的 PCB对象 其数据代码 组成的总结【进程】 内核PCB数据结构对象描述你这个进程所有的属性值 你自己的代码和数据在磁盘当中所形成的可执行程序1.3.2 组织进程上面谈到了操作系统如何利用PCB去描述一个相关的进程接下去我们来说说如何去组织进程大学的校园我们都知道比中学的大多了。但是呢在学校里每个学生他的人身是自由的不可能每个人拿着一个牌子上面自己写上个人的基本信息这是不现实的。比方说学院的辅导员想要找一个学生的话一定是通过在学籍系统中找到这个学生的所在行记录才可以对其进行相关的操作读者可以认为这个信息记录即为我们在上面所讲到的【PCB对象】因此操作系统如果要去管理一个进程的话只需要找到某个进程的PCB对象即可通过这个PCB对象就可以找到相对应的代码和数据那这个PCB结构体中就一定存在一个东西叫做结构体指针里面保存了某个内容的地址方便我们很快地知道其它对应的PCB信息struct PCB{进程的编号进程的状态进程的优先级...相关的指针信息struct PCB* next;};这样可能还是抽象了一点我们再举个形象点的案例读者可能是在校大学生亦或者是已经工作的人。但是无论是谁都要去投递简历找工作。如果读者有当过面试官的话一定可以知道每天公司都会收到成千上百份的简历因为面试官无法看到投递者这个人所以其只能通过简历来识别这个人那么读者就可以认为每一份简历就相当于是每个进程所专属的【PCB对象】所以面试官对于面试者的管理就是对简历的管理。让进程去某个队列里排队不是让代码去排队而是进程的PCB结构体对象去排队我在一开始的时候就讲到过在内存中不仅仅是跑着一个进程而是有可能会存在多个进程。那对于这多个进程要如何去起到关联呢这一点其实我们在讲操作系统的时候也有讲到过我们学习编程语言像C/C、Java可以帮助我们去很好地描述一个抽象的事物我们学习数据结构与算法可以帮助我们更好地去组织相同的对象数据那么在Linux内核中最基本的组织进程的是用task_struct的方式采用【双向链表】组织的那有了上面的这个理论操作系统在对于进程的管理就转换成了对于链表的增删查改二、查看进程明白了操作系统如何去描述并组织进程接下去我们就切身地来看一看进程长什么样吧makefile文件1 code:code.c2 gcc -o $ $^3 .PHONY:clean4 clean:5 rm -f code测试代码1 #includestdio.h2 #includeunistd.h34 int main()5 {6 while(1)7 {8 printf(我是一个进程\n);9 sleep(1);10 }11 return 0;12 }我们使用make自动化工具进行编译然后运行如下所示按ctrl c可以结束运行我们历史上执行的所有指令工具自己的程序运行起来全部都是进程现在我们运行code这个程序那我们如何查看这个进程的属性呢下面我们来学习一个系统调用getpid这个系统调用的作用是返回该进程的pid我们使用man指令介绍如下所示谁调用这个函数就返回这个进程的pid。那么从哪里得到这个进程的pid呢答案是从PCB进程控制块中得到。我们修改测试代码让程序执行的时候显示出进程的pid如下所示1 #includestdio.h2 #includeunistd.h3 #includesys/types.h45 int main()6 {7 while(1)8 {9 printf(我是一个进程!我的pid%d\n,getpid());10 sleep(1);11 }12 return 0;13 }运行如下所示2.1 通过ps命令查看进程我们再次打开一个xshell让上面的那个xshell运行程序让新打开的xshell来查看运行了那些进程。使用ps axj可以查看系统里面运行的进程如下所示但是linux系统里面运行的进程太多了我们现在想要查看我们刚刚运行的进程可以使用 ps axj | grep code如下所示使用 ps axj | grep code之后就显示了code进程相关的信息但是我们并不知道这些信息是什么可以使用ps axj | head -1来显示属性列然后在后面写上分号再次写上ps axj | grep code如下所示也可以使用如下所示ps axj | head -1 ps axj | grep code当我们查code进程的时候那第二行是什么呢当我们查进程的时候这个grep选项总是会被显示出来这是因为整个命令从左向右查的时候grep也是一个命令我们上面刚刚说过linux系统下的指令执行起来也是一个进程grep过滤条件里面本来就包含了code所以grep进程也会显示出来。如果不想要显示grep可以使用如下指令ps axj | head -1 ps axj | grep code | grep -v grep2.2 杀掉进程2.2.1 使用ctrlc当我们进程运行的时候我们可以使用ctrlc来杀掉进程如下所示然后我们再次查看进程的时候就没有了如下所示2.2.2 使用kill -9 pid我们把进程运行起来之后如下所示使用kill -9 2888380杀掉进程如下所示2.3 通过ls / proc命令查看进程我们将刚刚的程序运行然后使用ls /proc来查看进程如下所示我们可以通过文件的方式去查看进程操作系统不仅仅可以把磁盘上的文件使用ls这种命令展现出来还可以把内存的数据也以文件的方式呈现出来让我们可以动态的看见内存的相关数据。像proc就是内存级别的文件系统跟磁盘没有关系里面放的是当前系统实时的进程信息。但是呢上面这些呢是全部的进程若我们只是想要查看某个进程的话就要根据其PID值去进行对应的查找。这个PID呢就是我们在上面在介绍task_struct这个Linux下的PCB结构体的时候所讲到的【标识符】这个东西它是 描述本进程的唯一标示符用来区别其他进程然后我们查看这个文件的详细信息如下所示ls /proc/2909582 -l这里我们主要关注两个一个是cwd目录另一个则是execwd叫做current work dir表示进程当前工作目录。这个exe对应的是进程的可执行文件。一个进程被cpu执行的时候它知道自己是从哪里来的。是启动哪一个程序时才有了我这个进程。我们可以将这个文件删了程序现在还是能运行的这是为什么呢前面刚刚说过cpu是不访问磁盘的我们执行这个程序时已经把这个程序的数据拷贝到内存了我们删除这个文件是磁盘的文件内存中还是有这个程序的相关数据和代码的所以还是能正常执行进程。当我们杀掉进程之后这个文件就不存在了如下所示2.4 查看进程父进程的信息我们执行code可执行程序创建一个进程那这个进程是谁创建的呢答案是父进程。在上面我们使用man来查看getpid的时候有一个getppid,如下所示这个getppid就是查看这个进程父进程的pid我们编写代码来查看父进程id如下所示1 #includestdio.h 2 #includeunistd.h 3 #includesys/types.h 4 5 int main() 6 { 7 while(1) 8 { 9 printf(我是一个进程!我的pid%d,我的父进程pid%d\n,getpid(),getppid()); 10 sleep(1); 11 } 12 return 0; 13 }运行如下所示我们运行code程序之后系统给我创建了一个进程进程pid是2918243父进程pid是2621879.然后我们重新运行几次如下所示我们发现我自己的进程pid是一直变化的而父进程pid是不变的。其实的话我自己的进程重新启动之后pid发生了变化这个现象是很正常的每次重新启动进程其 PID 值是会出现不同的情况举个很简单的例子来说吧小王在高考结束完后上了一所不是很理想的大学在开学前两天时学习为其分配了对应的学号。但是呢小王却并不满意自己所待的这个学校所以就退了学继续参加高考在又一次的高考结束后他还是被原来的这所学校给录取。但是呢我们可以知道即使你进了一个学校两次但是学号却不一定是一样的这也就是为什么一个进程在重新启动后会出现不同PID值的原因。但是为什么父进程的pid不变呢我们的父进程是谁啊我们使用父进程pid来查看进程如下所示ps axj| head -1ps axj | grep 2621879 | grep -v grep我们发现我们自己启动的程序是bash进程创建的也就是我自己启动的程序父进程都是bash。bash就是命令行解释器。命令行解释器本质上就是一个进程。其实我们每一次登录我们的云服务器时操作系统会给每一个登录用户分配一个bash也就是说我们每次登录一次就会创建一个bash进程。现在我们理解了bash是一个进程我们在使用xshell连接我们的云服务器的时候光标的前面是什么这就是bash打印出来的字符串bash进程打印出来之后它就在这里等着如果我们没有输入任何命令bash就卡在这里了。所以我们命令行输入的所有命令都是喂给了bash进程bash拿到命令就给我们分析了然后把结果返回给我们。原来bash就是一个进程啊先打印一个字符串然后等待我们输入命令帮我们执行最后把运行结果返回给我。所以我们命令行上执行的lspwdtouch等等这些命令执行后都会创建相应的进程父进程都是bash2.5 总结1在程序里面使用系统调用函数getpid查看进程的pid2使用 ps ajxps axj | grep codeps axj | head -1 ps axj | grep codeps axj | head -1 ps axj | grep code | grep -v grep3使用ls /proc4ctrlc杀死进程5kill -9 pid 杀死pid进程三、如何创建进程创建新进程在Linux的下是由父进程来完成的创建完成的新进程是子进程。新进程的地址空间有两种可能性子进程是父进程的复制品除了PID和task_struct是子进程自己的其余的都从父进程复制而来子进程装入另一个程序。fork函数用于创建一个新的进程使用fork函数来创建一个进程时子进程只是完全复制父进程的资源。这样得到的子进程和父进程是独立的具有良好的并发性。但是进程间通信需要专门的机制。之前我们在Linux下启动一个进程的时候利用的是./可执行程序那是否有其他办法去启动一个进程呢当然是有的那就是使用fork()这个函数。3.1 fork函数在使用之前呢我们要先去查看一下这个函数该如何使用------使用man 手册查询一下 fork 函数的使用。使用命令man 2 fork如下所示fork的作用是创建一个子进程需要包含sys/types.h和unistd.h的头文件。测试代码如下1 #includestdio.h 2 #includesys/types.h 3 #includeunistd.h 4 5 int main() 6 { 7 printf(父进程开始运行,pid%d\n,getpid()); 8 fork(); 9 printf(进程开始运行,pod%d\n,getpid()); 10 return 0; 11 } ~我们编译运行一下如下所示为什么第二个printf语句会被执行两次呢当程序开始执行的时候会先打印第一句此时只有一个执行流然后调用fork函数创建一个子进程一旦fork函数执行完成之后就会有两个执行流了。这两个执行流也就是父进程和子进程都会执行后续的代码所以会执行两次并且pid的值应该不同父进程执行getpid会返回父进程的pid。子进程执行getpid函数会返回子进程的pid。下面我们来说明一下原理一个进程等于PCB加上自己的代码和数据使用fork创建子进程之后系统里面就多了一个进程进程需要有PCB自己的代码和数据那么子进程的PCB,代码和数据从哪里来呢答案是从父进程那里拷贝来的大部分数据是一样的只有少数的属性不一样例如pid和ppid等等。子进程创建的时候默认会执行父进程的代码和数据所以子进程在被调度的时候子进程就会执行父进程之后的代码第二个printf语句就会被执行两次。子进程被创建子进程现在是没有自己的代码和数据的因为目前没有给子进程加载新的代码和数据。所以默认共享父进程的代码和数据。虽然子进程能够看见第一个printf语句但是父进程以及执行过了子进程不需要再次执行。3.2 fork函数的返回值在3.1 我们简单使用了fork函数来创建一个子进程那么fork函数的返回值是什么呢我们使用man来查看一下如下所示这句话描述的是在成功创建子进程的情况下会把子进程的pid返回给父进程0被返回给子进程如果创建子进程失败-1被返回给父进程。这句话的意思是fork函数会返回两个不同的值函数怎么能返回两个不同的值呢现在我们想让父子进程来执行不同的代码逻辑代码如下所示1 #includestdio.h 2 #includesys/types.h 3 #includeunistd.h 4 5 int main() 6 { 7 printf(父进程开始运行,pid%d\n,getpid()); 8 pid_t idfork(); 9 if(id0) 10 { 11 perror(fork fail); 12 }else if(id0) 13 { 14 //子进程逻辑 15 while(1) 16 { 17 sleep(1); 18 printf(我是一个子进程我的pid%d,我的父进程id%d\n,getpid(),getppid()); 19 } 20 }else 21 { 22 //父进程逻辑 23 while(1) 24 { 25 sleep(1); 26 printf(我是一父进程进程我的pid%d,我的父进程id%d\n,getpid(),getppid()); 27 } 28 } 29 return 0; 30 }运行如下所示父进程的pid是3209461父进程的父进程id是2621979也就是bash进程。子进程的pid是3209462子进程的父进程id是3209461。父子进程执行了不同的代码块在fork之后父子代码是共享的只不过父进程认为自己的返回值是大于0执行大于0的逻辑子进程认为自己的返回值是0执行0的逻辑。所以就能做到父子进程执行不同的代码块。现在就有以下几个问题1为什么fork给父进程返回子进程pid给子进程返回02为什么一个函数会返回两次如何理解3为什么一个变量即0又大于0导致if else同时成立3.3 fork函数的三个问题3.3.1为什么fork给父进程返回子进程pid给子进程返回0上面我们说到当进程的代码执行到fork()函数的时候会将执行流一分为二父子进程通过不同的 id 返回值来区分以此执行不同的代码块。那其实很好理解了因为父子进程是两个不同的进程所以需要根据这个不同的返回值来进程区别那为什么父进程得到的是子进程的PID但是子进程却是0呢为什么不可以倒过来一个父亲可以有多个孩子但是呢一个孩子却只能有一个父亲 父亲所获取到的返回值是子进程的PID是由于他要靠不同的PID值来区分不同的孩子但子进程的返回值都是0的原因在于他一定只对应着某一个父进程只需让父进程知道它被成功创建出来了即可3.3.2 什么一个函数会返回两次如何理解如果一个函数已经执行到了return了这个函数的核心功能做完了嘛答案是已经做完了。fork函数的核心功能是创建一个子进程当执行到return之后这个子进程是不是已经被创建了。在执行return之前子进程已经被创建了是不是申请新的PCB了,然后拷贝父进程PCB,甚至将子进程放入调度队列中也可能正在执行这个子进程return是一个语句嘛答案肯定是的父子进程是执行相同的代码和数据的所以子进程会执行这个return同样的父进程也会执行这个return语句。所以return执行两次父子进程各自返回一次。3.3.3 为什么一个变量即0又大于0导致if else同时成立在我们上面的代码中我们会发现 id 这个变量会有两个不同的值这是为什么呢父进程创建子进程之后子进程共享父进程的代码那么父进程的数据也是共享的嘛答案也是共享的。那么子进程不就可以修改父进程的变量了吗例如父进程里面有一个全局变量然后子进程修改这个变量不就导致父进程的执行逻辑发生变化了吗虽然父子进程共享数据但是一旦有哪一个进程想要修改数据操作系统就会把要修改的数据在底层拷贝一份让目标进程修改这个拷贝所以父子进程修改数据不影响彼此的逻辑。这个概念叫写时拷贝。测试代码如下1 #includestdio.h 2 #includesys/types.h 3 #includeunistd.h 4 5 int gval100; 6 7 int main() 8 { 9 printf(父进程开始运行,pid%d\n,getpid()); 10 pid_t idfork(); 11 if(id0) 12 { 13 perror(fork fail); 14 }else if(id0) 15 { 16 //子进程逻辑 17 printf(我是一个子进程我的pid%d,我的父进程id%d,gval%d\n,getpid(),getppid(),gval); 18 sleep(5); 19 while(1) 20 { 21 sleep(1); 22 printf(子进程修改变量%d-%d\n,gval,gval10); 23 gval10; 24 printf(我是一个子进程我的pid%d,我的父进程id%d,gavl%d\n,getpid(),getppid(),gval); 25 } 26 }else 27 { 28 //父进程逻辑 29 while(1) 30 { 31 sleep(1); 32 printf(我是一个父进程我的pid%d,我的父进程id%d,gval%d\n,getpid(),getppid(),gval); 33 } 34 } 35 return 0; 36 }运行如下所示我们可以看到刚开始的时候父子进程的gval都是100然后子进程开始修改gval发现子进程的gval变化了而父进程的gval还是100当进程里数据没有发生修改时。那么子进程和父进程共享代码和数据如果有一个进程的数据发生了修改那么会对数据发送写时拷贝而代码依旧是共享的。现在回到上面的代码id是变量嘛父子进程是不是都可以访问这个id。在子进程里面fork会返回0就需要修改id的值此时就发生了写时拷贝。在父进程记录fork会返回子进程的pid同样也需要修改id的值也发生的写时拷贝。这就解释了一个变量为什么会有不同的值3.4 总结1.子进程是由父进程创建的2.子进程和父进程一般情况下共享代码和数据但如果有一方的数据或代码被修改那么操作系统会写时拷贝一份。3.fork函数不是真的有2个返回值而是两个进程都在fork函数执行了返回操作。4.fork返回父进程子进程的PID给子进程返回0进程创建失败返回-1