由一道牛客题目想到系统调用和库函数

本文内容概要:
1、Linux结构图;
2、系统调用和库函数概述;
3、基于int的linux的系统调用的具体实现;
4、为什么需要系统调用;
5、系统调用和库函数的关系。


引入:
前段时间在牛客网站上刷题时,看到这样一道题目,当时也是不知道该怎么做。之后去查阅了资料,才知道原来是考查系统调用和库函数,这里先贴出原题。
这里写图片描述
正确答案是C,因为C选项是系统调用,其他选项都是库函数。
下边就来简述系统调用和库函数,以及Linux下系统调用是如何实现的。


1.linux结构图:

这里写图片描述
这张图片来自于以下文章,http://blog.csdn.net/lf_2016/article/details/54587020感谢作者~~


2.系统调用与库函数概述:
系统调用是应用程序(库函数也是应用程序的一部分)与操作系统内核之间的接口(对于windows来讲,它并不是与应用程序的最终接口,API才是最终接口),它决定了应用程序是如何与内核打交道的。无论程序是直接进行系统调用还是通过运行库,则最终还是会达到系统调用这个层面上。(下文将会具体分析)
系统调用既然是应用程序与操作系统之间的接口,那么所有的应用程序都必须依赖于系统调用,所以系统调用必须有严格而又明确的定义,并且具有稳定性和向后兼容性(所谓的向后兼容性就是随着系统的更新,之前旧的系统调用仍然可以使用,只是需要增加新的接口)。
库函数是为了方便人们编写应用程序而引出的。比如C语言写的第一个程序hello world,就是调用库函数中的printf()函数完成输出的。
很多操作系统是以系统调用作为应用程序的最底层的,而windows的最底层接口是windows API。尽管windows的内核提供了数百个系统调用(windows下又把系统调用称为系统服务),但是出于种种原因,windows并没有将系统调用公开,而是在系统调用之上,建立了这样一个API层,让应用程序只能调用API层的函数,而不是如Linux这样的操作系统直接使用系统调用。


