SocketIO通讯

IO模型

  • 非阻塞IO处理
    消耗cpu
  • 阻塞IO处理
    消耗资源
  • 多路IO复用
    内核监听多个文件描述符的属性(读写缓冲区)变化
    某个文件描述符的读缓冲区变化了,可以读了,将这个事件告知应用层
  • 异步IO
  • 信号驱动式IO模型
    signal

IO复用

select

跨平台,windows使用较多

typedef struct
  {
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];//1024/(8*8)=16
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;

简化

typedef struct{
    long int fds_bits[16];
}fd_set;
//fd_set就是一个long int类型的数组。因为每一位可以代表一个文件描述符。所以fd_set最多表示1024个文件描述符
//long int 是8字节,8*8=64位,64*16=1024位,1个位表示一个文件描述符,共1024个文件描述符

#define	FD_SET(fd, fdsetp)	__FD_SET (fd, fdsetp) //加入句柄
//#define FD_SET(fd,fdsetp)  fdsetp->__fds_bits[fd/64] |= (long int)1<<(d%64)
//描述符15 __fds_bits[15/64]=>__fds_bits[0]的第15位置1
//描述符127 __fds_bits[127/64]=>__fds_bits[1]的第63位置1==(1<<(127%64))
#define	FD_CLR(fd, fdsetp)	__FD_CLR (fd, fdsetp) //移除句柄
#define	FD_ISSET(fd, fdsetp)	__FD_ISSET (fd, fdsetp)
//检测fd在fdset集合中的状态是否变化,当检测到fd状态发生变化时返回真,否则,返回假(也可以认为集合中指定的文件描述符是否可以读写)
#define	FD_ZERO(fdsetp)		__FD_ZERO (fdsetp) //初始化(fd_set->fds_bits数组成员设置为0),可理解为初始化一个装有fd的文件夹
select (int __nfds, fd_set *__restrict __readfds,
		   fd_set *__restrict __writefds,
		   fd_set *__restrict __exceptfds,
		   struct timeval *__restrict __timeout)
//nfds:集合中所有文件描述符的范围,为所有文件描述符的最大值加1
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main(void)
{
    fd_set set;
    // long int l=(long int)1<<(127%64);
    FD_ZERO(&set);
    FD_SET(10,&set);
    FD_SET(65,&set);


    int i;
    for(i = 0;i < sizeof(set);i++)
    {
        printf("set.__fds_bits[%d]:%ld\t",i,set.__fds_bits[i]);
    }
    printf("\n/\n");
    FD_SET(1,&set);
    for(i = 0;i < sizeof(set);i++)
    {
        printf("set.__fds_bits[%d]:%ld\t",i,set.__fds_bits[i]);
    }
    
    printf("\n/\n");
    FD_ZERO(&set);
    FD_SET(16,&set);
    for(i = 0;i < sizeof(set);i++)
    {
        printf("set.__fds_bits[%d]:%ld\t",i,set.__fds_bits[i]);
    }
	return 0;
}

poll

nt poll (struct pollfd *fds, unsigned int nfds, int timeout);

参数解释:
(1)fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定测试某个给定的fd的条件
(2)nfds:表示fds结构体数组的长度
(3)timeout:表示poll函数的超时时间,单位是毫秒
函数功能:
监视并等待多个文件描述符的属性变化
函数返回值:
(1)返回值小于0,表示出错
(2)返回值等于0,表示poll函数等待超时
(3)返回值大于0,表示poll由于监听的文件描述符就绪返回,并且返回结果就是就绪的文件描述符的个数

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};
`fd`:每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。
`events`:表示要告诉操作系统需要监测fd的事件(输入、输出、错误),每一个事件有多个取值
`revents`:revents 域是文件描述符的操作结果事件,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回

events&revents

事件 描述 是否可作为输入(events) 是否可作为输出(revents)
POLLN 数据可读(包括普通数据&优先数据)
POLLOUT 数据可写(普通数据&优先数据)
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读(linux不支持)
POLLPRI 高优先级数据可读,比如TCP带外数据
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLRDHUP TCP连接被对端关闭,或者关闭了写操作,由GNU引入
POPPHUP 挂起
POLLERR 错误
POLLNVAL 文件描述符没有打开

