linux中的系统调用

前言:本文只讨论linux中的系统调用,不考虑windows等其他操作系统。

两点:
1.系统调用时,进程调用的是操作系统的内核函数,不是进程。
2.系统调用时,会出现上下文切换,但和进程调度时的上下文切换还是有区别的。

Part1:

早期,内存使用的是哈佛结构,即程序和数据分开存储,这种划分是硬件层面的。到了现代,内存普遍采用的是冯诺依曼结构,即程序和数据存储在一起。但是,在linux中,内存又被按逻辑划分为成了两部分:内核部分和进程部分。内核部分归操作系统内核使用,进程部分归进程使用。

Part2:

百度关于“系统调用”的解释:

操作系统的主要功能是为管理硬件资源和为应用程序开发人员提供良好的环境来使应用程序具有更好的兼容性,为了达到这个目的,内核提供一系列具备预定功能的内核函数,通过一组称为系统调用(system call)的接口呈现给用户。系统调用把应用程序的请求传给内核,调用相应的内核函数完成所需的处理,将处理结果返回给应用程序。
现代的操作系统通常都具有多任务处理的功能,通常靠进程来实现。由于操作系统快速的在每个进程间切换执行,所以一切看起来就会像是同时的。同时这也带来了很多安全问题,例如,一个进程可以轻易的修改进程的内存空间中的数据来使另一个进程异常或达到一些目的,因此操作系统必须保证每一个进程都能安全的执行。这一问题的解决方法是在处理器中加入基址寄存器和界限寄存器。这两个寄存器中的内容用硬件限制了对储存器的存取指令所访问的储存器的地址。这样就可以在系统切换进程时写入这两个寄存器的内容到该进程被分配的地址范围,从而避免恶意软件。
为了防止用户程序修改基址寄存器和界限寄存器中的内容来达到访问其他内存空间的目的,这两个寄存器必须通过一些特殊的指令来访问。通常,处理器设有两种模式:“用户模式”与“内核模式”,通过一个标签位来鉴别当前正处于什么模式。一些诸如修改基址寄存器内容的指令只有在内核模式中可以执行,而处于用户模式的时候硬件会直接跳过这个指令并继续执行下一个。
同样,为了安全问题,一些I/O操作的指令都被限制在只有内核模式可以执行,因此操作系统有必要提供接口来为应用程序提供诸如读取磁盘某位置的数据的接口,这些接口就被称为系统调用。
当操作系统接收到系统调用请求后,会让处理器进入内核模式,从而执行诸如I/O操作,修改基址寄存器内容等指令,而当处理完系统调用内容后,操作系统会让处理器返回用户模式,来执行用户代码。
系统调用在本质上是应用程序请求 OS 内核完成某功能时的一种过程调用,但它是一种特殊的过程调用,它与一般的过程调用有下述几方面的明显差别:
(1) 运行在不同的系统状态。一般的过程调用,其调用程序和被调用程序都运行在相同的状态——系统态或用户态;而系统调用与一般调用的最大区别就在于:调用程序是运行在用户态,而被调用程序是运行在系统态。
(2) 状态的转换通过软中断进入。由于一般的过程调用并不涉及到系统状态的转换,可直接由调用过程转向被调用过程。但在运行系统调用时,由于调用和被调用过程是工作在不同的系统状态,因而不允许由调用过程直接转向被调用过程。通常都是通过软中断机制,先由用户态转换为系统态,经核心分析后,才能转向相应的系统调用处理子程序。
(3) 返回问题。在采用了抢占式(剥夺)调度方式的系统中,在被调用过程执行完后,要对系统中所有要求运行的进程做优先权分析。当调用进程仍具有最高优先级时,才返回到调用进程继续执行;否则,将引起重新调度,以便让优先权最高的进程优先执行。此时,将把调用进程放入就绪队列。
(4) 嵌套调用。像一般过程一样,系统调用也可以嵌套进行,即在一个被调用过程的执行期间,还可以利用系统调用命令去调用另一个系统调用。当然,每个系统对嵌套调用的深度都有一定的限制,例如最大深度为 6。但一般的过程对嵌套的深度则没有什么限制 [1] 。

百度关于“内核函数”的解释:

在 OS 的核心(内核)中都设置了一组用于实现各种系统功能的子程序(过程),并将它们提供给应用程序调用,即内核支持函数。由于这些函数是 OS 系统本身程序模块中的一部分,为了保护操作系统程序不被用户程序破坏,一般都不允许用户程序访问操作系统的程序和数据,所以也不允许应用程序采用一般的过程调用方式来直接调用这些过程,而是向应用程序提供了一系列的系统调用命令(即程序接口),让应用程序通过系统调用去调用所需的内核支持函数的过程。

Part3:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
Part4:

小结:

出于安全考虑,进程的权限被限制,有些操作是无法自己亲自执行,比如IO。但是linux系统提供了一种机制,使得进程可以借助操作系统内核来实现。这种机制就是系统调用。比如,当进程需要读取磁盘上的数据时,便会通过系统调用,由内核函数(不可能是进程!)将数据从磁盘中读取到内核的内存空间中,再将数据从内核的内存空间拷贝到进程的内存空间。
那进程又是怎样实现系统调用呢?答案是通过软中断来实现。软中断时会有上下文切换吗?
前面说在linux中,内存又被分为两部分:内核空间和进程空间。内核空间归操作系统内核使用,进程空间归进程使用。这是一种笼统的说法。事实上,linux内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每个进程会有两个栈:一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。
当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。
当进程因为中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到cpu堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。
那么,我们知道从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,我们是如何知道内核栈的地址的呢?
关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在用户态运行的相关信息,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部删除。因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。(如果不是出于某种设计理念,linux大可不必在创建进程的时候,就为进程在内核空间中开辟内核栈,选择在进程由用户态陷入内核态时在内核空间中为进程开辟内核栈也行。)
综上所述,进程从用户态陷入内核态到从内核态恢复到用户态的过程中,出现了上下文切换。不过,这和发生在进程之间的调度时的上下文切换还是有区别的。值得注意的是,在采用抢先式调度的系统中,当系统调用返回时,要重新进行调度分析是否有更高优先级的进程就绪,这时候就有可能出现进程之间的调度了。

Part5:
IO模型的发展与系统调用
BIO模型
在这里插入图片描述

BIO的问题:线程太多!出现线程太多的根本原因就是阻塞:每增加一个客户端,主线程就clone一个子线程去服务,而且子线程还是采用阻塞的方式,一个子线程只能服务一个客户端。当客户端数量过多时,子线程数量也跟着变多。线程过多又会引发以下问题:
1)消耗内存:线程的栈区是独享的,过多线程会消耗大量内存。
2)消耗cpu:创建线程是通过clone来实现,clone的过程需要系统调用。创建过多线程需要频繁系统调用,导致内核大量占用cpu资源。此外,过多线程频繁切换也会消耗cpu。
解决办法:只有采用非阻塞NIO才能进一步提升性能。
NIO模型
在这里插入图片描述

问题:
因为每询问一个客户端连接fd时,都要发生一次系统调用。当客户端连接太多时,每次循环内,就会出现大量的系统调用,这非常消耗cpu资源。而问题关键在于,只有部分客户端向服务端发送了数据,这就造成了资源浪费。
Select模型
在这里插入图片描述
Select模型的特征:内核增加了一个select的系统调用,该调用有一个参数fds。fds是所有客户端连接,内核收到select系统调用后,只会向程序返回向程序发送了数据的连接,从而过滤掉了无效连接。程序收到select(fds)的返回结果,再次发生系统调用recvfrom,但是和NIO相比,压力大大减轻,因为只需要处理那些发送了数据的连接。整个过程是同步的。Select又称为多路复用器。
问题:fds数据量不能太多
1)select(fds)需要传输fds,当fds数据量比较大时,从用户空间将fds拷贝至内核空间很消耗cpu资源
2)内核在接收到select后,当fds数据量比较大时,需要处理大量数据,内核因此又要大量占用cpu时间,导致cpu被浪费在内核空间。
Epoll模型
在这里插入图片描述
解释:
1)调用epoll_create()在linux内核中建立一个epoll数据结构,这个数据结构包含两个重要的成员:红黑树和链表。
2)调用epoll_ctl将服务端的socket添加进红黑树。
3)调用epoll_wait将链表返回给用户空间的服务端。服务端根据这个结果处理客户端的数据读写请求。
注意:
1)epoll_wait还会将新的客户端(也是socket)连接加入epoll,也就是红黑树。
2)将红黑树中的状态同步到链表是由事件驱动回调函数来完成的。
Epoll高效的原因。
1.共享内存,免去拷贝fd造成的开销。
2.基于事件驱动,不再是轮询。

小结:从BIO->NIO->select->poll->epoll,是一个逐步改进的过程,其驱动力就是来源于尽可能减少系统调用。因为无论是创建线程,还是传递数据,都需要进行系统调用。

Part6:
jvm通过内存的变迁来避免更多的系统调用,进而提高IO效率。如下:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
解释:
方法区和堆一样也是各个线程的共享区域,主要包含了被虚拟机加载的类型变量,常量,静态变量,即时编译后的代码缓存等数据。
永久代和元空间最大的区别在于元空间不再使用虚拟机内存而是使用本地内存,元空间大小只受本地内存限制。参数-XX:MetaspaceSize和-XX:MaxMetaspaceSize可以调整元空间大小。之所以用元空间取代永久代,是因为永久代中存储字符串和数组容易出现性能问题和内存溢出,而且类和方法信息难以确定,要指定永久代大小不容易,太小容易导致永久代溢出,太大容易导致老年代溢出。而且,在永久代进行GC复杂度高,效率低。JDK7的HotSpot已经把原本存放在永久代的字符串常量,静态变量等移除,到JDK1.8直接放弃了永久代的概念,在本地内存中实现了元空间来代替。把JDK7中永久代还剩余的内容全部挪到了元空间。
JDK8 HotSpot JVM 使用本地内存来存储类元数据信息并称之为:元空间(Metaspace);这与Oracle JRockit 和IBM JVM’s很相似。这将是一个好消息:意味着不会再有java.lang.OutOfMemoryError: PermGen问题,也不再需要你进行调优及监控内存空间的使用,但是新特性不能消除类和类加载器导致的内存泄漏。你需要使用不同的方法以及遵守新的命名约定来追踪这些问题。

什么是直接内存?
堆外内存和本地内存含义接近,直接内存和它们还是有区别的,直接内存是位于本地内存中更特殊的一块内存。在内核态期间,调用ByteBuffer.allocateDirect()时,会分配一块直接内存区间(direct memory),jvm和操作系统可以共享该区域,IO时可以减少一次读写操作。
在这里插入图片描述

参考:
1
2
3

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值