Socket与系统调用深度分析

Posted wenkail

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket与系统调用深度分析相关的知识,希望对你有一定的参考价值。

linux中,将程序的运行空间分为内核空间与用户空间(内核态和用户态),在逻辑上它们之间是相互隔离的,因此用户程序不能访问内核数据,也无法使用内核函数。当用户进程必须访问内核或使用某个内核函数时,就得使用系统调用(System Call)。在Linux中,系统调用是用户空间访问内核空间的唯一途径.

 《一》 Socket API编程接口:

(1)API:应用编程接口,即应用程序与系统之间的接口,本质是一些预先定义的函数集合,

功能:应用程序或开发人员可利用API访问系统中的资源和取得 OS 的服务(例如利用API访问一组例程),实现计算机软件之间的相互通信。

(2)Socket:对Socket理解为一种特殊的文件,是对“open—write/read—close”模式的一种实现,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),包括socket()、bind()、listen()、connect()、accept()、read()、write()以及close()等函数.

《二》 系统调用机制:

系统调用:就是一种特殊的接口。通过这个接口,用户可以访问内核空间。系统调用规定了用户进程进入内核的具体位置。

系统调用是用户进程进入内核的接口层,它本身并非内核函数,但它是由内核函数实现,进入内核后,不同的系统调用会找到各自对应的内核函数,这些内核函数被称为系统调用的“服务例程”。比如系统调用getpid实际调用了服务例程为sys_getpid(),或者说系统调用getpid是服务例程sys_getpid()的“封装例程”。
具体步骤:用户进程-->系统调用-->内核-->返回用户空间。

系统调用就是为了解决上述问题而引入的,是提供给用户的“特殊接口”。

 系统调用规定用户进程进入内核空间的具体位置。

   (1).程序运行空间从用户空间进入内核空间。

   (2)处理完后再返回用户空间。

《三》 API与系统调用的区别和联系

(1)区别:API是函数的定义,规定了这个函数的功能,跟内核无直接关系。而系统调用是通过中断向内核发请求,实现内核提供的某些服务。

(2)联系:程序员调用的是API(API函数),然后通过与系统调用共同完成函数的功能。   因此,API是一个提供给应用程序的接口,一组函数,是与程序员进行直接交互的。系统调用则不与程序员进行交互的,它是根据API函数,通过一个软中断机制向内核提交请求,以获取内核服务的接口。

技术图片

 《四》  Socket系统调用过程分析

1、   函数原型:int socket(int domain, int type, int protocol);

2、   内核实现源码分析

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
    int retval;
    struct socket *sock;
    int flags;
... retval
= sock_create(family, type, protocol, &sock); if (retval < 0) goto out; retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); if (retval < 0) goto out_release; out: /* It may be already another descriptor 8) Not kernel problem. */ return retval; out_release: sock_release(sock); return retval; }

下面主要分析sock_create和sock_map_fd这两个函数,看它们是怎么在内核一步步创建和管理我们使用的socket。

 (1)sock_create函数

sock_create() 实际调用的是 __sock_create()。

int __sock_create(struct net *net, int family, int type, int protocol,
                  struct socket **res, int kern)
{
    int err;
    struct socket *sock;
    const struct net_proto_family *pf;
...
    err = security_socket_create(family, type, protocol, kern);//SElinux相关,暂不关注
    /*
     *  Allocate the socket and allow the family to set things up. if
     *  the protocol is 0, the family is instructed to select an appropriate
     *  default.
     */
    sock = sock_alloc();//创建struct socket结构体

    sock->type = type;//设置套接字的类型
...
    pf = rcu_dereference(net_families[family]);//获取对应协议族的协议实例对象
...
    err = pf->create(net, sock, protocol, kern);
    if (err < 0)
        goto out_module_put;

...
    err = security_socket_post_create(sock, family, type, protocol, kern);//SElinux相关,暂不关注

    *res = sock;
...
}
 

 其中sock_alloc()和pf->create()这两个函数比较重要,Sock_alloc()函数分配一个struct socket_alloc结构体,将sockfs相关属性填充在socket_alloc结构体的vfs_inode变量中,以限定后续对这个sock文件允许的操作。同时sock_alloc()最终返回socket_alloc结构体的socket变量,用于后续操作。