读就绪:

1)socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
2)socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
3)监听的socket上有新的连接请求;
4)socket上有未处理的错误;

写就绪:

1)socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大⼩), 大于等于低水位标记 SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
2)socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发 SIGPIPE信号;
3)socket使⽤非阻塞connect连接成功或失败之后;
4)socket上有未读取的错误;
异常就绪:
socket上收到带外数据

epoll

linux使用
没有1024文件描述符限制
不需要拷贝监听的描述符到内核
返回已变化的描述符,不需要遍历树

cat /proc/sys/fs/epoll/max_user_watches
800890

注册到epoll实例中的最大文件描述符的数量限制

函数

  • epoll_create: 创建一个epoll实例,文件描述符红黑树
#include <sys/epoll.h>
      int epoll_create(int size);
    参数:
      size :  监听的文件描述符的上限,  2.6版本之后写1即可,
    返回:  返回树的句柄
  • epoll_ctl: 将监听的文件描述符添加到epoll实例中(上树 下树 修改节点),实例代码为将标准输入文件描述符添加到epoll中
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
    epfd : 树的句柄,epoll实例
    op : EPOLL_CTL_ADD 上树   
         EPOLL_CTL_DEL 下树 
         EPOLL_CTL_MOD 修改
    fd : 上树,下树的文件描述符
    event :   上树的节点
    typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;

           struct epoll_event {
               uint32_t     events;      /* Epoll events */  需要监听的事件
               epoll_data_t data;        /* User data variable */ 需要监听的文件描述符
           };
事件宏 events
    EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    EPOLLOUT: 表示对应的文件描述符可以写;
    EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    EPOLLERR: 表示对应的文件描述符发生错误;
    EPOLLHUP: 表示对应的文件描述符被挂断;
    EPOLLET: 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level Triggered)来说的。
    EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
  • epoll_wait: 监听;等待epoll事件从epoll实例中发生, 并返回事件以及对应文件描述符
#include <sys/epoll.h>

       int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
    功能: 监听树上文件描述符的变化
    epfd : 树的句柄
    events : 接收变化的节点的数组的首地址
    maxevents :  数组元素的个数
    timeout : -1 永久监听  大于等于0 限时等待
返回值: 返回的是变化的文件描述符个数

事件模型(epoll_wait工作方式)

水平触发(level-triggered)LT(默认)
  • 持续的高电平或持续的低电平
  • socket接收缓冲区不为空 有数据可读 读事件一直触发
  • socket发送缓冲区不满 可以继续写入数据 写事件一直触发
    一直触发epoll_wait耗费资源
边沿触发(edge-triggered)ET(常用)
  • 电平有高到低的一个变化或由低到高的变化
  • socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
  • socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
    读写缓冲区一次全部读取或写满
//设置cfd为非阻塞(循环读取缓冲区)
int fl=fcntl(sock,F_GETFL);
fcntl(sock,F_SETFL,fl|O_NONBLOCK);
//设置cfg读边沿触发,不设置lfd
ev.data.fd=cfd;
ev.events=EPOLLIN | EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
//判断lfd变化,并且是读事件变化
if(evs[i].data.fd == lfd && evs[i].events & EPOLLIN)
 {
    struct sockaddr_in cliaddr;
    char ip[16]="";
    socklen_t len = sizeof(cliaddr);
    int cfd = Accept(lfd,(struct sockaddr *)&cliaddr,&len);//提取新的连接

    printf("new client ip=%s port =%d\n",inet_ntop(AF_INET,&cliaddr.sin_addr.s_addr,ip,16)
							,ntohs(cliaddr.sin_port));
    //设置cfd为非阻塞
    int flags = fcntl(cfd,F_GETFL);//获取的cfd的标志位
    flags |= O_NONBLOCK;
    fcntl(cfd,F_SETFL,flags);
    //将cfd上树
    ev.data.fd =cfd;
    ev.events =EPOLLIN | EPOLLET;
    					      epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
				
}
else if (evs[i].events & EPOLLIN) //cfd变化 ,而且是读事件变化
{
//循环读取
  while (1)
  {
    char buf[4] = "";
    //如果读一个缓冲区,缓冲区没有数据,如果是带阻塞,就阻塞等待,如果
    //是非阻塞,返回值等于-1,并且会将errno 值设置为EAGAIN
    int n = read(evs[i].data.fd, buf, sizeof(buf));
    if (n < 0) //出错,cfd下树
    {
      //如果缓冲区读干净了,这个时候应该跳出while(1)循环,继续监听
      if (errno == EAGAIN)
      {
	break;
      }
      //普通错误
      perror("");
      close(evs[i].data.fd); //将cfd关闭
      epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
      break;
    }
    else if (n == 0) //客户端关闭 ,
    {
      printf("client close\n");
      close(evs[i].data.fd);							     //将cfd关闭
      epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]); //下树
      break;
    }
    else
    {
      //	printf("%s\n",buf);
      write(STDOUT_FILENO, buf, 4);
      write(evs[i].data.fd, buf, n);
    }
  }
}

