1.从计算机CPU与I/O设备的交互方式谈起

计算机CPU与I/O设备的交互方式有最早的程序查询(也叫轮询)方式,发展到后来的程序中断方式,DMA方式等。简单来说,最早的程序查询方式的机制是,CPU若想和I/O设备交互,首先向I/O设备发出命令,查询并读取设备的状态,如果此时设备可用,则设备开始进行准备工作;CPU每隔一段时间便向设备发出命令,以查询并读取设备的当前状态;当设备准备好后,开始进行数据的传输,在传输过程中CPU同样要每隔一段时间就查询设备发送数据的情况,以防止存储I/O交互数据的寄存器(也叫数据端口)溢出导致传输失败。程序查询方式最明显的特点在于:I/O设备无任何自主性,I/O设备的状态转换和数据传输的全过程均由CPU全程干预,CPU必须抽出大量的时间用于定期轮询I/O设备的情况,大大降低了CPU的运算效率。

上图是程序查询方式和程序中断方式的执行示意图。

为什么要让CPU和I/O设备的交互如此频繁呢?换句话说,为什么要让I/O设备毫无任何自主性呢?进一步讲,如果让I/O设备有着初步的自主性,允许I/O设备在准备好以及数据传输完毕后主动通知CPU,从而打断了CPU的执行,令CPU转而服务I该/O设备,这样做就可以大大提高CPU的执行效率,这就是中断方式。在著名计算机入门教程《穿越计算机的迷雾》中,作者是这样讲述中断机制的:“中断的意思是在做一件事情的时候临时打个岔,中途去做另外一件事情,然后再回来。这好比拍一下中央处理器的肩膀,告诉它这里有一件事情需要它过来帮个小忙。在有些计算机原理书上,他们把中断看成你在吃饭,突然电话铃响了,于是你放下碗筷去接电话,然后再坐下来接着吃。”

中断机制的执行具体过程如下:

①关中断,目的是防止其他中断源前来破坏现场;

②保存断点,这是为了保证中断服务程序执行完毕后能正确返回原处;

③引出中断服务程序,将其于内存中的地址送入CPU的程序计数器PC,这本质上就是一个CPU指令系统的特殊寻址过程;寻址中断服务程序的入口内存地址有两种策略:硬件向量法(硬件产生中断向量,中断向量由中断号决定,中断号的概念在下一段有具体解释)和软件查询法(利用软件编程的方式事先规定好);

④保存现场状态;

⑤开中断,这是为了响应更高级的中断请求,实现中断嵌套;

⑥执行中断服务程序;

⑦关中断,这是为了保证恢复现场时不被外界打扰;

⑧恢复原来的现场和屏蔽字;

⑨开中断,中断返回。(中断服务程序的最后一条指令)

其中,①~③由硬件自动完成,该过程也被抽象描述为“中断隐指令”(这只是一个抽象过程,不是真正的指令);其余步骤由中断服务程序完成。

如上所述,中断机制有两个好处:第一个好处,也是最明显的好处——通过赋予I/O设备一定的独立性从而增大CPU执行效率。中断机制的第二个好处是,不同的外设有不同的中断信号,因此它们都被CPU分配了各自不同的中断号,这就意味着计算机内存里可以防止多个不同的程序,而不是像以前那样每次只能有一个,这也意味着中断的种类可以有多种多样,中断机制不仅可以用在CPU与I/O设备交互上,还可用于软件应用程序与操作系统的交互上——系统调用。

2.从中断到系统调用

如上所述,中断分为繁多的类型,因此中断也有不同的分类方法。

最常用的分类方法是“外中断”和“内中断”。该方法可以涵盖所有的中断。

外中断(Interruption,有时直接被称为“中断”)指来自CPU和内存以外的部件引起的中断,如上文所述的I/O设备中断,如用户在键盘上输入命令等。外中断有时直接被称为“中断”。