pf->create调用的就是inet_create()函数

static int inet_create(struct net *net, struct socket *sock, int protocol,
                       int kern)
{
    struct sock *sk;
    struct inet_protosw *answer;
    struct inet_sock *inet;
    struct proto *answer_prot;
    unsigned char answer_flags;
    char answer_no_check;
    int try_loading_module = 0;
    int err;
...
    sock->state = SS_UNCONNECTED;

    /* Look for the requested type/protocol pair. */
lookup_protocol:
    err = -ESOCKTNOSUPPORT;
    rcu_read_lock();
    //根据socket传入的protocal在inetsw[]数组中查找对应的元素
    list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {

        err = 0;
        /* 如果我们在socket的protocal传入的是6,即TCP协议,那么走这个分支 */
        if (protocol == answer->protocol) {
            if (protocol != IPPROTO_IP)
                break;
        } else {
            /* 如果socket的protocal传入的是0,那么走这个分支 */
            if (IPPROTO_IP == protocol) {
                protocol = answer->protocol;//重新给protocal赋值,因此socket中protocal传入的是0或者6,都是可以的
                break;
            }
            if (IPPROTO_IP == answer->protocol)
                break;
        }
        err = -EPROTONOSUPPORT;
    }
...
    sock->ops = answer->ops;//将查找到的对应协议族的协议函数操作集赋值给我们之前创建的socket
    answer_prot = answer->prot;
...
    //创建sock结构体,注意这里创建的结构体类型是sock,之前我们通过sock_alloc创建的结构体类型是socket
    sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot);
    if (sk == NULL)
        goto out;
...
    sock_init_data(sock, sk);//sock初始化

    //另一部分初始化,里面有对各个socket连接定时器的初始化
    if (sk->sk_prot->init) {
        err = sk->sk_prot->init(sk);
        if (err)
            sk_common_release(sk);
    }
...
}
}
 
(2)sock_map_fd()

这个函数主要有两个部分,一个是创建file文件结构,fd文件描述符,另一部分是将file文件结构和fd文件描述符关联,同时将上一步返回的socket也一起绑定,形成一个完整的逻辑

static int sock_map_fd(struct socket *sock, int flags)
{
    struct file *newfile;
    //获取一个未使用的文件描述符,文件描述符的管理这里就暂不分析了
    int fd = get_unused_fd_flags(flags);
    if (unlikely(fd < 0))
        return fd;

    //分配file结构体
    newfile = sock_alloc_file(sock, flags, NULL);
    if (likely(!IS_ERR(newfile))) {
        fd_install(fd, newfile);
        return fd;
    }
...
}
 

至此,socket系统调用就结束了,将fd返回用户使用。

综上,socket系统调用的操作:首先在内核生成一个socket_alloc 和tcp_sock类型的对象,其中sock_alloc对象中的socket和tcp_sock对象的sock绑定,sock_alloc对象中的inode和file类型对象绑定。然后将分配的文件描述符fd和file对象关联,最后将这个文件描述符fd返回给用户使用。

经过这一连串操作,用户只要使用fd,内核就能根据这个fd进行网络连接管理的各种操作。

《五》跟踪分析Socket相关系统调用的内核处理函数

本次跟踪分析基于上次构建的MenuOS系统, 通过gdb设置断点来跟踪分析socket内核处理函数;

在linux-5.0.1目录下打开新的终端,执行命令,如下:

gdb
file vmlinux
target remote:1234
b __sys_socket
b __sys_bind
b __sys_listen
b __sys_shutdown

通过以上指令,系统会进入gdb模式,通过file vmlinux加载vmlinux,用target remote:1234和menuos连接, 后面指令为设置的端点,执行到对应函数时,程序暂停,按c执行下去。结果显示如下,可见在replyhi 执行过程中,调用了socket()、bind()、listen()等API函数 ,实验完成。

技术图片

技术图片

 

 

 


 

以上是关于Socket与系统调用深度分析的主要内容,如果未能解决你的问题,请参考以下文章

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析

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

Socket与系统调用深度分析

Socket与系统调用深度分析