边沿触发仅触发一次,水平触发会一直触发
libevent 采用水平触发, nginx 采用边沿触发

信号驱动式IO模型

signal

程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕后再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。

信号类型

Linux系统共定义了64种信号,分为两大类:可靠信号与不可靠信号,前32种信号为不可靠信号,后32种为可靠信号

  • 不可靠信号: 也称为非实时信号,不支持排队,信号可能会丢失, 比如发送多次相同的信号, 进程只能收到一次. 信号值取值区间为1~31;
  • 可靠信号: 也称为实时信号,支持排队, 信号不会丢失, 发多少次, 就可以收到多少次. 信号值取值区间为32~64

信号表

在终端,可通过kill -l查看所有的signal信号

取值 名称 解释 默认动作
1 SIGHUP 挂起
2 SIGINT 中断
3 SIGQUIT 退出 不能被忽略、处理和阻塞
4 SIGILL 非法指令
5 SIGTRAP 断点或陷阱指令
6 SIGABRT abort发出的信号
7 SIGBUS 非法内存访问
8 SIGFPE 浮点异常
9 SIGKILL kill信号 不能被忽略、处理和阻塞
10 SIGUSR1 用户信号1
11 SIGSEGV 无效内存访问
12 SIGUSR2 用户信号2
13 SIGPIPE 管道破损,没有读端的管道写数据
14 SIGALRM alarm发出的信号
15 SIGTERM 终止信号
16 SIGSTKFLT 栈溢出
17 SIGCHLD 子进程退出 默认忽略
18 SIGCONT 进程继续
19 SIGSTOP 进程停止 不能被忽略、处理和阻塞
20 SIGTSTP 进程停止
21 SIGTTIN 进程停止,后台进程从终端读数据时
22 SIGTTOU 进程停止,后台进程想终端写数据时
23 SIGURG I/O有紧急数据到达当前进程 默认忽略
24 SIGXCPU 进程的CPU时间片到期
25 SIGXFSZ 文件大小的超出上限
26 SIGVTALRM 虚拟时钟超时
27 SIGPROF profile时钟超时
28 SIGWINCH 窗口大小改变 默认忽略
29 SIGIO I/O相关
30 SIGPWR 关机 默认忽略
31 SIGSYS 系统调用异常
对于signal信号,绝大部分的默认处理都是终止进程停止进程,或dump内核映像转储。 上述的31的信号为非实时信号,其他的信号32-64 都是实时信号

信号产生

硬件方式

  • 用户输入:比如在终端上按下组合键ctrl+C,产生SIGINT信号;
  • 硬件异常:CPU检测到内存非法访问等异常,通知内核生成相应信号,并发送给发生事件的进程;

软件方式

通过系统调用,发送signal信号:kill(),raise(),sigqueue(),alarm(),setitimer(),abort()

信号注册和注销

注册

在进程task_struct结构体中有一个未决信号的成员变量 struct sigpending pending。每个信号在进程中注册都会把信号值加入到进程的未决信号集。

  • 非实时信号发送给进程时,如果该信息已经在进程中注册过,不会再次注册,故信号会丢失
  • 实时信号发送给进程时,不管该信号是否在进程中注册过,都会再次注册。故信号不会丢失