内中断,又叫“异常”(Exception,这个概念在高级语言编程中经常被提到),则指在CPU和内存内部产生的中断,最简单的例子,如“拔电源”,系统突然断电,CPU确实失去了电能因此无法工作,这是典型的内中断。此外,如地址非法,除数为0,算数操作溢出,内存页面失效,用户程序执行了特权指令等均为内中断。显然,系统调用属于内中断。

此外,还有“硬件中断”和“软件中断”的分类。硬件中断是指外部硬件产生的中断,这显然属于外中断;软件中断指的是,通过编程实现的,通过某条指令产生的中断,显然系统调用属于软件中断,软件中断又属于内中断。

3.从程序接口到系统调用

操作系统为用户和应用程序均提供了对计算机硬件系统的接口。前者为命令接口,后者为程序接口,命令接口,如SHELL,脚本等。程序接口,由一组系统调用命令(也叫广义指令)组成,用户通过在程序中使用这些系统调用命令来请求操作系统为其提供服务。用户在程序中可以直接使用这组系统调用命令向系统提出各种服务要求。如当前流行的图形用户界面GUI,其本质就是利用系统调用。

4.系统调用

系统调用,就是用户在程序中调用操作系统所提供的一些子功能,系统调用可以被看做特殊的公共子程序。系统中的各种共享资源都由操作系统统一掌管,因此在用户程序中,凡是与资源有关的操作,如存储分配,进行I/O传输,及管理文件等,都必须通过系统调用方式向操作系统提出夫区请求,并由操作系统代为完成。同城,一个操作系统提供的系统调用命令有数百条。这些系统调用按功能大致可分为如下几类:

设备管理——完成设备的请求与释放,以及设备启动禁用等功能;如多个进程同时争夺一个声卡;

文件管理——完成文件的创建,读写等;如下载器在下载之前需要用户设定文件存储地址;

进程管理——完成对进程的创建,撤销,阻塞与唤醒等;

进程通信——完成进程之间的消息传递或信号传递等功能;

内存管理——完成内存的分配,回收以及获取作业占用内存区大小和初始地址等;

显然,系统调用运行在系统的核心态,通过系统调用的方式来使用系统功能,可以摆正系统的稳定性和安全性,防止用户随意更改或访问系统的数据或命令。系统调用命令是由操作系统提供的一个或多个子程序模块实现的。系统调用的运行机制为:用户通过操作系统运行上层程序,如系统提供的命令解释程序或用户自编程序,而上层程序的运行依赖于操作系统的底层管理程序提供服务支持,当需要管理程序服务时,系统则通过硬件中断机制进入和心态,运行管理程序;也可能是程序运行出现异常情况,被动地需要管理程序的服务,这时就通过异常处理来进入核心态。当管理程序运行结束时,用户程序需要继续运行,则通过相应的保存的程序现场退出中断处理程序或异常处理程序,返回断点处继续执行。

操作系统从用户态转向核心态的情况有:系统调用——用户程序要求操作系统的服务,发生一次中断,用户程序中产生一个错误状态,用户程序企图执行一条特权指令等。如果程序的运行由用户态转向核心态,会用到访管指令,这是一条在用户态使用的,因此不是特权指令。

上图是系统调用的执行过程。

上图显示了操作系统用户态和内核态之间的关系。系统调用是二者间重要的桥梁,SocketAPI正是这一点的体现。

5.利用gdb跟踪系统调用

上次实验已经跟踪到了Linux内核的start_kernel函数,本次实验继续利用gdb跟踪集成了replyhi的MenuOS系统的Socket相关系统调用。

简述gdb的使用:

首先启动一终端,输入gdb

然后键入file 带路径的你要测试的文件名(因此建议在该文件所处的目录下打开用于gdb的终端)

gdb常见命令:

我们在这里对Socket两个非常常用的系统调用bind()和listen()进行跟踪,具体步骤如下:

