accept用例分析

case本身说明:

Verify that accept() returns the proper errno for various failure cases

验证accept()是否返回正确的errno.

测试log

accept01    1  TPASS  :  bad file descriptor successful
accept01    2  TPASS  :  bad file descriptor successful
accept01    3  TPASS  :  invalid socket buffer successful
accept01    4  TPASS  :  invalid salen successful
accept01    5  TPASS  :  invalid salen successful
accept01    6  TPASS  :  no queued connections successful
accept01    7  TPASS  :  UDP accept successful

~
测试错误类型包括:

bad file descriptor、invalid socket buffer、invalid salen、no queued connections、 UDP accept等

accept函数

accept()

接收一个套接字中已建立的连接

使用格式

#include <sys/types.h>
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能参数描述

accept()系统调用主要用在基于链接的套接字类型,比如SOCK_STREAM和SOCK_SEQPACKET.它提取出所监听套接字的等待连接队列中第一个连接请求,创建一个新的套接字,并返回指向该套接字的文件描述符。新建立的套接字不在监听状态,原来所监听的套接字也不受该系统调用的影响。

备注: 新建立的套接字准备发送send()和接收数据recv().

参数:

sockfd, 利用系统调用socket()建立的套接字描述符,通过bind()绑定到一个本地地址(一般为服务器的套接字),并且通过listen()一直在监听连接。

addr, 指向struct sockaddr的指针,该结构用通讯层服务器对等套接字的地址(一般为客户端地址)填写,返回地址addr的确切格式由套接字的地址类别(比如tcp或udp)决定;若addr为NULL,没有有效地址填写,这种情况下,addrlen也不使用,应该置为NULL;

备注: addr是个指向局部数据结构sockaddr_in的指针,这就是要求接入的信息本地的套接字(地址和指针)

addrlen, 一个值结果参数,调用函数必须初始化为包含addr所指向结构大小的数值,函数返回时包含对等地址(一般为服务器地址)的实际数值;

备注: addrlen 是个局部整型变量, 设置为sizeof(struct sockaddr_in)

如果队列中没有等待的连接,套接字也没有被标记为Non-blocking, accept()会阻塞调用函数知道连接出现;如果套接字被标记为Non-blocking, 队列中也没有等待的连接,accept返回错误EAGAIN或EWOULDBLOCK.

备注: 一般来说,实现时accept()为阻塞函数,当监听socket调用accept()时,它先到自己的receive_buf中查看是否有连接数据包;

若有,把数据拷贝处理啊,删掉接收到的数据包,创建新的socket与客户发来的地址建立链接;

若没有,就阻塞等待;

为了在套接字中有到来的连接时得到通知,可以使用select()或poll().当尝试建立新连接时,系统发送一个可读事件,然后调用accept()为该链接获取套接字。另一种方法是,当套接字中有连接到来时设定套接字发送SIGIO信号。

返回值

成功时,返回非负整数,该整数是接收到套接字的描述符;出错时,返回-1,相应的设定全局变量errno.

错误处理

Linux下,accept()把已等待的网络错误传给新建立的连接,当作是accept()返回的错误。这与其他的BSD实现是不同的。为了可靠运行,应该在accept()之后检测协议已定义的一些网络错误,并把这些错误当作EAGAIN并重试。对于tcp/ip协议来说,主要有:ENETDOWN,EPROTO,ENOPROTOOPT,EHOSTDOWN,ENONET,EHOSTUNREACH,EOPNOTSUPP和ENETUNREACH。

accept01.c代码分析

主要函数包括mian、setup、cleanup、setup0、cleanup0、setup1、cleanup1、setup2、setup3

test_case_t结构体