注销

  • 非实时信号:不可重复注册,最多只有一个sigqueue结构;当该结构被释放后,把该信号从进程未决信号集中删除,则信号注销完毕;
  • 实时信号:可重复注册,可能存在多个sigqueue结构;当该信号的所有sigqueue处理完毕后,把该信号从进程未决信号集中删除,则信号注销完毕

信号处理

内核处理进程收到的signal是在当前进程的上下文,故进程必须是Running状态。当进程唤醒或者调度后获取CPU,则会从内核态转到用户态时检测是否有signal等待处理,处理完,进程会把相应的未决信号从链表中去掉

处理时机

signal信号处理时机: 内核态 -> signal信号处理 -> 用户态:

  • 在内核态,signal信号不起作用;
  • 在用户态,signal所有未被屏蔽的信号都处理完毕;
  • 当屏蔽信号,取消屏蔽时,会在下一次内核转用户态的过程中执行;

处理方式

进程对信号的处理方式: 有3种

  • 默认 接收到信号后按默认的行为处理该信号。 这是多数应用采取的处理方式。
SIG_DFL默认动作处理程序
signal(signum,SIG_DFL)
  • 忽略 接收到信号后不做任何反应
SIG_IGN忽略信号处理程序
signal(signum,SIG_IGN)
例: signal(SIGINT,SIG_IGN)
    signal(SIGPIPE,SIG_IGN)

SIG_DFL,SIG_IGN 分别表示无返回值的函数指针,指针值分别是0和1,这两个指针值逻辑上讲是实际程序中不可能出现的函数地址值。

  • 自定义 用自定义的信号处理函数来执行特定的动作

信号安装

进程处理某个信号前,需要先在进程中安装此信号。安装过程主要是建立信号值和进程对相应信息值的动作。

信号安装函数

  • signal():不支持信号传递信息,主要用于非实时信号安装;
  • sigaction():支持信号传递信息,可用于所有信号安装;

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

  • signum:要操作的signal信号。
  • act:设置对signal信号的新处理方式。
  • oldact:原来对信号的处理方式。
  • 返回值:0 表示成功,-1 表示有错误发生

sigaction结构体

struct sigaction {
    union
      {
	/* Used if SA_SIGINFO is not set.  */
	__sighandler_t sa_handler;
	/* Used if SA_SIGINFO is set.  */
	void (*sa_sigaction) (int, siginfo_t *, void *);
      }
        __sigaction_handler;
      # define sa_handler	__sigaction_handler.sa_handler
      # define sa_sigaction	__sigaction_handler.sa_sigaction
      #else
        __sighandler_t sa_handler;
      #endif