打开Ubuntu虚拟机后,进入上次实验的装有MenuOS内核的文件夹,在实验之前,首先要修改上次的~/MenuOS/menu/Makefile文件:

  1. cd ~/MenuOS/menu
  2. sudo su
  3. # 切换至root用户以修改Makefile文件
  4. gedit Makefile
  5. # 去掉-S

然后,和上次一样,打开MenuOS,效果如下:

  1. make rootfs

此时切不可关闭该终端和QEMU,返回到目录../linux-5.0.1下,打开另一个终端,输入如下命令:

  1. gdb
  2. file ./vmlinux
  3. target remote:1234
  4. break __sys_bind
  5. break __sys_listen

此时,gdb已经给__sys_bind和__sys_listen两个Socket系统调用设定了断点,gdb响应如下:

说明gdb已找到这两条系统调用的函数定义所在。

然后开始对MenuOS的运行:

  1. c #在gdb终端输入,接下来两条命令在QEMU中输入
  2. replyhi
  3. hello

效果如上图,gdb已经成功跟踪到系统调用。

而__sys_bind()系统调用究竟做了哪些事情呢?我们根据gdb给我们函数定义地址的信息,在~/linux-5.0.1/net/socket.c中找到了相应的函数定义,如下图:

该函数的讲解如下:

  1. /*
  2. * Bind a name to a socket. Nothing much to do here since it's
  3. * the protocol's responsibility to handle the local address.
  4. *
  5. * We move the socket address to kernel space before we call
  6. * the protocol layer (having also checked the address is ok).
  7. */
  8.  
  9. /*
  10. *上面一段话的大概意思是,bind()系统调用仅负责将进程的名字与socket绑定;
  11. *此外,bind()也负责将该socket转入内核处理;
  12. *至于处理本地地址,这是网络协议所需要做的事情。
  13. */
  14. int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
  15. {
  16. struct socket *sock;
  17. struct sockaddr_storage address;
  18. int err, fput_needed;
  19. /*
  20. *以fd为索引从当前进程的文件描述符表中,找到对应的file实例,
  21. *然后从file实例的private_data中,获取socket实例
  22. */
  23.  
  24. sock = sockfd_lookup_light(fd, &err, &fput_needed);
  25. if (sock) {
  26. /*
  27. * 将用户空间的地址拷贝到内核空间的缓冲区中
  28. */
  29. err = move_addr_to_kernel(umyaddr, addrlen, &address);
  30. if (!err) {
  31. /*
  32. * SELinux相关,不需要关心。
  33. */
  34. err = security_socket_bind(sock,
  35. (struct sockaddr *)&address,
  36. addrlen);
  37. /*
  38. * 如果是TCP套接字,sock->ops指向的是inet_stream_ops,
  39. * sock->ops是在inet_create()函数中初始化,所以bind接口
  40. * 调用的是inet_bind()函数。
  41. */
  42.  
  43. if (!err)
  44. err = sock->ops->bind(sock,
  45. (struct sockaddr *)
  46. &address, addrlen);
  47. }
  48. fput_light(sock->file, fput_needed);
  49. }
  50. return err;
  51. }
  52.  
  53. SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
  54. {
  55. return __sys_bind(fd, umyaddr, addrlen);
  56. }

__sys_listen()的代码如下图所示:

该函数的讲解如下:

  1. /*
  2. * Perform a listen. Basically, we allow the protocol to do anything
  3. * necessary for a listen, and if that works, we mark the socket as
  4. * ready for listening.
  5. */
  6.  
  7. /*
  8. *上述解释的大致含义是,该系统调用的作用是将端口置为监听状态;
  9. *具体操作视协议而定
  10. */
  11.  
  12. int __sys_listen(int fd, int backlog)
  13. {
  14. struct socket *sock;
  15. int err, fput_needed;
  16. int somaxconn;
  17.  
  18. sock = sockfd_lookup_light(fd, &err, &fput_needed);
  19. if (sock) {
  20. /*
  21. * sysctl_somaxconn存储的是服务器监听时,允许每个套接字连接队列长度
  22. * 的最大值,默认值是128
  23. */
  24. somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
  25. /*
  26. * 如果指定的最大连接数超过系统限制,则使用系统当前允许的连接队列
  27. * 中连接的最大数。
  28. */
  29. if ((unsigned int)backlog > somaxconn)
  30. backlog = somaxconn;
  31.  
  32. err = security_socket_listen(sock, backlog);
  33. if (!err)
  34. /*
  35. * 从这里开始,socket以后所用的函数将根据TCP/UDP而视协议而定
  36. */
  37. err = sock->ops->listen(sock, backlog);
  38.  
  39. fput_light(sock->file, fput_needed);
  40. }
  41. return err;
  42. }
  43.  
  44. SYSCALL_DEFINE2(listen, int, fd, int, backlog)
  45. {
  46. return __sys_listen(fd, backlog);
  47. }

而实际上,在进行Linux系统维护与测试时,一般不需要掌握用gdb跟踪系统调用的所有深层代码实现,更重要的是,利用strace命令跟踪一个进程在运行过程中发生了哪些系统调用,结果如何。

strace命令的常用格式为:

strace +带路径的你要检测的文件名

其常用参数为:

  1. -t 在每行输出的前面,显示秒级别的时间
  2. -T 显示每次系统调用所花费的时间
  3. -v 对于某些相关调用,把完整的环境变量,文件stat结构等打出来。
  4. -f 跟踪目标进程,以及目标进程创建的所有子进程
  5. -e 控制要跟踪的事件和跟踪行为,比如指定要跟踪的系统调用名称
  6. -o strace的输出单独写到指定的文件
  7. -s 当系统调用的某个参数是字符串时,最多输出指定长度的内容,默认是32个字节
  8. -p 指定要跟踪的进程pid, 要同时跟踪多个pid, 重复多次-p选项即可。

因为很多情况下不知道你想要跟踪的进程的路径,因此需要得知其进程PID号,需要使用pidof命令,如查询火狐浏览器的PID:

  1. firefox
  2. pidof firefox

结果如下:

然后可以输入如下命令,开始跟踪其系统调用:

  1. strace -tt -T -v -f -e trace=network -o ./firefoxlog.txt -p 30589 -p 30544 -p 30494 -p 30443

然后就可以跟踪到如下内容:(下述内容是我之前跟踪的结果,因此PID号与上文并不一致)

可见,firefox浏览器最常调用的三个系统调用分别为:recvmsg(),sendmsg(),socketpair(),它们分别负责发送/接收套接字的创建,数据包的发送和接收。

对recvmsg(),sendmsg()的深度解析,详见文末的参考链接:

参考链接:

https://github.com/mengning/net/blob/master/doc/systemcall.md

https://blog.csdn.net/weixin_40039738/article/details/81095013

https://blog.csdn.net/u014209688/article/details/71311973