struct test_caset {         
    int domain;  /*AF_INIT, AF_UNIX,....*/
    int type; /* SOCK_STREAM, SOCK_DGRAM...*/
    int proto; /* protocol number(usually 0 = default) */
    struct sockaddr *sockaddr; /*socket address buffer */
    socklen_t *salen;  /* accept's 3rd argument */
    int retval;   /* syscall return value */
    int experrno;  /* expected errno */
    void (*setup)(void);
    void(*cleanup)(void);
    char *desc;
} 
 tdat[] = {
     {PF_INET, SOCK_STREAM, 0, (struct sockaddr *)&fsin1,
         &sinlen, -1, EBADF, setup0, cleanup0,
         "bad file descriptor"},   //无效的文件描述符
      PF_INET, SOCK_STREAM, 0, (struct sockaddr *)&fsin1,
        &sinlen, -1, ENOTSOCK, setup0, cleanup0,
        "bad file descriptor"}, { //无效的文件描述符
      PF_INET, SOCK_STREAM, 0, (struct sockaddr *)3,
        &sinlen, -1, EINVAL, setup1, cleanup1,
        "invalid socket buffer"}, {  //无效套接字缓存区
      PF_INET, SOCK_STREAM, 0, (struct sockaddr *)&fsin1,
        (socklen_t *) 1, -1, EINVAL, setup1, cleanup1,
        "invalid salen"}, {   //无效的salen
      PF_INET, SOCK_STREAM, 0, (struct sockaddr *)&fsin1,
        &sinlen, -1, EINVAL, setup2, cleanup1,
        "invalid salen"}, {
      PF_INET, SOCK_STREAM, 0, (struct sockaddr *)&fsin1,
        &sinlen, -1, EINVAL, setup3, cleanup1,
        "no queued connections"}, {  //队列中没有等待的连接
      PF_INET, SOCK_DGRAM, 0, (struct sockaddr *)&fsin1,
        &sinlen, -1, EOPNOTSUPP, setup1, cleanup1,
        "UDP accept"},};
int TST_TOTAL = sizeof(tdat) / sizeof(tdat[0]); /*通过数组的长度,计算测试用例数量*/

先定义测试case需要的结构体,然后进行了结构体初始化,定义了7种错误类型,对应7个测试case,和测试log是对应的。 顺便看一下 SOCK_STREAM和SOCK_DGRAM的区别: sock_stream 是有保障的(即能保证数据正确传送到对方)面向连接的SOCKET,多用于资料(如文件)传送。 sock_dgram 是无保障的面向消息的socket , 主要用于在网络上发广播信息。 SOCK_STREAM是基于TCP的,数据传输比较有保障。SOCK_DGRAM是基于UDP的,专门用于局域网,基于广播 SOCK_STREAM 是数据流,一般是tcp/ip协议的编程,SOCK_DGRAM分是数据抱,是udp协议网络编程

再看说一下AF_INET和PF_INET的差别:

在Unix/Linux系统中,在不同的版本中这两者有微小差别.对于BSD,是AF,对于POSIX是PF.理论上建立socket时是指定协议,应该用PF_xxxx,设置地址时应该用AF_xxxx

分别看一下这几个错误码

  • EBADF: 当作为参数的套接字不是一个有效的文件描述符(在Linux下用文件描述符来表示设备文件和普通文件。文件描述符是一个整型的数据,所有对文件的操作都通过文件描述符实现。文件描述符是文件系统中链接用户空间和内核空间的枢纽)时,抛出这个错误码。无效的文件描述符是什么意思呢?就是fd已经close的,或者本身就不是个有效的socket的fd。

  • ENOTSOCK: 在非socket上执行socket操作。

  • EINVAL: 无效参数。提供的参数非法。有时也会与socket的当前状态相关,如一个socket并没有进入listening状态,此时调用accept,就会产生EINVAL错误。

  • EOPNOTSUPP: 不支持的操作。 引用对象的类型不支持尝试的操作。通常,这发生在套接字描述符不支持此操作,例如,试着接受数据报套接字上的连接的套接字。

接下来看几个辅助函数。

1.setup函数

static void setup(void)
{
     TEST_PAUSE;
     /*初始化本地 sockaddr*/
     sin0.sin_family = AF_INET;  //绑定本地地址或连接远程地址时需要初始化sockaddr_in结构,其中指定address family时一般设置为AF_INET,即使用IP
     sin0.sin_port = 0;
     sin0.sin_addr.s_addr = INADDR_ANY; //指定地址为0.0.0.0的地址
}

TEST_PAUSE是一个宏定义,代表usc_global_setup_hook()函数。作用是当设置了暂停标志后,暂停SIGUSR1,当新的信号来时,再继续。下面是usc_global_setup_hook函数实现:

#define TEST_PAUSE usc_global_setup_hook();
int usc_global_setup_hook()

