限制进程:内核态和用户态

进程可分为两种类型。一是操作系统自身运行时的内核类进程,也称为操作系统进程。另一种即非内核类进程,不是操作系统自身的进程,而是想要实现某些功能,用户自己去启动的程序产生的进程,也称为用户类进程或用户进程。

总而言之,就是要限制用户进程
操作系统要具备最高掌控权限,其他用户进程都必须不能具备这些权限
操作系统必须能够在任何有需要的时候暂停某个进程并切换到操作系统内核进程,即拿到CPU的控制权

要让操作系统具备最高权限,并让用户进程不具备最高权限,CPU提供了两种运行模式:内核模式和用户模式

回到操作系统:中断

要想在任何需要的时候回到操作系统,这相当于是改变了CPU的正常执行流程,所以一个非常熟悉的字眼-中断(Interrupt)就出现了。通过中断,可以保证回到操作系统,从而将CPU的控制权交给操作系统

中断分为硬件中断和软件中断
任何硬件,都有自己的IRQ(中断请求),都可以在需要的时候通过总线向CPU发送硬件中断通知
比如时钟中断、IO中断
软件也可以发送中断,比如请求一个系统调用(system call),关于系统调用,后面在介绍

中断时是如何进入到内核态的呢?
其实,要进入内核态只需要执行特殊的指令即可,一般称之为trap指令,当执行trap指令后,就会进入内核态,回到操作系统
在操作系统执行完相关操作后,就要回到用户态,要回到用户态也只需要执行特殊指令即可,一般称之为return-from-trap指令。

操作系统的服务端口:系统调用

用户进程工作在用户态,它是受限的,它要取得结果就只能请求工作在内核态的操作系统帮助完成这些操作,并将操作结果交给用户进程

系统调用就是操作系统提供给用户进程请求操作系统做一些特权操作的接口,即为用户进程提供服务的窗口,在linux下可以通过man syscalls命令查看Linux提供的所有系统调用

系统调用如open()和read()都像是函数。其实它们确实都是函数,只不过是比较特殊由操作系统提供的,一般是由汇编语言编写或掺杂了部分汇编代码,因为它们要和硬件交互

C库包含了所有和系统调用同名的库函数,或者说,这些库函数是对系统调用的封装,执行这些库函数时,会发起封装在其内的同名系统调用的请求。例如,C库有open()函数,它用来打开文件,但是C程序工作在用户模式下,是没有权限打开文件的,于是在执行到open()函数时,open()库函数会发起一个同名的open()系统调用请求操作打开文件
对于非C程序,其实本质还是一样的。比如CPython,也有open()函数,它是对C库函数的再次封装

CPU的归属:Idle进程

操作系统并不总是繁忙。例如个人PC上任务比较轻,多数时候都无法充分利用CPU,当CPU没有任务执行的时候,CPU在干嘛呢?
操作系统提供了一个称为Idle的进程,当CPU没有任务要执行,就去执行该进程,它是CPU空闲下来时“休息”的位置。

Idle进程的工作非常的轻松,就是累积CPU空闲时间,执行Idle进程的时间,就是它的空闲时间。也正因为如此,个人PC机上(像Windows系统)查看到的Idle进程的CPU使用率几乎总是90%以上,它表示CPU的空闲程度。

资源隔离:虚拟内存

操作系统使用了一个称为虚拟内存(Virtual Memory,VM)的概念来实现进程的内存管理,虚拟内存也称为地址空间或虚拟地址空间,和物理内存的称呼相呼应
虚拟内存是操作系统对进程营造的另一个假象:让进程以为自己占有了所有的物理内存。但实际上,它只使用了物理内存的一部分,每个进程所实际使用的那部分物理内存由操作系统进行分配和回收,从而实现内存的管理

虚拟内存的主要目标之一是对进程透明,进程不应该感知到内存被虚拟这个事实,相反,还要让进程以为自己拥有所有的物理内存,相怎么使用就怎么使用,而不应该受到限制。

虚拟内存另一个目标是保护进程,进程A不允许访问到进程B的虚拟内存时,在进程A崩溃时也不应该让它影响到进程B,更不能让用户进程影响到操作系统本身。所以,每个进程都是完全隔离的,互不影响

进程的地址空间布局:分段

