0 00:00:00,000 --> 00:00:07,240 1 00:00:07,480 --> 00:00:13,000 接下来我们讨论系统调用 2 00:00:13,040 --> 00:00:17,160 系统调用是操作系统对上提供服务的接口 3 00:00:17,200 --> 00:00:19,480 它是在怎么实现的 4 00:00:19,520 --> 00:00:22,160 然后我们在这里会说 5 00:00:22,200 --> 00:00:24,880 你提供的功能可以通过函数调用 6 00:00:24,920 --> 00:00:26,280 也可以通过系统调用 7 00:00:26,320 --> 00:00:28,320 这两者之间到底有什么区别 8 00:00:28,360 --> 00:00:31,160 然后他们在实现的时候开销是什么样子 9 00:00:31,200 --> 00:00:33,760 我们在这一部分来进行讨论 10 00:00:33,800 --> 00:00:37,680 首先我们看一个标准的函数调用 11 00:00:37,720 --> 00:00:39,080 比如说我想在这里的 12 00:00:39,120 --> 00:00:41,280 我的应用程序这个大家前面都写过 13 00:00:41,320 --> 00:00:46,760 我有个printf我想在屏幕上输出一份信息 14 00:00:46,800 --> 00:00:50,120 我在这里有一个printf就可以了 15 00:00:50,160 --> 00:00:52,000 背后实际上他怎么做 16 00:00:52,040 --> 00:00:54,880 你写程序设计的时候 17 00:00:54,920 --> 00:00:57,880 看到情况是说他有一个标准C库 18 00:00:57,920 --> 00:01:02,520 C库里提供了printf底下就是printf 19 00:01:02,560 --> 00:01:04,560 来实现功能给你用就行了 20 00:01:04,600 --> 00:01:09,320 但是他背后实际他是转成了操作系统的write 21 00:01:09,360 --> 00:01:11,640 系统调用 然后write系统调用 22 00:01:11,680 --> 00:01:13,280 它的实现是在内核里的 23 00:01:13,320 --> 00:01:15,960 根据你write里头参数不同 24 00:01:16,000 --> 00:01:19,280 会把你print输出的文件 25 00:01:19,320 --> 00:01:21,120 可能输出的屏幕 26 00:01:21,160 --> 00:01:24,000 那把这个图再转换一种形式来看 27 00:01:24,040 --> 00:01:26,720 我们更关心系统调用这个接口的话 28 00:01:26,760 --> 00:01:28,760 你会看到跟这个很相似 29 00:01:28,800 --> 00:01:31,880 但是这是说这是我关心的 上面是应用程序 30 00:01:31,920 --> 00:01:33,760 把我们传统说的应用程序 31 00:01:33,800 --> 00:01:37,800 和你那个系统库都算到这个上面去了 32 00:01:37,840 --> 00:01:40,080 这是我看到系统调用接口 33 00:01:40,120 --> 00:01:41,680 在这个系统调用接口里头 34 00:01:41,720 --> 00:01:44,280 进到内核里头去 由于write 35 00:01:44,320 --> 00:01:46,040 你这个系统调用的编号的不同 36 00:01:46,080 --> 00:01:49,360 我在这选择不同的系统调用编号 37 00:01:49,400 --> 00:01:53,200 这个编号导致我write里头采用它的实现 38 00:01:53,240 --> 00:01:56,040 最后给出结果来 返回回去 39 00:01:56,080 --> 00:01:58,120 那我在屏幕上或者文件里 40 00:01:58,160 --> 00:02:01,200 就写出了我的printf内容了 41 00:02:01,240 --> 00:02:04,360 这是我们说函数调用 42 00:02:04,400 --> 00:02:05,880 那么对于系统调用来说 43 00:02:05,920 --> 00:02:10,440 实际上是说他在底下提供操作系统服务的接口 44 00:02:10,480 --> 00:02:12,880 通常情况下 我们是用C 45 00:02:12,920 --> 00:02:17,800 或者C++高级语言来使用这些的 46 00:02:17,840 --> 00:02:19,320 而你在写程序的时候 47 00:02:19,360 --> 00:02:22,000 通常并不直接去使用系统调用 48 00:02:22,040 --> 00:02:25,920 而把系统调用封装到一个库里头 49 00:02:25,960 --> 00:02:27,920 比如像我们标准C库 50 00:02:27,960 --> 00:02:31,960 应用程序是访问这些库里的库函数来实现的 51 00:02:32,000 --> 00:02:34,600 实际上接见访问系统调用 52 00:02:34,640 --> 00:02:38,440 下面这几个是我们常见的应用编程接口 53 00:02:38,480 --> 00:02:41,360 也就相当于我们系统调用最后封装完了之后 54 00:02:41,400 --> 00:02:43,400 用户用到接口是什么样的 55 00:02:43,440 --> 00:02:45,160 在不同的系统里面是不一样 56 00:02:45,200 --> 00:02:49,680 比如说在Windows它有一个Win32 API 57 00:02:49,720 --> 00:02:55,600 是用Windows操作系统内核的服务来实现Win32 库 58 00:02:55,640 --> 00:02:57,920 那在这个库里用户来使用 59 00:02:57,960 --> 00:03:03,560 而在Unix Linux这一类系列的系列有一个POSIX接口 60 00:03:03,600 --> 00:03:09,040 这个POSIX接口提供了用户需要用到各种各样的库 61 00:03:09,080 --> 00:03:13,920 而这个POSIX接口底下会访问系统调用接口 62 00:03:13,960 --> 00:03:15,960 来实现它的服务 63 00:03:16,000 --> 00:03:18,040 而对于Java虚拟机来说 64 00:03:18,080 --> 00:03:20,080 实际他上面有一个Java API 65 00:03:20,120 --> 00:03:23,120 让虚拟机里的应用程序间接的 66 00:03:23,160 --> 00:03:27,800 会转到我的系统调用接口上来 67 00:03:27,840 --> 00:03:29,520 这是外界使用的情况 68 00:03:29,560 --> 00:03:31,480 他内部实现是什么样 69 00:03:31,520 --> 00:03:35,040 每一个系统调用都有一个系统调用编号 70 00:03:35,080 --> 00:03:37,600 然后依据这个编号不同 71 00:03:37,640 --> 00:03:38,760 我们来使用不同的功能 72 00:03:38,800 --> 00:03:44,000 在这张图我们可以看到这是系统调用进来的入口 73 00:03:44,040 --> 00:03:47,200 从这通过软中断进到系统内核里来 74 00:03:47,240 --> 00:03:49,120 他首先体现为一个中断 75 00:03:49,160 --> 00:03:51,160 中断看是系统调用软中断 76 00:03:51,200 --> 00:03:53,320 这个时候就转到这个地方来 77 00:03:53,360 --> 00:03:56,080 这个地方你那里系统调用的编号 78 00:03:56,120 --> 00:03:56,840 在这编号 79 00:03:56,880 --> 00:03:59,400 在这是体现为一个功能编号 80 00:03:59,440 --> 00:04:03,360 功能编号不同我会选取不同系统调用实现 81 00:04:03,400 --> 00:04:05,840 OK 得到它的结果之后返回回来 82 00:04:05,880 --> 00:04:11,400 这是对外提供的系统调用编号 83 00:04:11,440 --> 00:04:12,440 从用户的角度讲 84 00:04:12,480 --> 00:04:15,520 我们关心只是从这进 这个地方出来 85 00:04:15,560 --> 00:04:18,760 其他部分我不关心了 86 00:04:18,800 --> 00:04:21,720 你不关心的底下内容 87 00:04:21,760 --> 00:04:23,840 但是在这里我们需要关心一个事情 88 00:04:23,880 --> 00:04:26,520 就是我在使用系统调用接口的时候 89 00:04:26,560 --> 00:04:30,120 我需要把我需要的服务告诉内核 90 00:04:30,160 --> 00:04:32,400 这是我要准备参数的地方 91 00:04:32,440 --> 00:04:34,320 你把这信息准备好之后 92 00:04:34,360 --> 00:04:37,440 我就可以系统调用在内部实现 93 00:04:37,480 --> 00:04:39,320 就可以知道我如何做处理 94 00:04:39,360 --> 00:04:41,000 处理完之后把结果返给你 95 00:04:41,040 --> 00:04:42,280 而我们通常在这用的时候 96 00:04:42,320 --> 00:04:45,280 这部分又封装到函数库 97 00:04:45,320 --> 00:04:50,080 而函数库我们在上面用就不用关心了 98 00:04:50,120 --> 00:04:55,400 接下来我们说系统调用和函数调用的不同 99 00:04:55,440 --> 00:04:58,120 那对于所使用的指令来讲 100 00:04:58,160 --> 00:05:02,560 系统调用使用的是int和iret 101 00:05:02,600 --> 00:05:06,280 而函数调用使用的是call ret 102 00:05:06,320 --> 00:05:10,760 这四条指令 他是在指令级是完全不同的 103 00:05:10,800 --> 00:05:13,120 那么他们的区别体现在什么地方 104 00:05:13,160 --> 00:05:16,160 实际功能区别在于函数调用 105 00:05:16,200 --> 00:05:19,040 我们知道你在程序的设计里已经学到过了 106 00:05:19,080 --> 00:05:21,000 我为了调用一个函数 107 00:05:21,040 --> 00:05:23,320 我需要把参数压到堆栈里头去 108 00:05:23,360 --> 00:05:26,160 然后转到相应函数去执行 109 00:05:26,200 --> 00:05:29,800 执行时候从堆栈获取我的参数信息执行 110 00:05:29,840 --> 00:05:31,920 返回的结果放在那里在返回回来 111 00:05:31,960 --> 00:05:33,880 这样的话你在上面的函数调用 112 00:05:33,920 --> 00:05:37,200 就知道我相关的返回的结果 113 00:05:37,240 --> 00:05:39,640 然后用这个结果继续往下执行 114 00:05:39,680 --> 00:05:41,880 而对于系统调用来讲 115 00:05:41,920 --> 00:05:45,360 他由于内核是受保护的 116 00:05:45,400 --> 00:05:49,760 而应用程序是他自己的区域 117 00:05:49,800 --> 00:05:52,280 在这为了保护内核的实现 118 00:05:52,320 --> 00:05:54,560 这个地方内核和用户态的 119 00:05:54,600 --> 00:05:56,720 应用程序之间使用不同的堆栈 120 00:05:56,760 --> 00:05:59,720 所以在这会有一个堆栈的切换 121 00:05:59,760 --> 00:06:02,320 切换之后由于处于内核态 122 00:06:02,360 --> 00:06:04,600 我就可以使用特权指令 123 00:06:04,640 --> 00:06:07,120 这些特权指令所导致的结果 124 00:06:07,160 --> 00:06:11,000 就是我这个时候可以直接对设备进行控制 125 00:06:11,040 --> 00:06:14,120 而你在用户态是不可能进行的 126 00:06:14,160 --> 00:06:17,080 这就好比说我们在银行里头 127 00:06:17,120 --> 00:06:20,800 你可以告诉银行的营业员 128 00:06:20,840 --> 00:06:25,680 我需要从我的某一个帐号里取多少钱 129 00:06:25,720 --> 00:06:28,000 这个取钱操作到银行的内部 130 00:06:28,040 --> 00:06:31,680 他的营业员是可以去直接打开保险柜 131 00:06:31,720 --> 00:06:33,720 打开保险柜取出你所需要的钱 132 00:06:33,760 --> 00:06:36,800 并且在你帐号上做相应的记录 133 00:06:36,840 --> 00:06:39,800 这些记录就好比说你记到你堆栈一样的 134 00:06:39,840 --> 00:06:42,840 他记到他自己内部的堆栈上 135 00:06:42,880 --> 00:06:44,240 以至于说如果说 136 00:06:44,280 --> 00:06:47,080 我们两用同一个堆栈会有什么问题 137 00:06:47,120 --> 00:06:48,240 我们两用同一个堆栈 138 00:06:48,280 --> 00:06:50,960 用户其他代码可以改你堆栈的信息 139 00:06:51,000 --> 00:06:53,720 这对于系统来说是不安全 140 00:06:53,760 --> 00:06:55,640 好比说你从里面取了钱 141 00:06:55,680 --> 00:06:58,960 结果你帐号上的金额并未减少 142 00:06:59,000 --> 00:07:00,760 这银行不干或者反过来说 143 00:07:00,800 --> 00:07:03,040 你把钱存到银行里去了 144 00:07:03,080 --> 00:07:05,600 但是银行帐号上钱并没有增加 145 00:07:05,640 --> 00:07:07,440 这个时候你是不干 146 00:07:07,480 --> 00:07:10,960 基于这种理由我这会有相应切换 147 00:07:11,000 --> 00:07:13,360 银行可以在里进行特权的操作 148 00:07:13,400 --> 00:07:18,440 当然我们在这里只是对int iret call ret 149 00:07:18,480 --> 00:07:22,240 这几条指令的最主要的区别做一个介绍 150 00:07:22,280 --> 00:07:27,080 如果说大家在实现新的系统调用的时候 151 00:07:27,120 --> 00:07:29,600 你可能需要去查详细的区别 152 00:07:29,640 --> 00:07:36,520 这是对X86来讲 它的CPU指令手册 153 00:07:36,560 --> 00:07:38,680 那在手册里头有相应链接 154 00:07:38,720 --> 00:07:43,400 因特尔在不停更新它的手册 155 00:07:43,440 --> 00:07:46,320 你会看到在这里不同的CPU上 156 00:07:46,360 --> 00:07:48,040 这些指令它的实现 157 00:07:48,080 --> 00:07:51,000 有什么样的一致性和相应的区别 158 00:07:51,040 --> 00:07:54,800 你要想在特定平台上正确的运行 159 00:07:54,840 --> 00:08:01,440 最后需要准确了解它的行为 160 00:08:01,480 --> 00:08:03,120 最后我们还有一个问题需要来讨论 161 00:08:03,160 --> 00:08:05,640 就是我在什么时候用中断 162 00:08:05,680 --> 00:08:10,240 用系统调用 什么时候用函数调用 163 00:08:10,280 --> 00:08:13,040 这个时候我们说系统调用 164 00:08:13,080 --> 00:08:16,600 会比你函数调用更安全 165 00:08:16,640 --> 00:08:19,840 但是它也有它的问题 它的开销会比你大 166 00:08:19,880 --> 00:08:22,400 原因是我有一个用户态到内核态的切换 167 00:08:22,440 --> 00:08:25,800 具体有哪些开销 我们在这列出来 168 00:08:25,840 --> 00:08:28,720 首先你有一个切换的引导 169 00:08:28,760 --> 00:08:30,960 这是硬件上需要做的事情 170 00:08:31,000 --> 00:08:34,560 再有一个是你在内核里有另外一个堆栈 171 00:08:34,600 --> 00:08:36,360 如果说第一次调用的时候 172 00:08:36,400 --> 00:08:39,040 这个时候会有内核堆栈的建立 173 00:08:39,080 --> 00:08:41,160 然后我在这里传参数的时候 174 00:08:41,200 --> 00:08:46,840 这个参数的有效性合法性是需要做验证的 175 00:08:46,880 --> 00:08:48,480 切换到内核执行的时候 176 00:08:48,520 --> 00:08:51,560 由于我访问的代码有切换 177 00:08:51,600 --> 00:08:54,040 那么在这种情况下 178 00:08:54,080 --> 00:08:57,000 内核需要访问到用户态的一些信息 179 00:08:57,040 --> 00:09:00,080 这个时候会把做一个地址空间上的映射 180 00:09:00,120 --> 00:09:03,600 这些映射会导致你的缓存会有变化 181 00:09:03,640 --> 00:09:07,600 这个时候你的TLB的内容也会有失效 182 00:09:07,640 --> 00:09:11,240 所有这些都会导致用户态和内核态切换的时候 183 00:09:11,280 --> 00:09:14,800 你的系统调用开销是大于函数调用的 184 00:09:14,840 --> 00:09:14,880 185 00:09:14,920 --> 00:09:14,960