int usc_global_setup_hook(void)
{
#ifndef UCLINUX
   /*定义temp变量存储信号旧信号的动作,中断后恢复*/
   int (*_TMP_FUNC) (void);
   /*中断等待sigusr1.*/
    if (STD_PAUSE) {
        _TMP_FUNC = (int (*)())singnal(SIGUSR1, STD_go);
        pause()
        signal(SIGUSR1, (void (*)())_TMP_FUNC);
    }

    if (STD_TP_sbrk) || STD_LP_sbrk)
        STD_start_break = sbrk(0); /*获取原始sbreak大小*/

    if (STD_TP_sbrk){
        sbrk(STD_TP_sbrk):
        if (Debug)
            print("after sbrk(%d)\n", STD_TP_sbrk);
    }
#endif
    return 0;
}

2.setup0函数case1测试前设置 static void setup0(void) { if (tdat[testno].experr == EBADF) s = 400; //什么都不打开的文件 else if ((s = open("/dev/null ", O_WRONLY)) == -1) tst_brkm(TBROK | TERRNO, cleanup, "error opening /dev/null"); }

3.setup2

static void setup2(void)
{
    setup1(); //调用setup1()获取套接字s
    sinlen = 1;  //s置为无效
}

4.setup3

static void setup3(void)
{
    int one = 1;

    setup1();
    SAFE_IOCTL(cleanup, s, FIONBIO, &one);
}

SAFE_IOCTL

#define SAFE_IOCTL(cleanup_fn, fd, request, ...)             \
    ({int ret = ioctl(fd, request, __VA_ARGS__);         \                                                            
    ret < 0 ?                                          \                                                            
    tst_brkm(TBROK | TERRNO, cleanup_fn,              \                                                            
    "ioctl(%i,%s,...) failed", fd, #request) \                                                            
    safe_: ret;})

6.cleanup、cleanup0, cleanup1

static void cleanup(void)
{
}

static void cleanup0(void)
{
    s = -1;
}

static void cleanup1(void)
{
    (void)close(s);
    s = -1;
}

重新设置s = -1

OK,现在看一下main函数。

int main(int ac, char *av[])
{
    int lc;

    tst_parse_opts(ac, av, NULL, NULL);  //ltp参数解析函数

    setup();

    for (lc = 0; TEST_LOOPING(lc); ++lc){
        test_count = 0;
        for (testno = 0; testno < TST_TOTAL; ++testno){
            tdat[testno].setup();

            TEST(accept(s, tdat[testno].sockaddr,
                tdat[testno].salen));
            if (TEST_RETURN > 0)
                TEST_RETURN = 0;
            if (TEST_RETURN != tdat[testno].retval ||
                (TEST_RETURN < 0 &&
                 TEST_ERRNO != tdat[testno].experrno)){
                  "%ld(expected %d), errno %d (expected"
                  "%d)", tdat[testno].desc,
                  TEST_RETURN, tdat[testno].desc,
                  TEST_ERRNO, tdat[testno].experrno);
            } else {
                tst_resm(TPASS, "%s successful",
                    tdat[testno].desc);
            }
            tdat[testno].cleanup();
        }

    }
    cleanup();
    tst_exit();
}

先看一下tst_parse_opts()函数

void tst_parse_opts(int argc, char *argv[], const option_t *user_optarg,
                    void (*user_help)(void))
{
    const char *msg;

    msg = parse_opts(argc, argv, user_optarg, user_help); //parse_opts是参数解析函数,后面文章再进行介绍

    if(msg)
        tst_brkm(TBROK, NULL, "OPTION  PARSING ERROR - %s ", msg);
}

接下来调用setup,准备测试环境,初始化本地IP地址。然后是一个嵌套的两层循环,第一次是测试次数的遍历,通过TEST_LOOPING实现。 TEST_LOOPING是int usc_test_looping(int counter)函数,该函数后面的文章再进行解释。tst_count初始化为0。

第二层循环是遍历case数组。tdat[testno].setup(),调用对应setup准备case的特殊设置。

TEST启动测试,

#define TEST(SCALL) \
    do { \
    errno = 0; \
    TEST_RETURN = SCALL; \
    TEST_ERRNO = errno; \
}

然后将tdat中的对应参数,分别执行accept函数,如果预期的返回值不符或则返回值小于0且和预期的错误类型不匹配,则调用tst_resm(),进行出错处理。tst_resm将在后面进行分析。如果符合预期,则打印TPASS,然后调用tdat[testno].clearnup()分别清理环境。

最后调用cleanup()清理,tst_exit()退出,tst_exit()在单独分析ltp测试框架时分析。

到此,我们就清楚accept测试的内容了。

by 李鹏