Linux的虚拟地址空间采用“分段+分页”结合的方式实现。先看分段,之后再介绍分页。

分段是将内存划分成各个段落(Segment),每个段落的长度可以不同,且虚拟地址空间中未使用的空间不会映射到物理内存中,所以操作系统不会为这段空间分配物理内存。这样的话,内核为刚创建的进程分配的物理内存中,所以操作系统不会为这段空间分配物理内存。这样的话,内核为刚创建的进程分配的物理内存可以很小

虚拟空间分为了如下几个段:
文本段(Text):也称为代码段。进程启动时会将程序的代码加载到物理内存中,文本段映射到这片物理内存。
初始化数据:包含程序显式初始化的全局变量和静态变量,这些数据是在程序真正运行前就已经确定的数据,所以可以提前加载到内存保存好。
未初始化数据(BSS):未初始化的全局变量和静态变量,这些变量的值是在程序真正运行起来并为其赋值后才能确定的,所以程序加载之初,只需要记录它的内存地址和所需大小。出于历史原因,这段空间也称为BSS段。
栈(Stack):是一个可以动态增长和收缩的内存段落,由栈帧(Stack Frames)组成,进程每调用一次函数,都将为该函数分配一个栈帧,栈帧中保存了该函数的局部变量、参数值和返回值。注意,编译器会将函数参数放入寄存器来优化程序,只有寄存器放不下的参数才使用栈帧来保存。
堆(Heap):程序运行时,变量的值以及动态请求分配的内存都在这个内存段落中。
内核段(Kernel):这部分是操作系统内核运行时所占用内存在各进程虚拟地址空间中的映射。所有进程都有,且映射地址相同,因为都映射到内核使用的内存。这段内存只有内核能访问,用户进程无法访问到该段落。

使用分段的好处就是“各段自扫门前雪”,虽然在地址空间中每个分段的地址都是连续的,但实际上,每个分段映射到物理内存地址时是独立的,段与段之间可以不连续。这是因为CPU为每个段都使用一对(即两个)特殊的寄存器:基址寄存器和界限寄存器。

在翻译虚拟地址的时候,(虚拟空间中)每个段落中的虚拟地址加上该段落基址寄存器中的基址值就能转换成物理内存的地址

最后再说明一点,内存地址翻译的任务既可以由操作系统来做,也可以由硬件CPU来做。但如果完全由操作系统来完成,就需要频繁地陷入到内核态,这样效率会非常低。所以,这项任务交给CPU硬件来完成,操作系统只需在必要的时候介入,比如分配内存、回收内存等

栈空间:用户栈和内核栈

每个进程都有两个栈空间:用户栈(User Stack)和内核栈(Kernel Stack)
在进程内存布局中出现的栈段落就是用户栈,它能被用户进程直接访问。而内核栈是存放在内核维护的内存空间中的,即进程内存布局最顶端的Kernel段内部,用户进程无法访问Kernel段,自然也无法访问内核栈。
一般场景下所说的栈,默认指的都是用户栈,只有在谈论到进程底层细节的时候,才会特别指明内核栈。

每当进程调用一次函数,都会在用户栈中为该函数分配一个栈帧(stack frame),也称为调用栈(call stack),当该函数返回时又会释放该栈帧。释放的栈帧不会从虚拟内存中移除,它可以被之后调用的函数重新使用,所以栈空间的大小是不会减小的。

栈帧中保存了传递给该函数的参数、该函数中定义的局部变量、函数的返回值、调用该函数的程序计数器副本,以及一些其它重要信息。
什么是程序计数器?这是CPU中的一个寄存器,在这个寄存器中保存了下一个要执行指令的指针。所以,CPU每执行一个指令的时候,就会设置这个寄存器使它指向下一个指令。

当main()函数调用函数fun()的时候,PC寄存器就已经指向了这个指令,CPU可以将这个指令的指针的值(也就是PC的副本)保存在fun()函数的栈帧中,这样fun()执行完成后就能将这个指针重新放回到CPU的PC寄存器中,使得CPU重新回到main()函数调用fun()的位置处,从而调用者main可以取得函数fun()栈帧中的返回值(这个时候fun()的栈帧被释放),并继续执行下面的代码。