    sigset_t sa_mask;//信号集
    int sa_flags;
    void (*sa_restorer)(void);//已过时,POSIX不支持它,不应再被使用
  • sa_handler和sa_sigaction是信号处理函数,二者选其一(选择sa_handler和signal一样),选择sa_sigaction,设置sa_flags=SA_SIGINFO
    • void (*sa_sigaction) (int signo, siginfo_t *info, void *p);
      int signo:内核返回的信号值
      siginfo_t *info:内核返回当前信号的信息
      void *p:sigqueue发送信息会携带sigval联合体,p就指向这个联合体
  • sa_mask:定义阻塞信号集(屏弊的信号集合),指定信号处理程序执行过程中需要阻塞的信号
  • sa_flags:标示位
    • SA_RESTART:使被信号打断的syscall重新发起。
    • SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
    • SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到SIGCHLD信号,这时子进程如果退出也不会成为僵 尸进程。
    • SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
    • SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
    • SA_SIGINFO:使用sa_sigaction成员而不是sa_handler作为信号处理函数。
typedef struct
  {
    int si_signo;		/* Signal number.  */
#if __SI_ERRNO_THEN_CODE
    int si_errno;		/* If non-zero, an errno value associated with
				   this signal, as defined in <errno.h>.  */
    int si_code;		/* Signal code.  */
#else
    int si_code;
    int si_errno;
#endif
#if __WORDSIZE == 64
    int __pad0;			/* Explicit padding.  */
#endif

    union
      {
	int _pad[__SI_PAD_SIZE];

	 /* kill().  */
	struct
	  {
	    __pid_t si_pid;	/* Sending process ID.  */
	    __uid_t si_uid;	/* Real user ID of sending process.  */
	  } _kill;

	/* POSIX.1b timers.  */
	struct
	  {
	    int si_tid;		/* Timer ID.  */
	    int si_overrun;	/* Overrun count.  */
	    __sigval_t si_sigval;	/* Signal value.  */
	  } _timer;

	/* POSIX.1b signals.  */
	struct
	  {
	    __pid_t si_pid;	/* Sending process ID.  */
	    __uid_t si_uid;	/* Real user ID of sending process.  */
	    __sigval_t si_sigval;	/* Signal value.  */
	  } _rt;

	/* SIGCHLD.  */
	struct
	  {
	    __pid_t si_pid;	/* Which child.	 */
	    __uid_t si_uid;	/* Real user ID of sending process.  */
	    int si_status;	/* Exit value or signal.  */
	    __SI_CLOCK_T si_utime;
	    __SI_CLOCK_T si_stime;
	  } _sigchld;

	/* SIGILL, SIGFPE, SIGSEGV, SIGBUS.  */
	struct
	  {
	    void *si_addr;	    /* Faulting insn/memory ref.  */
	    __SI_SIGFAULT_ADDL
	    short int si_addr_lsb;  /* Valid LSB of the reported address.  */
	    union
	      {
		/* used when si_code=SEGV_BNDERR */
		struct
		  {
		    void *_lower;
		    void *_upper;
		  } _addr_bnd;
		/* used when si_code=SEGV_PKUERR */
		__uint32_t _pkey;
	      } _bounds;
	  } _sigfault;

	/* SIGPOLL.  */
	struct
	  {
	    __SI_BAND_TYPE si_band;	/* Band event for SIGPOLL.  */
	    int si_fd;
	  } _sigpoll;

	/* SIGSYS.  */
#if __SI_HAVE_SIGSYS
	struct
	  {
	    void *_call_addr;	/* Calling user insn.  */
	    int _syscall;	/* Triggering system call number.  */
	    unsigned int _arch; /* AUDIT_ARCH_* of syscall.  */
	  } _sigsys;
#endif
      } _sifields;
  } siginfo_t __SI_ALIGNMENT;


/* X/Open requires some more fields with fixed names.  */
#define si_pid		_sifields._kill.si_pid
#define si_uid		_sifields._kill.si_uid
#define si_timerid	_sifields._timer.si_tid
#define si_overrun	_sifields._timer.si_overrun
#define si_status	_sifields._sigchld.si_status
#define si_utime	_sifields._sigchld.si_utime
#define si_stime	_sifields._sigchld.si_stime
#define si_value	_sifields._rt.si_sigval
#define si_int		_sifields._rt.si_sigval.sival_int
#define si_ptr		_sifields._rt.si_sigval.sival_ptr
#define si_addr		_sifields._sigfault.si_addr
#define si_addr_lsb	_sifields._sigfault.si_addr_lsb
#define si_lower	_sifields._sigfault._bounds._addr_bnd._lower
#define si_upper	_sifields._sigfault._bounds._addr_bnd._upper
#define si_pkey		_sifields._sigfault._bounds._pkey
#define si_band		_sifields._sigpoll.si_band
#define si_fd		_sifields._sigpoll.si_fd
#if __SI_HAVE_SIGSYS
# define si_call_addr	_sifields._sigsys._call_addr
# define si_syscall	_sifields._sigsys._syscall
# define si_arch	_sifields._sigsys._arch
#endif

请留意两个用于传输数据的变量:
sigval_t si_value; /* Signal value /
int si_int; / POSIX.1b signal */
实际上:

siginfo_t 结构体中(sigval_t) si_value就是sigqueue函数中传入的第三个参数sigval
siginfo_t 结构体中(int) si_int就是从sigqueue函数中传入的第三个参数sigval.sival_int中获得
siginfo_t 结构体中si_ptr就是从sigqueue函数中传入的第三个参数sigval.sival_ptr中获得
所以可以通过sigqueue种对sigval值的传入来实现信号带值,在信号处理函数中void sa_sigaction(int num,siginfo_t *info,void *d)读取第二个参数中对应变量的值来获取信号所带参数,通过这种方法实现进程间通讯

