zoukankan      html  css  js  c++  java
  • Linux操作系统学习_操作系统是如何工作的

    Linux操作系统学习_操作系统是如何工作的

    实验五:Linux操作系统是如何工作的?

    学号:SA1****369

    操作系统工作的基础:存储程序计算机、堆栈(函数调用堆栈)机制和中断机制

          首先要整明白的一个问题是什么是存储程序计算机?其实存储程序计算机正是冯.诺依曼最初提出的计算机体系模型,现在我们一提到冯.诺依曼体系结构首先会想到的应该是包含输入设备、输出设备、存储器、控制器、运算器组成的经典模型,但是这里我们强调的存储程序计算机的主要特征实际上并非如此,存储程序计算机的概念相当于是褪去现代计算机华丽的外衣,所呈现出来的实实在在的计算机的本质。主要由处理器和存储器组成,中间以总线相连。

           CPU通过总线从存储器中读取指令和数据进行处理,采用的主要机制是函数调用推栈机制。在不发生中断、异常以及系统调用的过程中,每个进程的执行都符合函数调用堆栈机制。

          接下来说说堆栈机制。具体的函数调用堆栈机制,在我之前的博文中已经详细阐述,具体见实验一 计算机是怎样工作的 SA*****369 张*铭 。

    PS:另外也可参阅C函数调用机制(x86的linux环境下)

          中断机制是现代操作系统的特征所在,即这是区分操作系统是否为现代操作系统的一个重要依据,也是现代操作系统能够实现多任务的一个重要因素。我们可以将硬件中断(即外围设备产生的中断)与软中断(即系统调用)共同称为中断。CPU在运行一个进程的过程中,接受到一个中断信号,则CPU会停下正在执行的任务,转而去处理相应的中断服务例程。每个不同的中断都对应着一个相应的中断服务处理程序。

    简述操作系统(内核)是如何工作

    参考材料:

    Linux操作系统学习_用户态与内核态之切换过程

    分析进程切换宏 switch_to

    linux2.6.29 swtich_to 详细分析

    在接受到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设置为目的地址。

     

    (二)下面的例子简单地说明如何获取UDP包中的源地址(interface addresses)、目标地址(destination addresses)。为了代码的简单,下面代码段省去了错误检查。

     

    复制代码
    // 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)
    }
    复制代码

     

    (三)下面我将给出一个完整可运行的例子,这个例子实现了接收UDP广播包,发送UDP广播包,并在接收的时候,打印出UDP包的路由目的IP地址和头标识目的地址。

     

    复制代码
    #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
    " );
            exit ( EXIT_FAILURE );
        }
        printf ( "IP address Checking!
    " );
        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 
    " );
        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!
    " );
    
        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!
    " );
            exit ( 2 );
        }
        //发一个消息给目标主机
        if ( sendto ( sockfd, "HELLO", strlen ( "HELLO" ), 0, &peeraddr, socklen )
    <0 )
        {
            printf ( "sendto err in udptalk!
    " );
            exit ( 3 );
        }
        printf ( "end of sendto 
    " );
        printf ( "start of recv&send message loop!
    " );
        for ( ;; )//接收消息循环
        {
            
            printf ( "Waiting For Message...!
    " );
            n=recvmsg ( sockfd, &mh, 0 );
            //判断socket是否有错误发生
            if ( n<0 )
            {
                printf ( "recvfrom err in udptalk!
    " );
                exit ( 4 );
            }
            else
            {
                cmbuf[n]=0;
                printf ( "Receive:%dByte。	The Message Is:%s
    ", 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 ",dst); } if ( ( inet_ntop ( AF_INET,& ( pi->ipi_addr ),ipi,sizeof ( ipi ) ) ) !=NULL ) { printf ("头标识目的地址ipi_addr=%s ",ipi); } } printf ( "Send Some Message To Server " ); if ( sendto ( sockfd, "Hello", strlen ( buffer ), 0, &peeraddr, socklen) <0 ) { printf ( "sendto err in udptalk! " ); exit ( 3 ); } } }
    复制代码

     

    例子的使用说明

     

    1、开启虚拟机下面的例子程序

     

    2、通过windows下面的网络调试助手向虚拟机发送数据

     

     image

     

    结果截图

     

     

    因为通过虚拟网卡的,所以我们看到目标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_name与msg_namelen 
    这些成员只有当我们的套接口是一个数据报套接口时才需要。msg_name成员指向我们要发送或是接收信息的套接口地址。成员msg_namelen指明了这个套接口地址的长度。 
    当调用recvmsg时,msg_name会指向一个将要接收的地址的接收区域。当调用sendmsg时,这会指向一个数据报将要发送到的目的地址。 
    注意,msg_name定义为一个(void *)数据类型。我们并不需要将我们的套接口地址转换为(struct sockaddr *)。

     

    成员msg_iov与msg_iovlen 
    这些成员指定了我们的I/O向量数组的位置以及他包含多少项。msg_iov成员指向一个struct iovec数组。我们将会回忆起I/O向量指向我们的缓冲区。成员msg_iov指明了在我们的I/O向量数组中有多少元素。

     

    成员msg_control与msg_controllen 
    这些成员指向了我们附属数据缓冲区并且表明了缓冲区大小。msg_control指向附属数据缓冲区,而msg_controllen指明了缓冲区大小。

     

    成员msg_flags 
    当使用recvmsg时,这个成员用于接收特定的标记位(他并不用于sendmsg)。在这个位置可以接收的标记位如下表所示

     

    标记位 描述 
    MSG_EOR  当接收到记录结尾时会设置这一位。这通常对于SOCK_SEQPACKET套接口类型十分有用。
    MSG_TRUNC 这个标记位表明数据的结尾被截短,因为接收缓冲区太小不足以接收全部的数据。
    MSG_CTRUNC 这个标记位表明某些控制数据(附属数据)被截短,因为缓冲区太小。
    MSG_OOB 这个标记位表明接收了带外数据。
    MSG_ERRQUEUE 个标记位表明没有接收到数据,但是返回一个扩展错误。

     

     

     

    3、struct cmsghdr结构

     

    recvmsg与sendmsg函数允许程序发送或是接收附属数据。然而,这些额外的信息受限于一定的格式规则。下面将会介绍控制信息头与程序将会用来管理这些信息的宏。

     

    属信息可以包括0,1,或是更多的单独附属数据对象。在每一个对象之前都有一个struct cmsghdr结构。头部之后是填充字节,然后是对象本身。最后,附属数据对象之后,下一个cmsghdr之前也许要有更多的填充字节。在这里,我们将要关注的附属数据对象是文件描述符与证书结构。 
    图1显示了一个包含附属数据的缓冲区是如何组织的。

     

     

                        graphics/17fig01.gif

     

                                                      图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 附属数据对象是一个包含证书信息的结构

     

         

     

    (二)涉及的函数

     

    1、setsockopt函数原型

     

    //setsockopt函数原型
    #include <sys/types.h>       
    #include <sys/socket.h>
    int setsockopt(int s, int level, int optname,
                          const void *optval, socklen_t optlen);

     

     

    2、cmsg 宏 
    由于附属数据结构的复杂性,Linux系统提供了一系列的C宏来简化我们的工作。另外,这些宏可以在不同的UNIX平台之间进行移植,并且采取了一些措施来防止将来的改变。这些宏是由cmsg(3)的man手册页来进行描述的,其概要如下:

     

    复制代码
    #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);
    复制代码

     

    CMSG_DATA()宏 
    这个宏接受一个指向cmsghdr结构的指针。返回的指针值指向跟随在头部以及填充字节之后的附属数据的第一个字节(如果存在)。如果指针mptr指向一个描述文件描述符的可用的附属数据信息头部,这个文件描述符可以用下面的代码来得到:

     

    struct cmsgptr *mptr;
    int fd; /* File Descriptor */
    . . .
    fd = *(int *)CMSG_DATA(mptr);

     

    CMSG_FIRSTHDR()宏 
    这个宏用于返回一个指向附属数据缓冲区内的第一个附属对象的struct cmsghdr指针。输入值为是指向struct msghdr结构的指针(不要与struct cmsghdr相混淆)。这个宏会估计msghdr的成员msg_control与msg_controllen来确定在缓冲区中是否存在附属对象。然后,他会计算返回的指针。 
    如果不存在附属数据对象则返回的指针值为NULL。否则,这个指针会指向存在的第一个struct cmsghdr。这个宏用在一个for循环的开始处,来开始在附属数据对象中遍历。

     

    CMSG_NXTHDR()宏 
    这个用于返回下一个附属数据对象的struct cmsghdr指针。这个宏会接受两个输入参数: 
    指向struct msghdr结构的指针 
    指向当前struct cmsghdr的指针 
    如果没有下一个附属数据对象,这个宏就会返回NULL。

     

    LinuxC下获取UDP包中的路由目的IP地址和头标识目的地址

     

    参考链接

     

    Get destination address of a received UDP packet

     

    套接字选项(四)

     

    inet_pton & inet_ntop函数

     

    in_addr

     

    Listen for and receive UDP datagrams in C

     

    关于struct msghdr和struct cmsghdr

     

    Linux Socket Programming by Example - Warren Gay

     

    Linux Socket学习

     

     

     

     

     

     
    标签: Linux网络

     

          在我的上一篇博文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都是在恢复之前由用户态切换到内核态时,各个寄存器的状态的变化。到此,我们的系统又由运行于内核态切换到了用户态。

     
     
     
  • 相关阅读:
    idea 常用功能
    405 跨域问题的解决
    哪些情况会报 404
    [LeetCode]题解(python):077-Combinations
    [LeetCode]题解(python):076-Minimum Window Substring
    [LeetCode]题解(python):075-Sort Colors
    [LeetCode]题解(python):074-Search a 2D Matrix
    [LeetCode]题解(python):073-Set Matrix Zeroes
    [LeetCode]题解(python):072-Edit Distance
    [LeetCode]题解(python):071-Simplify Path
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3163147.html
Copyright © 2011-2022 走看看