内核栈
操作系统还为每个进程维护另一个栈:内核栈。这个栈的位置在内核的内存区域中,只有内核能够访问,用户进程无法访问。
内核栈的作用是存放上下文切换时的进程信息。
当进程A要切换到进程B时,首先要陷入内核,然后内核将CPU中关于进程A的进程信息(即某些寄存器中的值)保存在进程A的内核栈中,然后内核将CPU中关于进程A的进程信息(即某些寄存器中和的值)保存在进程A的内核栈中,然后从进程B的内核栈中恢复进程B的信息到CPU的某些寄存器中,再退出内核模式回到进程B,这样CPU就开始执行进程B了。

分页和页表

除了分段,空间管理的第二种常见方式是分页。

Linux将虚拟内存划分成固定大小的页(Linux中的页大小是4KB),并且以页作为操作内存的最小单元。例如一次性读取一页,虚拟内存中的页称为虚拟页。对应的,物理内存也会划分成固定大小的页来管理,称为物理页,也常称为页框或页帧。物理页和虚拟页大小相等。

值得注意的是,虽然虚拟内存和物理内存都将空间全部划分成页,但不可能会为所有虚拟页分配好所有对应的物理页,所以虚拟页有一个有效位的属性,如果该页有分配对应的物理页则有效,否则无效。

为了将虚拟页的页号和物理页的页号也对应起来,也需要进行地址翻译。

正是因为页表中的内容太多,如果不采用其他技术,它也将占用巨大的内存(内存中的页表甚至可以大到几百兆,所以必须优化减小它)。有两种常见的思路解决这类问题:分段加分页结合、多级页表。这两种空间管理问题的解决思想,在文件系统中也一样使用了。此外,还有常用的技术是使用虚拟大内存页,内存页变大,所需要保存的映射记录就越少,效率也越高。

通过分段+分页的方式,不再为整个地址空间分配一个页表,而是为每个段落分配一个页表,这样每个页表的大小就减小了,而且段落是独立管理的,那么每个段落中的页表的访问频繁度也将不一样。在文件系统中,分段+分页的思想体现为块组+数据块。但是分段+分页的方式也是有缺点的,它虽然为每个段划分页表,但仍然为所有的内存划分了页,且总页表大小并没有改变。所以,操作系统不使用这种方案。

第二种方案是使用多级页表,也是Linux中使用的方案,它不依赖于分段。它的思想是:如果某页表中包含的所有页表项都是无效的页(例如未分配的),就不为这段空间的页维护页表,这样就能减小页表的大小。这个逻辑其实很简单:对于没有分配的页,没有必要去记录这些页的翻译方式。

由于不是所有页都维护了页表,所以使用一个称为页目录(Page Director)的数据结构去记录所有的页表(通过指向页表的指针记录),并标记每个页表是否有效,页表有效表示该页表已经分配,这也意味着该页表中一定有正在使用中的有效页。因为页目录是页表的更高一层次,所以称为多级页表。

页翻译:快速地址转换

虽然操作系统也能将虚拟页表翻译成内存中对应的页帧,但是它仍然很慢。另一方面,如果访问每个页都需要操作系统来参与帮忙翻译,这会频繁陷入内核,效率是非常低的,所以要再次把任务交给CPU去做。
将底层任务交给硬件提高效率
前文介绍的虚拟地址翻译,以及这里介绍的页翻译,本都可以由操作系统完成,但是操作系统参与太多效率会非常低,这时候都将任务交给操作系统的好伙伴-硬件(CPU)来完成,这会减少大量的上下文切换,因为不用再陷入内核了。
不仅如此,磁盘IO本也是可以由操作系统参与完成的,但速度会更慢,所以也将IO任务交给硬件(硬盘)去完成。此外,还有网卡、显卡等等。
所以做个总结,只要频繁进行底层操作的任务,一般都会交给硬件而绕过操作系统内核,它们的原理都一样。而要交给硬件,硬件肯定要支持这类操作。
CPU对页做快速地址转换,其全称为translation-lookaside buffer(TLB)

OOM和Swap分区
进程的虚拟内存空间是映射到整个物理内存空间的,所以在进程自身看来它拥有了整个物理内存,它也能使用整个物理内存,只需在使用的时候请求操作系统帮忙分配更多空间即可。
其实,早期内存一般都比较小,很容易就出现内存不足的问题,所以很早就提出了一个交换分区的概念。

