Linux操作系统(进程)

(一)计算机基础

在这里插入图片描述

(二)进程

系统的每一个进程都是 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系统不可以

显式告诉内核忽略该信号,

在这里插入图片描述

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值