3.基于int的linux的系统调用的具体实现:
系统调用是属于操作系统内核的一部分的,必须以某种方式提供给进程让它们去调用。CPU可以在不同的特权级别下运行,而相应的操作系统也有不同的运行级别,用户态和内核态。
首先,我们得知道用户态和内核态的区别,参考文(http://blog.csdn.net/xieyutian1990/article/details/38413413)。


   内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态,因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;反之,当程序运行在0级特权级上时,就可以称之为运行在内核态。运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态。
 这两种状态的主要差别是: 处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的 ; 而处于核心态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。

运行在内核态的进程可以毫无限制的访问各种资源,而在用户态下的用户进程的各种操作都有着限制,比如不能随意的访问内存、不能开闭中断以及切换运行的特权级别。显然,属于内核的系统调用一定是运行在内核态下,但是如何切换到内核态呢?
操作系统一般是通过中断从用户态切换到内核态。
学习过计算机组成原理这类课程之后,我们都知道中断是有两个属性,一个是中断号(从0开始编号),一个是中断处理程序。通常,中断是有两种类型,一种是硬件中断(一般来源于硬件的异常或者其他事件的发生,比如键盘被按下),一种是软件中断(通常是一条指令,i386下是int指令),通常有一个参数,记录中断号。在i386下,windows的绝大多数的系统调用都是由int 0x2e来触发,而Linux则是使用int 0x80来触发所有的系统调用。
在Linux内核版本2.6.19版本一共提供了319个系统调用。EAX寄存器是用来存储系统调用的接口号。
EAX = 1,表示退出进程exit;EAX=2,表示创建进程fork,等等。
Linux的系统中断流程:

这里写图片描述

细节实现:
1>触发中断:
Linux下的系统调用是通过 宏函数来定义的。比如_syscall0是用于定义一个没有参数的系统调用的封装,定义在include /asm-i386/unistd.h中,总共有 7个宏函数。
Linux下支持的系统调用的参数至多有6个,用6个寄存器来传递,分别是EBX,ECX,EDX,ESI,EDI,EBP。使用的时候是依次使用,也就是说系统调用有一个参数的时候,使用寄存器EBX,而不是其他。
以上是系统调用的一种方式,那么另一种方式是什么呢?应用程序调用C库函数,库函数底层调用的是系统调用,就如下图所示(引自http://blog.csdn.net/skyflying2012/article/details/10044343
这里写图片描述

2>切换堆栈:
在学习C语言的中后期,我们大概掌握了一些基本知识,就开始想:为什么点击运行按钮程序就会跑起来?编译器在背后都干了哪些事,函数调用是怎么实现的等等问题。我们知道,函数的调用其实也就是利用中断的,借助于堆栈来保存函数的当前信息,当调用函数执行完成之后,就会继续执行当前函数。同样,系统调用也是有自己的调用堆栈,内核态有内核栈,用户态有用户栈,当要触发系统调用的时候,程序的执行流是从用户态到内核态,这时,程序的当前栈也是从用户栈切换到内核栈,当系统调用执行完成之后,又会从内核栈返回到用户栈。
总结堆栈的切换:
当系统调用的中断被触发的时候,CPU会切换到内核态,找到当前进程的内核栈,并在当前进程的内核栈中写入用户栈的信息,包括,SS(当前栈所在的页),ESP等信息;当系统调用执行完成之后,CPU会调用iret指令切换到用户态 ,iret指令会从内核栈中读取用户栈的信息,进行返回。

3>中断处理程序:
从上边的一个图(Linux系统中断流程),我们可以看出,当触发中断之后,系统会查询中断向量表,得知,0x80触发的是系统调用,然后从EAX寄存器中得到系统调用编号,去执行相应的系统调用。这里也有点类似于函数的调用过程。

4.为什么需要系统调用?
(1)系统调用可以为用户控件提供访问硬件资源的统一接口,以至于应用程序不必去关注具体的硬件访问的操作。比如:fopen函数,使用的时候用户就不需要去管底层的寻道找道得到原理等等。再说,就是计算机的硬件资源是有限的,不能做到多个进程同时访问硬件资源,所以需要系统调用来控制。
(2)可以对系统进行保护,保证系统的稳定和安全。也就是说,用户访问内核的路径事先是已经被规定好的。

5.系统调用和C库函数的关系:
并不是一一对应,几个库函数对应一个系统调用(malloc()和free()对应的是brk系统调用)。也有一个库函数对应一个系统调用(open()函数对应open系统调用)。也有些库函数不需要系统调用(比如strcpy函数,atoi函数)。

【总结 】
(1)系统调用是应用程序与操作系统内核的接口,但是不是最终接口。windows下API才是最终接口,因为windows下的系统调用不公开。
(2)系统调用是通过中断来触发的,Linux下是0x80来触发。
(3)系统调用最多可以有6个参数,放在6个寄存器EBX,ECX,EDX,ESI,EDI,EBP中,但是系统调用号是放在EAX中。
(4)触发系统调用的时候,CPU会从用户态切换到内核态,程序的当前栈也会从用户栈切换到内核栈,系统调用执行完成之后执行相反的操作。
(5)操作系统包括内核和其他程序,内核中包括进程管理,进程调度等等,其他程序包括函数库等等。所以我们可以认为内核约等于操作系统。


参考书籍:
《程序员的自我修养—-链接、装载、库》俞甲子 石凡 潘爱民 著

本人小白,如有问题,请不吝指出~~如果感兴趣的读者,可以自行去阅读《程序员的自我修养》一书的第12章节。

题目要求:给定一个二叉树和一个整数target,找出所有从根节点到叶子节点路径之和等于target的路径。 解思路:可以使用深度优先搜索(DFS)的方法来解决该问。首先定义一个辅助函数来进行递归搜索,该辅助函数的参数包括当前节点、当前路径、当前路径的和以及目标和。在搜索过程中,需要维护一个数组来保存当前节点到根节点的路径。搜索过程如下: 1. 如果当前节点为空,则返回。 2. 将当前节点的值添加到当前路径中。 3. 将当前节点的值累加到当前路径的和中。 4. 如果当前节点是叶子节点,且当前路径的和等于目标和,则将当前路径添加到结果中。 5. 递归地搜索当前节点的左子树和右子树,并传递更新后的当前路径和当前路径的和。 最后,在主函数中调用辅助函数,并返回结果即可。 以下是题目的完整代码实现: ```python class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def pathSum(root, target): def dfs(node, path, path_sum, target, res): if not node: return path.append(node.val) path_sum += node.val if not node.left and not node.right: # 当前节点是叶子节点 if path_sum == target: res.append(path[:]) # 注意需要复制一份path,否则会出现问 dfs(node.left, path, path_sum, target, res) dfs(node.right, path, path_sum, target, res) path.pop() # 回溯到父节点,去掉当前节点 path_sum -= node.val res = [] dfs(root, [], 0, target, res) return res ``` 这样就能找出所有满足路径和等于目标和的路径了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值