中断与系统调用深度分析(以网络编程接口SocketAPI为例)的更多相关文章

  1. Socket与系统调用深度分析

    学习一下对Socket与系统调用的分析分析 一.介绍 我们都知道高级语言的网络编程最终的实现都是调用了系统的Socket API编程接口,在操作系统提供的socket系统接口之上可以建立不同端口之间的 ...

  2. 5、QT分析之网络编程

    原文地址:http://blog.163.com/net_worm/blog/static/127702419201002842553382/ 首先对Windows下的网络编程总结一下: 如果是服务器 ...

  3. QT分析之网络编程

    原文地址:http://blog.163.com/net_worm/blog/static/127702419201002842553382/ 首先对Windows下的网络编程总结一下: 如果是服务器 ...

  4. Proxy源代码分析——谈谈如何学习Linux网络编程

    Linux是一个可靠性非常高的操作系统,但是所有用过Linux的朋友都会感觉到, Linux和Windows这样的"傻瓜"操作系统(这里丝毫没有贬低Windows的意思,相反这应该 ...

  5. Proxy源代码分析--谈谈如何学习Linux网络编程

    http://blog.csdn.net/cloudtech/article/details/1823531 Linux是一个可靠性非常高的操作系统,但是所有用过Linux的朋友都会感觉到,Linux ...

  6. UNIX网络编程——揭开网络编程常见API的面纱【上】

    Linux网络编程API函数初步剖析 今天我们来分析一下前几篇博文中提到的网络编程中几个核心的API,探究一下当我们调用每个API时,内核中具体做了哪些准备和初始化工作. 1.socket(famil ...

  7. Linux中断的系统调用

    早期UNIX系统的一个特性是:如果在进程执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行.该系统调用返回出错,其errno设置为EINTR.这样处理的理由是:因为一个信号 ...

  8. 揭开网络编程常见API的面纱【上】

    Linux网络编程API函数初步剖析 今天我们来分析一下前几篇博文中提到的网络编程中几个核心的API,探究一下当我们调用每个API时,内核中具体做了哪些准备和初始化工作. 1.socket(famil ...

  9. 浅谈C#网络编程(一)

    阅读目录: 基础 Socket编程 多线程并发 阻塞式同步IO 基础 在现今软件开发中,网络编程是非常重要的一部分,本文简要介绍下网络编程的概念和实践. Socket是一种网络编程接口,它是对传输层T ...

随机推荐

  1. 谷歌学术: but your computer or network may be sending automated queries. To protect our users, we can't process your request right now. See Google Help for more information.

    原因是屏蔽了日本和新加坡的服务器,切换服务器为其他地方即可

  2. 使用BulkLoad恢复hbase数据

    问题: hbase 集群启动不了,maste一直在初始化,数据面临丢失风险. 解决: 把hbfs上 /hbase 目录移走 改名为/hbase-bak 删除zk上的数据,重新建立一个新的hbase集群 ...

  3. Spark内核-任务调度机制

    作者:十一喵先森 链接:https://juejin.im/post/5e1c414fe51d451cad4111d1 来源:掘金 著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. ...

  4. PHPCMS V9.6.0 SQL注入漏洞分析

    0x01 此SQL注入漏洞与metinfo v6.2.0版本以下SQL盲注漏洞个人认为较为相似.且较为有趣,故在此分析并附上exp. 0x02 首先复现漏洞,环境为: PHP:5.4.45 + Apa ...

  5. 使用Attribute限制Action只接受Ajax请求

    原博文 https://www.cnblogs.com/h82258652/p/3939365.html 代码 /// <summary> /// 仅允许Ajax操作 /// </s ...

  6. Hello Python!用 Python 写一个抓取 CSDN 博客文章的简单爬虫

    网络上一提到 Python,总会有一些不知道是黑还是粉的人大喊着:Python 是世界上最好的语言.最近利用业余时间体验了下 Python 语言,并写了个爬虫爬取我 csdn 上关注的几个大神的博客, ...

  7. char*,const char*和string 互转

    1. string转const char* 1 string s = "abc"; 2 const char* c_s = s.c_str(); 2. const char*转st ...

  8. VS Code 自动化连接非固定IP地址EC2实例的解决方案

    问题描述 大家可能和我一样,平时在AWS上启动一台安装有Linux EC2实例作为远程开发机. (注:这里的EC2实例是配置用私钥进行登录的) 通常,你可以选择申请一个Elastic IP绑定到这台开 ...

  9. jpa 主键重复导致查询list的数据总是重复第一条数据

    背境: JPA 读取 Oracle 中的视图,同一条sql, 在数据库 IDE (PLSql)读出 878 条记录并正常显示,代码依然保存了 878 条记录,但所有记录均一样,即数据库中第一条记录. ...

  10. JAVA_基础IO流对象流(三)

    处理流:对象流 ObjectInputStream和OjbectOutputSteam用于存储和读取基本数据类型数据或对象的处理流.可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来. ...