swap分区是将磁盘当作内存使用,使得虚拟地址空间的范围大小可以超出物理内存的实际大小,现在在内存条件足够大的情况下swap分区最好不用。swap分区使用时容易造成进程抖动。

Shell内置命令、外部命令、别名、函数、保留关键字优先级

内置命令、别名、函数、外部命令

$ alias kill=“echo haha”
$ function kill()(echo hehe)

$ type -a kill
kill is aliased to `echo haha’ # 1.别名kill
kill is a function # 2.函数kill
kill ()
{
( echo hehe )
}
kill is a shell builtin # 3.内置kill
kill is /usr/bin/kill # 4.外部kill

别名、函数、保留关键字、外部命令

$ alias time=“echo haha”
$ function time()(echo hehe)

$ type -a time
time is aliased to `echo haha’ # 1.别名
time is a shell keyword # 2.保留关键字
time is a function # 3.函数
time ()
{
( echo hehe )
}
time is /usr/bin/time # 4.外部命令

按照这里的测试看来,优先级是 别名>保留关键字>函数>内置命令>外部命令
如果想要调用外部命令,则使用command cmd或使用全路径,如果想要调用内置命令,则可以使用builtin cmd

例如:
Code
#调用外部命令time
[root@me ~]$ command time
echo haha
[root@me ~]$ /usr/bin/time
echo haha

调用内置命令printf

[root@me ~]$ alias printf=“echo hehe”
[root@me ~]$ printf
echo hehe
[root@me ~]$ builtin printf ‘hello’
hello

内核态和用户态 用户空间和内核空间

当进程运行在用户空间时就是用户态、运行在内核空间时就是内核态

两个缓冲空间:Kernel buffer和io buffer

标准IO库可以看作是文件描述符的更高层次的封装,提供了比文件描述符操作IO更多的功能。例如,可以在IO对象上指定编码、指定换行符,此外还在用户空间提供了一个标准IO库的缓冲空间,通常可称为stdio buffer或IO buffer,而这些功能在文件描述符上都是没有的。标准IO库既然是高层封装,当然也会提供用户不使用这些功能(比如不使用IO Buffer),而是直接使用文件描述符,那么这时候的文件对象就相当于是文件描述符了,这时候的IO操作模式也就是裸IO模式。

所有从硬件读取或写入到硬件的数据,默认都会经过操作系统维护的这个Kernel Buffer。

例如,cat进程想要读取a.log文件,cat进程是用户空间进程,它自身没有权限打开文件以及读文件数据,它只能通过系统调用的方式陷入内核,请求操作系统帮助读取数据,操作系统读取数据后会将数据放入到page cache(对于普通文件维护的Kernel buffer称为page cache或buffer cache)。然后还要将内核空间page cache中的数据拷贝到用户空间的IO Buffer缓冲空间(因为cat程序的源代码中使用了标准IO库stdio),然后cat进程从自己的IO Buffer中读取数据。这就是整个读数据的过程。

需要注意的是,虽然这两段缓冲空间都在内存中,但仍然有拷贝操作,因为内核的内存空间和用户进程的虚拟内存空间是隔离的,用户空间进程没有权限访问到内核空间的内存,但是内核具有最高权限,允许访问任何内存地址。换句话说,在将Kernel Buffer的数据拷贝到IO Buffer空间的过程中,需要陷入到内核,OS需要掌控CPU。

此外,Linux也提供了所谓的直接IO模式,只需使用一个称为O_DIRECT的标记即可,这时会绕过Kernel Buffer,直接将硬件数据拷贝到用户空间。虽然看上去直接IO少了一个层次的参与感觉性能会更优秀,但实际上并非如此,操作系统为内核缓冲空间做了非常多的优化,使得并不会因此而降低性能。最典型且常见的一个优化是预读功能,它表示在读数据时,会比所请求要读取的数据量多读一点放入到Kernel Buffer,这样在下次读取接下来的一段数据时可以直接从Kernel Buffer中取数据,而无需再和硬件IO交互。所以,使用直接IO模式的场景是非常少的,一般只会在自带了完整缓冲模型的大型软件(比如数据库系统)上可能会使用直接IO模式。