  • sigaction-sa_handler使用
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void myHandler(int sig);

int main(int argc,char *argv[])
{
    struct sigaction act,oact;

    act.sa_handler=myHandler;
    sigemptyset(&act.sa_mask);
    act.sa_flags=0;
    sigaction(SIGINT,&act,&oact);
    sigaddset(&act.sa_mask,SIGINT);
    while (1)
    {
        printf("hello\n");
        pause();//信号会打断阻塞
    }
    return 0;
}
void myHandler(int sig)
{
    printf("i got signal: %d\n",sig);
}
    
  • sigaction-sa_sigaction使用
#include <stdio.h>
#include<stdlib.h>
#include<signal.h>
#include<unistd.h>

void sa_sigaction_func(int signo,siginfo_t *info,void *p)
{
    printf("signo=%d\n",signo);
    printf("sender pid=%d\n",info->si_pid);
    printf("si_errno=%d\n",info->si_errno);
    printf("si_code=%d\n",info->si_code);
    printf("si_uid=%d\n",info->si_pid);
    printf("si_status=%d\n",info->si_status);
    printf("si_value=%d\n",info->_sifields._rt.si_sigval.sival_int);
    printf("si_fd=%d\n",info->si_fd);
}
int main(int argc,char *argv[])
{
    struct sigaction act,oact;

    sigemptyset(&act.sa_mask);
    act.sa_flags=SA_SIGINFO;
    act.sa_sigaction=sa_sigaction_func;
    sigaddset(&act.sa_mask,10);
    sigaction(SIGUSR1,&act,&oact);
    while (1)
    {
        printf("pid is %d Hello\n",getpid());
        pause();
    }
}
信号集操作函数
  • sigemptyset(sigset_t *set):信号集全部清0;
  • sigfillset(sigset_t *set): 信号集全部置1,则信号集包含linux支持的64种信号;
  • sigaddset(sigset_t *set, int signum):向信号集中加入signum信号;
  • sigdelset(sigset_t *set, int signum):向信号集中删除signum信号;
  • sigismember(const sigset_t *set, int signum):判定信号signum是否存在信号集中
信号阻塞屏蔽函数
  • sigprocmask(int how, const sigset_t *set, sigset_t *oldset)); 不同how参数,实现不同功能
    • SIG_BLOCK:将set指向信号集中的信号,添加到进程阻塞信号集;
    • SIG_UNBLOCK:将set指向信号集中的信号,从进程阻塞信号集删除;
    • SIG_SETMASK:将set指向信号集中的信号,设置成进程屏蔽信号集;
  • sigpending(sigset_t *set)):(未决信号)获取已发送到进程,却被屏蔽的所有信号;
  • sigsuspend(const sigset_t *mask)):用mask代替进程的原有掩码,并暂停进程执行,直到收到信号再恢复原有掩码并继续执行进程。

信号发送

  • kill():用于向进程或进程组发送信号;
  • sigqueue():只能向一个进程发送信号,不能像进程组发送信号;主要针对实时信号提出,与sigaction()组合使用,当然也支持非实时信号的发送;
 #include <signal.h>
       int sigqueue(pid_t pid, int sig, const union sigval value);

参数:
pid:是指定接收信号的进程id,
sig:确定即将发送的信号,
sigval value:是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。

返回值成功返回0,失败返回-1

sigval

union sigval {
   int   sival_int;
   void *sival_ptr;
};

siginfo_t结构中si_value要么持有一个4字节的整数值,要么持有一个指针,这就构成了与信号相关的数据

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>

int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        printf("arguments error");
        exit(0);
    }
    pid_t pid=atoi(argv[1]);
    union sigval v;
    v.sival_int=222;
    //发送SIGUSR1
    sigqueue(pid,SIGUSR1,v);
    return 0;
}
  • alarm():用于调用进程指定时间后发出SIGALARM信号;
  • ualarm(1,1);
  • setitimer():设置定时器,计时达到后给进程发送SIGALRM信号,功能比alarm更强大;
  • abort():向进程发送SIGABORT信号,默认进程会异常退出。
  • raise():用于向进程自身发送信号;
  • pause(void):将调用进程挂起直至捕捉到信号为止。这个函数通常用于判断信号是否已到

