回顾
在上一篇从文件描述符到Socket引入了套接字socket,并说到了socket(),bind(),listen(),和accept()这几个系统调用,那这篇就来说说系统调用。
什么是系统调用 ?
上一篇socket()方法就是一个系统调用,开启了一块缓冲区,生成了一个套接字描述符,那更普遍的系统调用是什么呢?
我们知道当我们的应用想使用硬件时必须通过操作系统,具体来说是系统内核,而不是我们直接操作硬件,一方面操作硬件需要知道的细节太多,二是可能危害到操作系统的运行。所以在应用程序与硬件添加了中间层,这是操作硬件的抽象接口。当需要操作硬件时,应用程序去调用这些系统调用便可,这样做的好处很多:
- 应用程序不用关心硬件的细节,比如保存文件时,你不用考虑是机械硬盘还是固态硬盘。
- 系统保证了系统的稳定与安全。内核可根据用户类型.权限对需要进行的访问进行裁决,防止窃取其他进程信息或者危害操作系统自身
- 保证了多任务多进程。试想下每个进程都独自调用硬件接口,而内核一无所知的话,那没法做到任务调度。 以linux为例子,共有300个左右系统调用,可通过终端man 命令查看,比如我们想看socket() 这个系统调用(作者这是ubuntu 16.04系统)
man 2 socket
命令中的2是系统调用即system call。
内核态与用户态
我们常常听说进入内核态,但是具体是个什么意思?
操作系统为了保证系统稳定,设置了两种运行模式,当运行用户程序时处于用户模式中,例如我们自己实现的计算函数或方法,但是当我们调用了系统调用,就切换成了内核模式或者说进入了内核态。 关于进入内核态有很多相关知识,比如内核空间与用户空间,这个空间是基于内存来说的,操作系统加载后会开辟一段内存作为自己内核空间,为了避免用户程序能直接操作这段地址空间,就使用虚拟地址空间映射的技术,这样了每个应用程序都在一块虚拟的内存地址上操作,根本无法访问真实的内核地址。
系统调用执行过程
上面说到了执行系统调用会切换到内核态,并执行内核代码,那么具体怎么执行到内核代码的呢?
当用户程序调用了一个系统调用,会触发一个软中断,通过引发一个异常来促使系统去切换到内核态去执行异常处理程序。这样内核空间就可以代表应用程序在内核空间触发系统调用。在x86系统上预定义的软中断是中断号128,通过int$0x80触发该中断。这条指令触发一个异常导致系统切换到内核态并执行128号异常处理程序,而这个程序就是系统调用处理程序。 这里是不是很像我们平常java代码里 try catch一段代码,出现了异常则去做相关的处理操作。
socket建立大致流程
那么我们再来看一段伪代码
//创建socket
int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(s, ...)
//监听
listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...);
//将数据打印出来
printf(...)
当accept执行时,会被阻塞,当客户连接了服务端并且发送了数据,网卡向cpu发出一个中断信号,操作系统便能得知有连接到来,再通过网卡中断程序去处理连接。此时进入了内核态,去创建套接字。创建完成后唤醒用户程序返回了int c,即套接字描述符,然后往下执行,此时recv又是个系统调用且是阻塞方法,同样,把用户程序挂起,直到这次网卡数据完全准备好,触发软中断,进入内核态,内核忙着接受数据,接收完毕后copy数据到用户空间,再唤醒用户进程去处理接收到的数据。
此时从socket建立到数据发送大致流程就完成了,后续我们讲开始讲述操作系统IO模型。敬请期待.
喜欢本文的朋友们,欢迎长按下图关注订阅号"我的编程笔记",收看更多精彩内容~~