I

/O操作和DMA、RDMA

内核进程帮忙执行IO操作时,由于IO操作相比于CPU来说是极慢的操作,CPU不应该等待在这个过程中,而是切换到其它进程上去执行其它任务。这里再次涉及到一次上下文的切换:从内核态回到用户态的其它进程。
操作系统不执行IO操作,那么谁来执行IO操作呢?在以前,确实是操作系统进程掌控CPU来参与IO操作的,但是现在广泛使用一种称为DMA(Direct Memory Access)的技术,它称为直接内存访问,也就是说可以直接读、写内存,而不再需要操作系统做这样的操作。

除了DMA,还有更高级的RDMA(Remote Direct Memory Access)机制,它需要操作系统和硬件的支持,还需要编写RDMA方式的代码。
RDMA机制下,程序可以直接绕过Kernel Buffer,内核发现是RDMA操作后,直接告诉RDMA硬件将读取的数据(写操作也一样)写入到用户空间的IO Buffer,而不需要先拷贝到Kernel Buffer,再拷贝到IO Buffer。相当于是使用了O_DIRECT标记的直接IO技术。
像这种绕过内核功能的技术,通常称为内核旁路(Kernel Bypass),RDMA技术内核旁路的是一种,还有像TOE也是内核旁路的一种。

进程间通信

单机操作系统上的进程可以分为两类:
1.独立进程:这类进程不会和其它进程有任何交流。
2.协作进程:两个或多个进程之间需要交流
对于单机上不同进程之间的协作,各进程之间需要进行数据的交流,这种行为称为进程间通信,即进程与进程的通信。

进程间的通信方式如下:文件、管道、套接字、Unix域套接字、共享内存、文件映射、消息队列、信号、信号量、锁

管道
每创建一个管道,就有两个文件描述符,一个负责读管道,一个是负责写管道的。所以,使用管道通信时,可以看作是两个文件描述符加一段内核空间中的内存
管道只能协调有亲缘关系的进程通信,所谓亲缘,比如父子进程、兄弟进程。当某进程创建一个管道后,它就拥有了这个管道的两个文件描述符,它的子进程会继承这两个文件描述符,所以子进程也能读写这个管道

套接字
套接字用于协调不同计算机上的进程间通信,也就是基于网络的通信。当然,也可以在本机上使用套接字进行进程间的通信。

套接字通信的方式非常多,有Unix域套接字、TCP套接字、UDP套接字、链路层套接字等等。
创建TCP套接字时会返回这个套接字的文件描述符,可通过这个文件描述符对套接字进行读和写操作。
对比一下,当一个程序需要对一个磁盘文件同时进行读写操作(在命令行下似乎没有找到这种命令,但通过编程方式是很容易实现的)时,由于只通过单个文件描述符同时负责读和写,很可能需要通过不断移动文件指针的方式来改变读写的位置,否则数据很容易错乱。
而TCP套接字也是通过单个文件描述符进行读写套接字的,为了保证读和写的位置不错乱,操作系统在内核空间为每个TCP套接字维护了两个buffer空间,一个buffer用于写、一个buffer用于读。但提供读的buffer空间称为recv buffer,提供写的buffer空间称为send buffer,它们统称为socket buffer。
所以,服务端和客户端通过两个套接字通信就简单了,一端向send buffer写数据,该buffer的数据会通过已经建立好的TCP连接发送到另一端的recv buffer,于是另一端只需从recv buffer 中读数据即可实现不同计算机上的进程间通信
Unix域套接字
Unix域套接字用于本机进程间通信,一般用来实现双向通信的管道。Unix域套接字是比网络套接字轻量级且高效很多,因为它不涉及网络通信等等。
创建Unix域套接字后返回两个文件描述符,这两个文件描述符均对套接字可读、可写,从而实现全双工的双向通信。

同样的,为了避免使用单个文件描述符同时读、写造成的数据错乱,Unix域套接字也有两个buffer空间。

文件映射
文件映射是将磁盘上文件的某段数据映射到内核的一段物理内存上,然后将此物理内存映射到一个或多个进程的虚拟内存中。映射了文件的进程可以直接读、写这段内存来达到读、写磁盘文件的功能。
如果多个进程请求映射的文件区段相同,则只映射一次。
所以,文件映射进行了两次映射,一次映射是将属于磁盘文件的部分或全部数据块映射到物理内存页中,另一个映射是将物理内存页映射到进程的虚拟页上。

