进程是程序的一个执行实例,是担当分配系统资源(CPU时间,内存)的实体。
进程 = 内核数据结构 + 程序的代码和数据。
把程序运行起来,本质就是在系统中启动了一个进程。进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。Linux操作系统下的PCB是task_struct。
组织进程:所有运行在系统里的进程都以task_struct
链表的形式存在内核里。
查看进程:
/proc
系统文件夹查看。如:要获取PID为1的进程信息,你需要查看 /proc/1
这个文件夹。
top
和ps
这些用户级工具来获取。
通过系统调用getpid()
和getppid()
获取进程ID:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("pid: %d\n", getpid());
printf("ppid: %d\n", getppid());
return 0;
}
新建文件时如果不指定地址,会新建到当前目录下,这是因为每个进程都会记录下来自己对应的程序是谁,还会记录下来自己这个程序启动时所处的工作目录,这个工作目录可以更改(chdir)。
fork()->两个进程->父子关系->代码共享,但是数据各自一份 进程 = 内核数据结构+代码和数据,子进程只有内核数据结构,代码使用父进程的。进程具有很强的独立性,多个进程之间,运行时互不影响,即便是父子。
fork会有两个返回值,因为共享了代码。 fork之后谁先运行,由OS的调度器自主决定。
1、并发和并行
2、时间片 Linux、Windows等民用级别的操作系统,通常都是分时操作系统,它的特点是追求调用任务的公平性。
3、进程等待的本质:连入目标外部设备,CPU不再调度。
4、当内存资源严重不足的时候,操作系统把处于阻塞状态下的进程的代码部分换出到磁盘中,称为阻塞挂起状态,在阻塞状态转为运行状态前再将磁盘中的代码换入到阻塞队列中,换出换入时IO操作,这是用时间换空间的做法。有些场景中不适合操作,当内存资源严重不足时可能会直接杀掉进程。
当有
printf
时是S(休眠)状态,这是因为一个进程的时间片是非常短的,printf
并不是直接往显示器上打印,而是先打印到内存中的输出缓冲区中,速度快时缓存很容易写满,而显示器外设并不是时时刻刻就绪,有printf
就有IO,IO的速度很慢,死循环过程中大部分的时间都在做IO,执行printf
时才是R状态(由操作系统放到运行队列中被CPU调度,这个速度很快)。
等待键盘和等待磁盘是不一样的处理方案,等待键盘(S)可杀掉进程,等待磁盘(D)不可杀掉进程(不常见)。
一般T状态,也是在等待某个条件就绪。
进程被暂停再启动,这个程序就到后台去运行了(S后面没有+),这时候我们无法ctrl c
终止,只能使用kill -9 pid
终止。
可以执行程序后面用空格+& 来让一个进程到后台运行,后面显示进程pid。
打断点的本质是让当前进程暂停。
top
命令修改。
top + r +
要修改进程的pid + 预期修改的值。(系统禁止频繁修改)
修改nice
值的范围为:[-20, 20)
。
为什么nice的修改有范围而不能是任意值呢?分时OS注重进程调度的公平。
调度器要非常均衡地进行进程调度,那优先级的出现不是与其冲突了吗?
1、CPU调度器只会从active队列中选择进程来进行调度 2、调度有三种情况:
这里有个问题,优先级是绝对的吗?也就是优先级越高一定最先调度吗? 如果一个进程优先级在当前运行队列中是最高的,那它被调度运行,时间片到了后重新插入到运行队列中,那它的优先级可能还是最高的那就还是调度它吗?(如果有新进程来了)显然不是,因为这样调度是不公平的。
当一个进程被调度运行,时间片到了后不会继续插入到active队列中,而是插入到active的同胞兄弟expired队列中,如果有新来的进程也是插入到这个队列中,也就是说active队列中的进程只会越来越少,当active队列中的进程都被调度一遍后,只需要交换active和expired两个指针,然后重复这个操作进程调度。
也就是说,优先级只能保证这个进程在一个调度周期内被优先调度。
从上面的图中我们还可以看到有一个大小为5的数组,这个数组大小为什么是5呢?因为整型有32位,5*32=160刚好覆盖140个位置。 通过类似下面的代码来快速确定队列大致位置,一次就可以检索32位:
for (int i = 0; i < 5; i++)
{
if (bit_map[i] == 0)
continue;
else
{
//在32个比特位中详细确定哪一个队列
}
}
上面的代码可以保证检索队列在常数范围,这种调度算法就是Linux内核O(1)调度算法。
所有的进程都要用链表连接,进程既可以在调度队列中,也可以在阻塞队列中…这是怎么实现的呢?
事实上有一个专门计算偏移量的宏offsetof
:
我们知道main函数也是有参数的,只是平时不怎么用,也不清楚其参数的意义是什么。
我们经常使用的指令带选项,就是通过命令行参数实现的。
通过上面的示例,看出命令行参数的意义是什么?
同一个程序,可以根据命令行参数,根据选项的不同,表示出不同的功能。
参数是如何传递的?
我们启动起来的程序(子进程)读到了由shell(父进程)解析的数据。
shell的环境变量:
环境变量相关的命令:
echo
:显示某个环境变量值export
:设置一个新的环境变量env
:显示所有环境变量unset
:清除环境变量set
:显示本地定义的shell变量和环境变量putenv
:设置特定环境变量的值getenv
:获取特定环境变量的值前面我们说过,命令行执行我们自己的程序时需要带./
,表示在当前路径下找要执行的这个程序。而执行系统指令比如pwd
时不需要指定路径,系统默认会到/usr/bin/
路径下去找pwd
,为什么系统知道pwd
在usr/bin/
路径下呢?
因为
PATH
环境变量会告诉shell,应该到哪里去找系统指令。
PATH
中保存的是系统可执行文件的搜索路径集合。
所以如果我们不想带./
就可以执行我们自己的程序,可以有两种方法:
/usr/bin
路径下PATH
集合中。但是这种修改只是临时的,因为这些环境变量只是加载到bash进程中,它是内存级的。
这些环境变量,开始都是在系统的配置文件中,当启动一个shell进程,它就会读取用户和系统相关的环境变量的配置文件,形成自己的环境变量表,这个环境变量表也可以被子进程读取。 所以如果我们想自定义一个环境变量并让它永久有效,就可以修改源头——系统的环境变量配置文件。
当我们登录的时候->系统创建bash进程->读取当前登录用户下的环境变量配置文件->配置它自己的环境变量->将bash自己的路径改为当前用户的路径。
进程能获得自己所在的路径:
通过USER
环境变量,可以让程序识别用户身份,比如可以让某个程序只能指定用户运行:
环境变量可以被所有bash之后的进程全部看到(继承),所以环境变量具有全局属性。
进程具有独立性,但进程间可以通过环境变量进程数据传递(一般是只读数据)。
所谓的进程虚拟地址空间,本质上是一个内核数据结构对象(类似PCB)。
gval
是一个全局变量,在子进程中我们修改gval
的值,在父进程中不修改,在父子进程运行的过程中我们读取gval
的值,可以看到父子进程拥有各自独立的数据,但是取gval
的地址却是发现一致,为什么同一个地址中的gval
却有不同的值呢?
显然这是不可能的,事实上我们取出的这个地址只是虚拟地址,真实的gval
存在不同的物理地址中,而虚拟地址和真实的物理地址之间通过页表来建立映射关系,实现数据管理。
通过上图可以看到,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址。
为什么要有虚拟地址?
本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~