实验五:Linux操作系统是如何工作的?
学号:SA1****369
操作系统工作的基础:存储程序计算机、堆栈(函数调用堆栈)机制和中断机制
首先要整明白的一个问题是什么是存储程序计算机?其实存储程序计算机正是冯.诺依曼最初提出的计算机体系模型,现在我们一提到冯.诺依曼体系结构首先会想到的应该是包含输入设备、输出设备、存储器、控制器、运算器组成的经典模型,但是这里我们强调的存储程序计算机的主要特征实际上并非如此,存储程序计算机的概念相当于是褪去现代计算机华丽的外衣,所呈现出来的实实在在的计算机的本质。主要由处理器和存储器组成,中间以总线相连。
CPU通过总线从存储器中读取指令和数据进行处理,采用的主要机制是函数调用推栈机制。在不发生中断、异常以及系统调用的过程中,每个进程的执行都符合函数调用堆栈机制。
接下来说说堆栈机制。具体的函数调用堆栈机制,在我之前的博文中已经详细阐述,具体见实验一 计算机是怎样工作的 SA*****369 张*铭 。
PS:另外也可参阅C函数调用机制(x86的linux环境下)
中断机制是现代操作系统的特征所在,即这是区分操作系统是否为现代操作系统的一个重要依据,也是现代操作系统能够实现多任务的一个重要因素。我们可以将硬件中断(即外围设备产生的中断)与软中断(即系统调用)共同称为中断。CPU在运行一个进程的过程中,接受到一个中断信号,则CPU会停下正在执行的任务,转而去处理相应的中断服务例程。每个不同的中断都对应着一个相应的中断服务处理程序。
简述操作系统(内核)是如何工作
参考材料:
在接受到UDP包后,有时候我们需要根据所接收到得UDP包,获取它的路由目的IP地址和头标识目的地址。
在setsockopt中设置IP_PKTINFO,然后通过recvmsg来获取struct in_pktinfo(struct in_pktinfo是struct msghdr中msg_control的成员).in_pktinfo 结构体(如下所示),我们可以从in_pktinfo中获取路由目的地址(destination address of the packet)、头标识目的地址(source address of the packet)。这种方法只能用于UDP(数据报)传输中。
struct in_pktinfo { unsigned int ipi_ifindex; /* 接口索引 */ struct in_addr ipi_spec_dst; /* 路由目的地址 */ struct in_addr ipi_addr; /* 头标识目的地址 */ };
ipi_ifindex指的是接收包的接口的唯一索引,ipi_spec_dst指的是路由表记录中的目的地址,而ipi_addr 指的是包头中的目的地址。如果给 setsockopt传递了IP_PKTINFO,那么外发的包会通过在ipi_ifindex中指定的接口发送出去,同时把ipi_spec_dst设置为目的地址。
// sock 使用AF_INET协议族, socket类型SOCK_DGRAM setsockopt(sock, IPPROTO_IP, IP_PKTINFO, &opt, sizeof(opt)); // 这里,控制数据是脏数据。 char cmbuf[0x100]; // 目标IP地址 struct sockaddr_in peeraddr; //如果你想要获取UDP包中的数据,那么还需要为msg_iovec字段初始化 struct msghdr mh = { .msg_name = &peeraddr, .msg_namelen = sizeof(peeraddr), .msg_control = cmbuf, .msg_controllen = sizeof(cmbuf), }; recvmsg(sock, &mh, 0); struct cmsghdr *cmsg ; for ( // 遍历所有的控制头(the control headers) cmsg = CMSG_FIRSTHDR(&mh); cmsg != NULL; cmsg = CMSG_NXTHDR(&mh, cmsg)) { // 忽略我们不需要的控制头(the control headers) if (cmsg->cmsg_level != IPPROTO_IP || cmsg->cmsg_type != IP_PKTINFO) { continue; } struct in_pktinfo *pi = CMSG_DATA(cmsg); // 在这里, peeraddr是本机的地址(the source sockaddr) // pi->ipi_spec_dst 是UDP包中路由目的地址(the destination in_addr) // pi->ipi_addr 是UDP包中的头标识目的地址(the receiving interface in_addr) }
#include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #define BUFLEN 255 int main ( int argc, char **argv ) { struct sockaddr_in peeraddr, localaddr; int sockfd; int socklen, n; //(1)创建UDP数据报socket描述符 sockfd = socket ( AF_INET, SOCK_DGRAM, 0 ); if ( sockfd<0 ) { printf ( "socket creating err in udptalk\n" ); exit ( EXIT_FAILURE ); } printf ( "IP address Checking!\n" ); socklen = sizeof ( struct sockaddr_in ); memset ( &peeraddr, 0, socklen ); //(2)设置目标主机IP和端口,这里我们使用广播方式 peeraddr.sin_family=AF_INET; peeraddr.sin_port=htons ( atoi ( "8903" ) ); peeraddr.sin_addr.s_addr = htonl ( INADDR_BROADCAST ); //(3设置本机IP和端口,这里我们设置可以接收符合端口的所有的包 memset ( &localaddr, 0, socklen ); localaddr.sin_family=AF_INET; localaddr.sin_addr.s_addr = htonl ( INADDR_ANY ); //设置接收任何主机 printf ( "try to bind local address \n" ); localaddr.sin_port=htons ( atoi ( "8904" ) ); //(4)设置IPPROTO_IP标志,以便获取UDP包中的信息 int opt = 1; setsockopt ( sockfd, IPPROTO_IP, IP_PKTINFO, &opt, sizeof ( opt ) ); int nb = 0; //(5)设置为广播方式 nb = setsockopt ( sockfd, SOL_SOCKET, SO_BROADCAST, ( char * ) &opt, sizeof ( opt ) ); if ( nb == -1 ) { printf ( "set socket error..." ); exit ( EXIT_FAILURE ); } printf ( "IP address Checking!\n" ); char cmbuf[100];// 这里只是为控制数据申请一个空间 //(6)初始化msg_iovec字段,以便获取UDP包数据域 char buffer[BUFLEN+1]; struct iovec iov[1]; iov[0].iov_base=buffer; iov[0].iov_len=sizeof ( buffer ); //(7)初始化struct msghdr,以便获取UDP包中目标IP地址和源地址 struct msghdr mh = { .msg_name = &localaddr, .msg_namelen = sizeof ( localaddr ), .msg_control = cmbuf, .msg_controllen = sizeof (cmbuf ), .msg_iov=iov, .msg_iovlen=1 }; //(8)将本机的地址信息与sockfd绑定起来 if ( bind ( sockfd, &localaddr, socklen ) <0 ) { printf ( "bind local address err in udptalk!\n" ); exit ( 2 ); } //发一个消息给目标主机 if ( sendto ( sockfd, "HELLO", strlen ( "HELLO" ), 0, &peeraddr, socklen ) <0 ) { printf ( "sendto err in udptalk!\n" ); exit ( 3 ); } printf ( "end of sendto \n" ); printf ( "start of recv&send message loop!\n" ); for ( ;; )//接收消息循环 { printf ( "Waiting For Message...!\n" ); n=recvmsg ( sockfd, &mh, 0 ); //判断socket是否有错误发生 if ( n<0 ) { printf ( "recvfrom err in udptalk!\n" ); exit ( 4 ); } else { cmbuf[n]=0; printf ( "Receive:%dByte。\tThe Message Is:%s\n", n,buffer ); } //(9)初始化cmsghdr以便处理mh中的附属数据,通过遍历附属数据对象,找出我们感兴趣的信息 struct cmsghdr *cmsg ; for ( cmsg = CMSG_FIRSTHDR ( &mh ); cmsg != NULL; cmsg = CMSG_NXTHDR ( &mh, cmsg ) ) { // 忽略我们不需要的控制头(the control headers) if ( cmsg->cmsg_level != IPPROTO_IP || cmsg->cmsg_type != IP_PKTINFO ) { continue; } struct in_pktinfo *pi = CMSG_DATA ( cmsg ); //(10)将地址信息转换后输出 char dst[100],ipi[100];//用来保存转化后的源IP地址,目标主机地址
// pi->ipi_spec_dst 是UDP包中的路由目的IP地址(the destination in_addr) // pi->ipi_addr 是UDP包中的头标识目的地址(the receiving interface in_addr) if ( ( inet_ntop ( AF_INET,& ( pi->ipi_spec_dst ),dst,sizeof ( dst ) ) ) !=NULL ) { printf ( "路由目的IP地址IPdst=%s\n",dst); } if ( ( inet_ntop ( AF_INET,& ( pi->ipi_addr ),ipi,sizeof ( ipi ) ) ) !=NULL ) { printf ("头标识目的地址ipi_addr=%s\n",ipi); } } printf ( "Send Some Message To Server\n" ); if ( sendto ( sockfd, "Hello", strlen ( buffer ), 0, &peeraddr, socklen) <0 ) { printf ( "sendto err in udptalk!\n" ); exit ( 3 ); } } }
1、开启虚拟机下面的例子程序
2、通过windows下面的网络调试助手向虚拟机发送数据
因为通过虚拟网卡的,所以我们看到目标IP地址并不是网络调试助手中设置的IP,而是虚拟网卡的地址,通过Linux下的tcpdump我们可以看到其中网卡转发的过程。
1、struct in_addr
struct in_addr { in_addr_t s_addr; };
结构体in_addr 用来表示一个32位的IPv4地址.
in_addr_t 一般为 32位的unsigned long,其字节顺序为网络顺序(network byte ordered),即该无符号整数采用大端字节序
2、struct msghdr
recvmsg()使用 msghdr 结构体(structure )减少参数传递的数目。这个结构体定义在 <sys/socket.h>中,如下所示
struct iovec { /* Scatter/gather array items */ void *iov_base; /* Starting address */ size_t iov_len; /* Number of bytes to transfer */ }; struct msghdr { void *msg_name; /* optional address */ socklen_t msg_namelen; /* size of address */ struct iovec *msg_iov; /* scatter/gather array */ size_t msg_iovlen; /* # elements in msg_iov */ void *msg_control; /* ancillary data, see below */ size_t msg_controllen; /* ancillary data buffer len */ int msg_flags; /* flags on received message */ };
struct msghdr看上去似乎是一个需要创建的巨大的结构。但是不要怕。其结构成员可分为四组:
套接口地址成员: msg_name与msg_namelen。
I/O向量引用:msg_iov与msg_iovlen。
附属数据缓冲区成员:msg_control与msg_controllen。
接收信息标记位:msg_flags。
在我们将这个结构分为上面的几类以后,结构看起来就不那样巨大了。
标记位 | 描述 |
MSG_EOR | 当接收到记录结尾时会设置这一位。这通常对于SOCK_SEQPACKET套接口类型十分有用。 |
MSG_TRUNC | 这个标记位表明数据的结尾被截短,因为接收缓冲区太小不足以接收全部的数据。 |
MSG_CTRUNC | 这个标记位表明某些控制数据(附属数据)被截短,因为缓冲区太小。 |
MSG_OOB | 这个标记位表明接收了带外数据。 |
MSG_ERRQUEUE | 个标记位表明没有接收到数据,但是返回一个扩展错误。 |
3、struct cmsghdr结构
recvmsg与sendmsg函数允许程序发送或是接收附属数据。然而,这些额外的信息受限于一定的格式规则。下面将会介绍控制信息头与程序将会用来管理这些信息的宏。
属信息可以包括0,1,或是更多的单独附属数据对象。在每一个对象之前都有一个struct cmsghdr结构。头部之后是填充字节,然后是对象本身。最后,附属数据对象之后,下一个cmsghdr之前也许要有更多的填充字节。在这里,我们将要关注的附属数据对象是文件描述符与证书结构。
图1显示了一个包含附属数据的缓冲区是如何组织的。
图1 辅助数据结构是由各种子结构、数据区, 填充字节构成
我们需要注意以下几点:
cmsg_len与CMSG_LEN()宏值所显示的长度相同。
CMSG_SPACE()宏可以计算一个附属数据对象的所必需的空白。
msg_controllen是CMSG_SPACE()长度之后,并且为每一个附属数据对象进行计算。
struct cmsghdr { socklen_t cmsg_len; /* data byte count, including header */ int cmsg_level; /* originating protocol */ int cmsg_type; /* protocol-specific type */ /* followed by unsigned char cmsg_data[]; */ };
其成员描述如下:
成员 | 描述 |
cmsg_len | 附属数据的字节计数,这包含结构头的尺寸。这个值是由CMSG_LEN()宏计算的。 |
cmsg_level | 这个值表明了原始的协议级别(例如,SOL_SOCKET)。 |
cmsg_type | 这个值表明了控制信息类型(例如,SCM_RIGHTS)。 |
cmsg_data | 这个成员并不实际存在。他用来指明实际的额外附属数据所在的位置。 |
这一章所用的例子程序只使用SOL_SOCKET的cmsg_level值。这一章我们感兴趣的控制信息类型如下(cmsg_level=SOL_SOCKET):
cmsg_level | 描述 |
SCM_RIGHTS | 附属数据对象是一个文件描述符 |
SCM_CREDENTIALS | 附属数据对象是一个包含证书信息的结构 |
//setsockopt函数原型 #include <sys/types.h> #include <sys/socket.h> int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
#include <sys/socket.h> struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh); struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg); size_t CMSG_ALIGN(size_t length); size_t CMSG_SPACE(size_t length); size_t CMSG_LEN(size_t length); void *CMSG_DATA(struct cmsghdr *cmsg);
struct cmsgptr *mptr; int fd; /* File Descriptor */ . . . fd = *(int *)CMSG_DATA(mptr);
参考链接
关于struct msghdr和struct cmsghdr
Linux Socket Programming by Example - Warren Gay
在我的上一篇博文Linux操作系统学习_用户态与内核态之切换过程中,我已经详细介绍了Linux操作系统中为什么要分为用户态和内核态两种状态,以及用户态和内核态的区别。同时也从虚拟地址空间的角度阐述了内核空间与用户空间的概念,更是由此引出了内核栈与用户栈的概念及区别。在操作系统工作的过程中,大多数时间都是运行在用户态下。只有当用户态下的程序发生了系统调用,或者当进程在用户态下执行时发生了硬件中断,再或者当进程执行在用户态下时,发生了异常,这些情况下,系统都会发生用户态和内核态之间的切换,即由用户态切换到内核态。
下面简要的叙述一下操作系统的工作机制。首先一个进程,这里我们假设为A,运行在用户态下,此时产生了中断,这时需要调用操作系统内核态下的中断处理函数来对中断进行处理,因此,这时就会发生用户态和内核态之间的切换,将进程由运行于用户态切换到运行于内核态。具体详细的过程,前一篇博文中已经介绍了。在这一过程中,首先将esp寄存器的值指向内核空间的内核栈,将eip寄存器指向中断处理程序的入口地址,即中断处理程序的第一条指令。然后,将进程A用户态下的用户栈的esp值和eip值入栈。(可能有人会在这里产生疑惑,esp和eip的值都已经改变了,怎么还要将用户态下的值入栈呢?另外一个疑问就是,你怎么知道是压入到内核栈。对于第一个问题,关键在于TSS,虽然在之前将esp和eip的值都改变了,但是TSS中仍保存着用户态下的各个寄存器的信息,因此,将TSS中的esp和eip的信息入栈,即是将用户态下的用户栈的信息入栈,实际上是对进程前一状态进行保存,以便中断返回后可以继续正确的执行之前正在执行的任务。第二个问题其实很简单,因为第一步中已经将esp指向了内核栈的栈顶,而我们知道,每次由用户态切换到内核态时,内核栈都是空的,因此内核栈的栈顶和栈底实际上是一个地址,因此我们可以确定是压入到了内核栈)。然后将之前在用户态运行的应用程序的eflags,cs等寄存器的内容压入到内核栈中。然后调用SAVE_ALL宏来将其他所有的寄存器信息入栈保存。由于此时cs、eip已经指向了中断处理程序,因此,开始执行中断处理程序,对中断进行处理。当中断处理程序执行完毕后,会判断是否需要进行进程调度,若是需要,则调用schedule函数,然后在schedule中会调用switch_to宏,在switch_to宏中又会调用_switch_to函数来进行进程调度。具体的switch_to函数的原理,见上面的参考资料。
当进程需要从内核态返回到用户态时,主要是对前面所进行过的所有改变的一种恢复,首先调用restore_all来将eax、ebx等寄存器保存于内核栈中的值出栈,然后调用iret来恢复其他寄存器的状态,可以看出,iret是与SAVE_ALL相反的过程。总体来说,restore_all和iret都是在恢复之前由用户态切换到内核态时,各个寄存器的状态的变化。到此,我们的系统又由运行于内核态切换到了用户态。