既然每个进程都能访问到文件映射的物理内存,那么这段物理内存分配在进程虚拟内存布局的哪一部分呢?这个简单思考下即可得知答案,进程的虚拟内存中,只有堆内存和堆栈中间的未分配地址空间是可以由用户进程自由使用的地址区域,如果将文件映射到堆中,假如映射的文件数据较大,已分配的堆内存中空闲页很可能不足以映射该文件,操作系统需要为其分配更多的堆内存,另一方面,就算当前堆内存能放下文件映射,但也很可能因此而导致空闲页所剩不多,进程之后的运行很可能还是要请求分配新的堆内存,如此看来,倒不如直接映射在堆栈中间那片未分配的地址空间,并且为堆和栈都预留一段未分配空间。图中给出文件映射在进程内存布局中的位置。

共享内存
共享内存是直接从内核维护的内存中划分一片内存,并将该内存映射到一个或多个进程中。
因为可能多个进程映射到同一共享内存,所以某进程对此内存数据的修改会直接影响其他进程,这样就能在进程之间传递消息。但也正因为如此,在使用共享内存时,应当保证没有两个或以上的进程同时修改共享内存数据。
共享内存是效率最高的进程间通信方式,它完全内存化操作,且没有任何内存拷贝行为,此外,内存映射到不同进程之后,操作系统就不再参与该片内存的操作,用户进程可以有权访问这段内存。
内存共享和文件映射非常像,不同之处就在于共享内存没有对具体的磁盘文件进行映射,而是直接映射物理内存到进程中。所以,它也映射在进程堆栈中间的那片未分配内存上。

消息队列
消息队列用于在进程之间传递较小的数据,进程可以向一个或多个消息队列中放入数据,其他进程可以从消息队列中按照各种方式取出消息,从而实现进程间通信。
消息队列是一种生产者消费者模型,生产者生产消息放入队列等待被消费者消费走。如果消息队列已满,生产者被阻塞,如果队列已空,消费者被阻塞。当然,按照不同设计方式,可能会以通知的方式替代阻塞行为。
在分布式系统中,也常使用消息队列模型在多个服务器之间异步地传递消息。

信号
Linux也支持信号机制,它提供了一种软中断机制,可以由用户程序去决定何时产生中断。例如,用户可以在命令行下随时按下Ctrl+C键来终止当前正在运行的前台进程,按下Ctrl+C键的内部其实就是发送了一个称为SIGNAL的信号给前台进程。

Linux中的绝大多数信号都是由内核发送的,所以在发送信号给某进程之前,需要先陷入内核。

在shell中,也支持信号机制,可通过kill命令发送信号给指定进程。可能这里会出现一个疑惑,刚才说信号绝大多数是由内核发送的,为什么kill命令(bash下有两个命令,一个是bash内置kill命令,一个是外置kill命令,但无论如何,都是用户进程)对应的进程能够发送信号给其它进程?这是因为kill发送的信号先是传递给内核的,内核再将这个信号传递给对应进程。所以这里需要进行一次上下文切换。

信号量
在计算机领域中,信号量是一个整数值,如果是正数,表示有多少个信号量,也表示可使用的信号灯数量,信号量的值也可以是0或负数。信号量要结合PV操作才能真正起作用,P是减一个信号灯操作,V是加一个信号灯操作。

信号量有很多种变体,下面简单描述其中一种信号量的规则:
总结起来很简单:如果当前没有信号灯资源(小于或等于0),那么消费信号灯的进程就会被阻塞;如果有信号灯资源(大于0),就直接放行。如果一个进程是来生产信号灯资源的,那么这个进程当然要放行;因为添加了一个信号灯,那么还可以拥有唤醒一个被阻塞进程的能力。
最简单的信号量当然是初始时只使用1个信号灯,从而实现互斥锁(也称为互斥量)机制:P是申请锁操作,只有在有值为1的时候才能申请锁,否则被阻塞;V是释放锁,一直被放行。