信号集

数据类型sigset_t来存储,sigset_t称为信号集
sigset_t结构体(信号集)

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))  //16
typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];//数组长度16,一个unsigned long int是8位,共128位
} __sigset_t;

信号集是以位图呈现

  • 信号递达
    实际执行信号的处理动作称为信号递达(Delivery)
  • 未决信号
    信号从产生到递达之间的状态,称为信号未决(Pending)
    把信号设置成为屏蔽,在向这个进程发送屏蔽的信号就会成为未决信号
    设置为屏蔽信号到解除屏蔽号,这段时间内不管接收几次最后解除屏蔽信号只会处理一次未决信号
#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>

void handler(int sig){
    printf("get a sig,num is:%d\n",sig);
}
void print_sig(sigset_t *p)
{
    int i=1;
    for(;i<32;++i){
        if(sigismember(p,i)){
            printf("1");
        }else{
            printf("0");
        }
    }
    printf("\n");
}
int main(){
    signal(2,handler);

    sigset_t s,p,o;
    sigemptyset(&s);
    sigemptyset(&o);
    sigaddset(&s,SIGINT);

    sigprocmask(SIG_SETMASK,&s,&o);//信号集s中的SIGINT设置为屏蔽

    int count=0;

    while (1)
    {
        sigemptyset(&p);
        sigpending(&p);//当前进程已收到的未决信号(屏蔽的信号),解决屏蔽时会执行一次P中的未决信号
        print_sig(&p);
        sleep(1);
        if (count++==10)
        {
            sigprocmask(SIG_SETMASK,&o,NULL);//将之前的屏蔽信号集设置回去,并执行p未决集中的信号
            printf("recover block\n");
            sleep(3);
            count=0;
        }
    }
}
  • 信号阻塞
    进程可以选择阻塞(Block)某个信号,SIGKILL 和 SIGSTOP 不能被阻塞。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志

未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略

定时器

两类定时器

  • 实时:ITIMER_REAL,计时到达后发送SIGALRM信号
  • 虚拟:ITIMER_VIRTUAL,计时到达后发送SIGVTALRM信号
#include <stdio.h>
#include <sys/time.h>
#include <unistd.h>
#include <signal.h>

void sigroutine(int signo){
    switch (signo)
    {
    case SIGALRM:
        printf("Catch a signal -- SIGALRM\n");
        break;
    
    case SIGVTALRM:
        printf("Catch a signal -- SIGVTALRM\n");
        break;
    }
}
int main(){
    struct itimerval value,ovalue,value2;

    printf("process id is %d\n",getpid());
    //自定义处理信号
    signal(SIGALRM,sigroutine);
    signal(SIGVTALRM,sigroutine);

   value.it_interval.tv_sec=1;//设置秒,第一次计时到达秒数
   value.it_interval.tv_usec=0;//设置徽秒
   value.it_value.tv_sec=1; //间隔触发秒数
   value.it_value.tv_usec=0;
   setitimer(ITIMER_REAL,&value,&ovalue);//第三个参数返回已存在的定时器值

   value2.it_value.tv_sec=0;
   value2.it_value.tv_usec=500000;
   value2.it_interval.tv_sec=0;
   value2.it_interval.tv_usec=500000;
   setitimer(ITIMER_VIRTUAL,&value2,&ovalue);

   for(;;);
}

**信号会打断阻塞函数**

可重入函数

阻塞函数被信号打断,阻塞函数返回值为-1,这时需要判断全局错误码,如果errno==EINTR,则重入被打断的阻塞函数

...
ACCEPT:
    confd=accept(srvfd,(struct sockaddr*)(&tCliaddr),&tCliaddlen);
    if(confd==-1){
      if(errno==EINTR){
        goto ACCEPT;  
      }else{
        fprintf(stderr,"accept error");
        return -1;
      }
    }
...

应用场景

超时响应

posted @ 2021-09-21 21:25  wangzhilei  阅读(131)  评论(0编辑  收藏  举报