进程
(一)计算机基础
(二)进程
系统的每一个进程都是 bash 通过 fork() + exec () 得到的
1.进程基础
操作系统:(Windows 10)是一个软件,一个管理系统,用来管理计算机上的软硬件资源,为用户提供一个交互的接口,用户通过该接口来使用计算机
类比:学生管理系统,
进程:一个正在运行的程序(Windows系统下双击把QQ打开就是一个进程)
进程由操作系统进行管理:
- 每一个进程是一个活动的实体
- 所有进程共享内存,在内存中占用内存空间
- 使用一些软件资源
因此需要将每一个进程唯一标识出来,故给每一个进程分配一个ID号
操作系统通过一个双向链表来管理进程
2.进程状态
3.并发与并行
4.内存管理
(1) 物理内存
(2)虚拟内存
进程地址空间:
使用虚拟内存的原因:
物理内存较小不够用,此时就需要将物理内存扩展
虚拟内存:将磁盘划分一块空间当内存来使用
程序运行是在虚拟地址空间上进行,虚拟地址空间将程序内容映射到内存空间进行执行
磁盘和内存相比,磁盘速度非常慢,但是磁盘空间大,够用。此时计算机就可以运行大于自身内存的程序,不用将所有程序都放入内存中,执行哪一部分就先将该部分放入内存中,执行下一部分时若不在内存中就将下一部分搬进内存,内存放不下时,先将不用的部分换出去(系统将长时间不使用的数据替换到磁盘上一块指定的空间,这块空间就叫虚拟内存),保证执行程序时它的那一部分在内存中。若程序需要使用存放在虚拟内存的数据,虚拟地址空间不能直接到磁盘上取数据,数据要先从虚拟内存导入内存,然后从内存到虚拟地址空间
(三)Linux进程复制与替换
1.printf函数缓冲区问题
printf函数并不会将数据直接输出到屏幕,而是先存放在缓冲区,只有以下几种情况满足才会输出到屏幕
- 缓冲区满
- printf后加换行符:printf ( “%d” , a \n) ;
- 强制刷新缓冲区:fflush (stdout) ;
- 程序结束
linux系统中使用vi编写的main.c程序结束时一般不使用 return 0 ;
VS中编写程序使用return 0 ; 后系统自动调用了exit (0);
而是使用 exit (0) ;使用exit(0)结束进程前会先刷新缓冲区
exit( ) -->fflush( )—>_exit( )
_exit( ) :直接结束进程,不刷新缓冲区
原因:内核频繁的向屏幕上输出数据,开销较大,效率底
printf函数在底层调用了系统调用的write函数,printf先将需要打印的数据存放在buff[ ] 中,当缓冲区满后,当刷新缓冲区,当程序结束后,write()才会 将buff[ ] 中的数据打在屏幕上
2.主函数参数介绍
3.环境变量 Path
Windows 系统下:Path里存放的是路径。可执行程序存放的地点
Linux系统下:Path里存放的是路径。命令的路径
环境变量和main函数传进的参数一样,都是一些值。只不过这些值是从其父进程中继承过来的,从使用目的来说都是值,都一样;从来源来说不一样,main函数参数是用户自己传进来的,环境变量是从父进程继承来的。
3.1什么是环境变量
环境变量(Environment Variable )
环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。
3.2变量
可以随意给其赋值的一个存储单元
3.3环境
例如jvm这些都属于小软件,它们处于操作系统这个大软件中。
3.4环境变量的作用
变量在任何程序中的作用都是“被赋值/被取值”!这个全局变量操作系统可以使用,其内的小软件也可以使用!
4.复制进程 fork
注意处理僵死进程
运行时间比较短的进程不用处理,因为系统会自动处理
怕的是运行时间比较长的进程,同时还fork出很多子进程
通常情况下服务器上运行的进程例如QQ并不是运行一下就结束,因此需要处理好僵死进程
4.1 复制进程原理:
- 向系统申请一个ID号,唯一标识进程。
- 准备PCB用来描述该进程。直接将父进程的PCB复制一份,并将其PID该成新申请的ID号,该复制可以继承父进程的很多信息,例如:子进程依旧属于父进程的用户,父进程在那个终端,子进程也属于那个终端
- 复制进程实体。
复制进程实体时系统采用写时拷贝:父进程在内存中占用若干个页面,复制的子进程若一些页面并不做修改,此时父子进程就可以共享,不需要给子进程复制。除非子进程修改了数据,父子进程无法共享时才需要将需要修改的页面复制出来。因此采用写时拷贝,内存页面的拷贝会被延迟甚至免除 - 新生成的子进程和父进程一模一样
- 通过fork的返回值可以区分父子进程
父进程fork返回值是子进程的PID
子进程fork的返回值是0 - 子进程和父进程二者并发运行(同时运行还是时间片轮转取决于处理器,因为进程数目远远大于处理器数目,处理器还需要运行其他进程)
子进程中并没有执行int n =0 ; 但是子进程从fork之后执行时变量n的值为0,因为子进程复制了父进程,父进程中n =0;fork将父进程的内存空间整体复制给了子进程
sleep(1) :将程序阻塞。反映出来就是,执行到该处,放弃CPU,告诉系统暂时不执行我了,去执行其他进程,所以会有延迟1S
注意:
- 父子进程的pid可能连续,也可能不连续
- 子进程pid一般情况下大于父进程pid,但是有例外,当进程数目到达最大值,系统会重新从头开始分配pid,前面结束进程的pid会被重新使用,这时就会出现子进程pid小于父进程pid的情况
- 系统运行的每一个进程都会有一个父进程
0号进程没有父进程,是管理员自己做出来的进程 - 父子进程中打印的变量地址都是逻辑地址,因此都相同。
逻辑地址是在逻辑页面中的偏移量,因为子进程复制父进程会将逻辑页也复制,因此逻辑地址相同
物理地址是逻辑页映射到物理内存中的偏移量,不同进程在物理内存中映射的页不同,因此物理地址不一样
4.2复制进程的作用
- 复制进程后让多个进程协同完成某一个事情
- 复制出的子进程被另外一种方法替换,替换成另外一种程序
在终端中执行的命令父进程都是bash,bash将自己先复制一份,出现一个新的子进程,然后用执行的命令将其替换:bash先复制出子bash ,后用main 或者ps将其替换就生成了一个新的子进程
4.3 fork练习题
示例一:打印6个A
示例二:打印8个A
示例三:打印3个A
5.僵死进程
5.1退出码:
操作系统要求每一个进程产生的子进程结束后都生成一个退出码,子进程结束退出码存放在PCB中。退出码类似于Windows上编写程序时写的return 0;
代表程序成功结束,是一种情形状态,有可能是return 1等等。无论ruturn 什么,return 的值就是退出码,用来标识执行成功还是失败。在Linux系统上一般return 0 ;代表成功,return 其他标识不同形式的失败。
僵死进程就是已经结束的进程,但是没有消失
5.2原因:
子进程先于父进程结束,父进程没有获取子进程的退出码,此时进程实体已经没有了,但是子进程内核空间的结构体PCB依旧保持(其中存放有退出码),此时系统无法删除子进程的PCB,只有父进程获取了子进程的退出码,系统才会删除子进程PCB,意味着子进程消失,链表节点少了一个。此时就无法看到子进程的任何信息
5.3子进程结束分2步:
- 系统释放进程实体
- 父进程获取退出码,系统删除子进程PCB
5.4 父进程获取退出码:
父进程先结束如何获取子进程退出码
此时父进程先于子进程结束,但是父进程并没有僵死进程产生,说明父进程的父进程(bash)对父进程进行了处理获取了父进程的退出码。
子进程也没有出现僵死进程:
父进程结束,子进程变成了孤儿进程,系统就会重新为该孤儿进程找一个父进程,重新寻找的父进程一定负责获取孤儿进程的退出码,因此不存在僵死进程。书本上规定孤儿进程被1号进程收养,但是由于内核版本的更新,不一定是1号进程。
5.5僵尸进程的影响:
僵死进程少对系统无过大影响,但是僵死进程一旦多了影响就大了
因为如果PCB没有释放内核中结构体的空间就一直没有被处理,软件资源上pid就会一直被占用,无法被复用,系统的ID有限,一旦被耗光,新进程就无法产生
5.6 如何获取子进程退出码
子进程如果运行中途被Kill掉就不会有退出码
父进程wait() 可以获取子进程退出码,
注意wait()只要一执行僵死进程就已经被解决了,
因为当子进程还没有结束,父进程就会阻塞,直到子进程结束wait()得到子进程退出码,父进程才继续执行
退出码明明是3,为何变成了768:
原因:
系统将3(0011)左移了8位(0011 0000 0000 )32位系统,一个整型4字节
退出码的范围是-128 — 127 因此一个字节就足够了,其余字节标识其他信息,例如进程是否正常退出,只有正常退出才会有退出码
方法一:
方法二:
6.操作文件的系统调用
Linux 系统中一切皆文件:
将一些软硬件设备(键盘,磁盘存放的文件,管道…),均当作文件来处理,从而提供了一个统一的接口来访问它
6.1 接口(系统调用):
- open:打开文件
- read:读文件
- write:写文件
- close:关闭文件
man 1(命令) 2(系统调用) 3 (库函数)
6.2 区别系统调用和库函数:
系统调用:产生中断,先入内核,用户空间无法访问外部设备(键盘,磁盘,屏幕…)如果用户一旦访问外部设备,一定会经过内核进行系统调用(例如printf—>write)
库函数:调用库函数,跳到函数入口地址继续执行
- 系统调用是在内核中实现的,编写操作系统时就实现的
- 库函数:fopen(),fread(),fgets()…
fopen()在linux系统上就是调用open()实现的。因此fopen()封装了open()这个系统调用
6.3 打开文件 open
Windows上打开文件的方式:
- 二进制方式
- 文本方式
Linux上没有二进制和文本的区别
文件操作有关的系统调用:
显示文件编号:
6.3.1 文件描述符:
程序每次打开一个文件,都会返回一个整型值,称为文件描述符。系统中
为了标识所打开的文件,每个进程都会生成一个文件表, 每打开一个文件都会在文件表中登记,文件表类似于一个数组,记录该进程打开的文件。
每打开一个进程系统会默认打开3个文件
- 标准输入
- 标准输出
- 标准错误输出
文件表存在于PCB中
printf() 函数是通过系统调用write() 实现的操作
因此可以通过write()直接向屏幕写数据
6.4 示例
6.4.1示例一:
以写的方式打开一个文件,并向文件中写入数据
6.4.2示例二:
读取文件
6.4.3示例三:
利用系统调用实现文件拷贝
方法一:直接将文件名写入程序
方法二:将文件名作为主函数参数,给主函数传参
7.文件描述符与fork()之间的关系
fork()的实现只是一个浅拷贝,给了一个指针指向父进程所指向的地方
7.1示例一:父子进程共享节点
虽然代码结尾只有一个close(fd) 但是由于在if 判断之外,所以父子进程都需要执行,因此文件被关闭了2次。
由于父子进程共用struct file{}; 所以父子进程共享文件偏移量。当父子进程并行运行也可能存在父子进程打印的值一样,因为当父子进程访问时文件偏移量相同
7.2示例二:父子进程不共享节点
8.系统调用过程
9.替换进程 exec 系列
注意:成功不返回
把一个已经存在的进程换成另外一个进程
- 更换进程实体
- PCB不变,意味着原来进程ID号是多少新进程ID号依旧是多少
库函数系列:
库函数里封装了系统调用
系统调用:
最终实现进程替换的根本方法
把当前程序替换成用户指定的程序
9.1 execl
参考书籍:Linux程序设计第4版第11章11.3
l : 将参数一一列举
9.2 execlp
第一个参数给一个文件名就可以,执行它会从系统存放命令的地方寻找该命令,因此是启动系统已有的命令
系统环境变量PATH指向的位置就是存放命令的地点
用冒号分隔出路径,在这些位置就会存放命令
9.3 execle
多一个环境变量,是一个(char * )的数组
不改变环境变量时使用主函数的环境变量
用户可以自定义环境变量
9.4 execv
将参数放在数组中,有较好的通用性
数组大小为10,此时只传递了2个参数,系统默认后面为空指针,所以不用((char *)0)
9.5 execvp
不用加路径,直接写命令名称,由系统寻找
9.6 execve (系统调用)
路径名称 + 参数数组 + 环境变量
(四)信号
概念:通知进程产生了某种事件
信号是发给进程的,通过系统调用 kill() 将信号发送给进程
一.信号响应
用signal() 函数写了很多,此时应该注意只有最后一个signal() 函数有效,因为前面的都被覆盖了
2.忽略
SIG_IGN
1.默认
SIG_DFL
第一次输入Ctrl + c 打印信号代号,第二次输入按照默认情况中断进程
3.用户自定义
默认:原本键盘输入 Ctrl + c 就可以产生SIGINT信号,用来中断进程。注意,不是用户结束的进程,而是进程对信号的响应,自己结束的进程
用户自定义,当键盘输入 Ctrl + c 时打印接收到的信号
拿kill来举例子:
kill 可以结束前台运行的进程,但是当进程中止(Ctrl + z ) 进程就会忽略传递过来的信号,此时kill就无法结束该进程,因此设计时就考虑到这种情况,kill是15号信号,当程序中止,无法执行15号信号时,此时就会通过kill -9 信号结束该进程。9号信号不允许用户通过signal()来改变其响应方式,9号信号只有一个使命,内核收到9号信号就结束进程。因此每当程序启动,无论发生何种情况只有收到9号信号一定结束进程退出
二.例题:向一个指定的进程发送一个指定的信号:
三.信号的实现
感知进程是否收到信号是内核实现的,为了表示信号以及记录信号的响应方式,内核中有定义数据结构(一个整型变量,一个结构体数组)来表示。
信号的表示与响应:
- 内核中有一个整型变量,在进程的PCB中定义着,通过kill向某个进程发生信号,首先会通过pid找到该进程,再将信号的代号填充到整型变量的字节中,64位就可以表示64种不同的信号。
- 例如2号信号,就会将变量第2个bit位置为1;9号信号就会将第9个bit位置为1…
- 因此在极短的时间内重复发送命令是没有意义的,因为在进程还未响应将1变成0之前,重复的发送信号依旧是置为1,信号无法被记住有几个。
- 内核会感知PCB中的整型变量,看它那些位被置为1,就表明收到了那些信号,此后会调用对应的处理函数来响应,对应的处理函数在一个数组中存放
四.利用信号处理僵死进程
子进程结束,内核会发一个信号SIGCHLD,给父进程
子进程先结束并没有处理子进程,所以子进程变成了僵死进程
方法一:信号处理函数中wait()
该方法是通用方法
利用子进程结束发送的信号,调用wait(),此时父进程不会阻塞
方法二:忽略信号
该方法只能在linux系统上实现,uinux系统不可以
显式告诉内核忽略该信号,