一般锁分为两类:共享锁(Shared Lock)和互斥锁(Mutex)。共享锁也称为读锁,通常使用S字母表示;互斥锁也称为排他锁(Exclusive Lock)或写锁,通常使用X字母表示。
此外,使用锁需要考虑锁的粒度,即对多少资源上锁。锁的粒度越大,阻止其它进程的可能性就越大,多进程并发的能力就越差。锁的粒度越小,阻止其它进程的可能性就越小,并发的能力就越强。
但是,锁的粒度太小也不一定好,因为每个锁都是需要额外管理的,粒度越小,需要维护的锁数量越多。比如频繁创建锁和频繁释放锁的开销并不一定小,甚至在极端的时候比维护单个粗粒度的锁效率更低。
下面是shell命令行下flock命令的简单用法,更详细内容可man flock自行探索
bash
#在/tmp/a.lock上申请共享锁(-s),申请成功就运行sleep 10命令
#因为此时/tmp/a.lock上还没有任何锁,所以申请成功
$flock -s /tmp/a.lock sleep 10

#以下代码在终端2上执行
#在/tmp/a.lock上申请互斥锁(-x),申请成功就运行cat /etc/passwd命令
#因为/tmp/a.lock上已经有共享锁,所以阻塞,直到10s后共享锁释放
$ flock -x /tmp/a.lock cat /etc/passwd

程序和进程

程序是由源代码组成的静态文件,里面定义了这个程序要做什么事,实现什么功能。但是它仅仅只是定义了这些要做的事情以及要实现的功能,并没有去做这些事情。只有当程序在操作系统上运行起来之后产生了进程,才会由进程去做这些事情。所以,进程是程序在操作系统上运行之后产生的,是程序动起来之后的一个实例,因此也可以将进程看作是“运行中的程序”。
当开始执行程序后,操作系统负责将程序源代码装载到内存中,使其动起来。
早期一些操作系统,会一次性将程序所有相关代码和数据装载到内存,而现代的操作系统是lazy模式装载,只在程序的执行过程中需要某段代码时,临时去装载。
当代码和静态数据已经装载到内存后,OS还需要为将要运行的程序做一些额外的操作,比如为该进程分配一些内存;创建一些数据结构;初始化与IO相关的一些任务(比如Unix系统中,设置好每个进程都关联的3个文件描述符:stdout、stdin、stderr),等等。
当完成了这些操作后,程序开始执行。因为程序的执行要从main()函数开始,所以需要先跳转到main()函数,然后OS将CPU控制权交给新创建的进程,进程获取到CPU后就可以执行了。

进程表和进程数据结构

内核负责管理维护所有进程,为了管理进程,内核在内核空间维护了一个称为进程表的数据结构,这个数据结构中记录了所有进程,每个进程在数据结构中都称为一个进程表项。

进程表中除了记录了所有进程的PID,还使用一个字段记录了所有进程的指针,指向每个进程控制块(Process Control Block,PCB),请记住PCB这个词,它太重要了。
PCB就代表了一个进程
在Linux中,进程就是一个task_struct数据结构,所以PCB代表的就是task_struct
它代表的是进程的上下文,上下文切换中的上下文就来源于此

既然PCB代表的是进程,在这个数据结构中自然保留了和进程相关的很多信息,至少在进程上下文切换时,能够保存下在CPU中关于当前运行进程的一些重要寄存器信息(所以,PCB代表的是未执行的进程,CPU中某些寄存器中的数据代表当前正在运行的进程)。

PCB包含了进程非常重要的信息,是上下文切换的关键,它保存在每个进程的内核栈中(每个进程都有两个栈空间:用户栈和内核栈)。

操作系统修真秘籍汇总(直达元婴期)相关推荐

  1. 【操作系统】习题汇总

    操作系统相关习题汇总 第一章 概论复习 第三章 进程复习1 第三章 进程复习2 第一章 概论复习 操作系统是对( )进行管理的软件. A. 软件 B. 硬件 C. 计算机资源 D. 应用程序 正确答案 ...

  2. 操作系统面试问题汇总(超详细)

    操作系统的组成 1.驱动程序是最底层的.直接控制和监视各类硬件的部分,它们的职责是隐藏硬件的具体细节,并向其他部分提供一个抽象的.通用的接口. 2.内核是操作系统之最内核部分,通常运行在最高特权级,负 ...

  3. 单代号网络图计算例题_PMP-计算题知识集锦(项目管理考试计算题秘籍汇总PPT)...

    虽然PMP考试中计算机占用的比例并不是特别多,但是计算机也是一种很难的题型,这一次就给大家分享下PMP-计算题知识集锦(项目管理考试计算题秘籍汇总),希望能给你带来帮助! 本文主要分享PMP-计算题知 ...

  4. 想不想修真鸿蒙秘籍,想不想修真门派绝学一览 各门派秘籍汇总

    标签: 炼丹 绝学秘籍可以提升角色修炼速度,想要学习秘籍就必须加入拥有绝学的门派,那么想不想修真哪些门派有秘籍的呢?下面小编就给大家介绍一下. 一星门派,境界要求[筑基-金丹] 逍遥派(绝学) 黄泉派 ...

  5. 操作系统修炼秘籍(1):秘籍简介

    毋庸置疑,操作系统(Operating System,OS)是一个非常大的概念,涉及到的内容非常非常多,在探讨它的时候,往往会将操作系统置于一个比较底层的角度去对待,这也使得多数人对OS是" ...

  6. 【操作系统】选择题汇总大全

    OS选择题汇总 1. 采用分段存储管理的系统中,若地址用24位表示,其中8位表示段内地址,则允许分段的最大个数是( ). A.224 B.216 C.28 D.232 [答案]B 2. 允许多个用户同 ...

  7. 计算机四级操作系统原理知识汇总,2015年全国计算机四级《操作系统原理》考试内容...

    2015年全国计算机四级<操作系统原理>考试内容 一.操作系统概述 1.操作系统基本概念.特征.分类 2.操作系统主要功能 3.操作系统发展演化过程,典型操作系统 4.操作系统结构设计,典 ...

  8. 干货!操作系统基础知识汇总!转给要面试的同学吧

    作者:Guide哥 来源:公众号 JavaGuide 很多读者抱怨计算操作系统的知识点比较繁杂,自己也没有多少耐心去看,但是面试的时候又经常会遇到.所以,我带着我整理好的操作系统的常见问题来啦!这篇文 ...

  9. 第二代机器人操作系统课程资料汇总 Course Learning Materials for ROS2 2019.10.23

    ROS2全部课程资料专栏:https://blog.csdn.net/zhangrelay/article/category/9327597 ROS2开发最新动态资讯:https://blog.csd ...

最新文章

  1. 技术16期:如何更好的保证数据质量【大数据篇】
  2. eclipse的操作
  3. java数组解析_Java - 数组解析
  4. python如何自动缩进_Python缩进
  5. 你见过的MCU最高GPIO翻转频率是多少?
  6. Leetcode--671. 合并二叉树
  7. python基础语法类型_Python基础入门语法和变量类型(一)
  8. ZooKeeper(四) 使用Redis RedissonLock 实现分布式锁
  9. 【codevs1553】互斥的数,二分查找是个好东西
  10. Delphi+GDI
  11. miniUI mini-monthpicker ie8兼容性问题
  12. 眼坐标系和世界坐标系的相互转换
  13. 从java 转到 c# 知识点
  14. 10个常见的Android 新手误区
  15. 今天不聊技术,聊聊如何成为一个靠谱的软件从业人员
  16. 黑苹果键盘对应的相应按键
  17. Opencv-Python-导向滤波快速导向滤波
  18. 基于PHP+MySQL的大学生求职招聘网站
  19. python写的点名器(内附源码)
  20. html输入浮点型,对于input框限定输入值为浮点型的js代码

热门文章

  1. Eclipse 打开鼠标悬停提示功能
  2. 基于实时ETL的日志存储与分析实践
  3. sqlserver语句创建表
  4. impdp、expdp监控数据备份恢复完成进度(EXPDP/IMPDP/RMAN)
  5. 动态规划法——多段图的最短路径
  6. Python数据攻略-Python使用Numpy函数方法
  7. 去除Html换行引起的空格问题
  8. 迷你php框架,PHP 开源框架 MiniFramework 发布 2.0.0 版
  9. android 软键盘工具类,Android开发之弹出软键盘工具类简单示例
  10. kafka接受不到数据