并发之信号

大纲

  • 信号的概念
  • signal
  • 信号的不可靠
  • 可重入函数
  • 信号的响应过程
    • 没收到信号
    • 收到信号
    • SIG_IGN原理
    • 标准信号为什么会丢失
    • 不能从信号处理函数中随意的往外跳
  • 信号常用函数
    • kill
    • raise
    • alarm
      • 小实现
    • pause
      • 结合alarm和pause实现类似漏桶的程序
    • setitimer
      • 使用样例
    • abort
    • system
  • 信号集
  • 信号屏蔽字
    • 举例
    • sigprocmask使用样例
  • 扩展
    • sigsuspend
    • sigaction
  • 实时信号
  • 综合令牌桶实现

同步,就是调用某个东西是,调用方得等待这个调用返回结果才能继续往后执行。
异步,和同步相反 调用方不会立即得到结果,而是在调用发出后调用者可以继续执行后续操作。
可以通过查询法或者通知法处理异步。查询法就是不停的看异步事件有没有处理好,适合异步事件发生频率较高时,而通知法是异步事件结束会进行通知,适合异步发生频率较低的情况。

信号的概念

信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。
举个信号的例子:

  1. 用户输入命令,在Shell下启动一个前台进程。
  2. 用户按下Ctrl-C,这个键盘输入产生一个硬件中断。
  3. 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用
    户态 切换到内核态处理硬件中断。
  4. 终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了
    一 个SIGINT信号给该进程)。
  5. 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,需先处理PCB中记
    录的信号,发现有一个SIGINT信号待处理,用这个信号的默认处理动作是终止进程,所
    以直接终止进程而不再返回它的用户空间代码执行。

前台进程在运行过程中用户随时可能按下Ctrl-C键产生一个信号,也就是说该进 程的用户空间代码执行到任何地值都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流 程来说是异步(Asynchronous)的。

signal

有个signal后,就等于是给程序规定了一个动作,执行这个动作的条件是:1. 程序没结束 2. 信号到来

//typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//常用下面这种形式
void (*signal(int signum, void (*func)(int)))(int)

第一个参数指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。 第二个参数是个函数的地址,描述了与信号关联的动作,它可以取以下三种值:

  1. SIG_IGN
    忽略这个信号
//在终端每秒打印一个*
int main()
{
        signal(SIGINT, SIG_IGN);
        //SIGINT是按中断键产生的信号,一般是ctrl+c
        //现在忽略这个信号
        for (int i = 0; i < 10; i++)
        {
                write(1, "*", 1);
                sleep(1);
        }
        exit(0);
}
  1. SIG_DFL
    恢复对信号的系统默认处理。不写此处理函数默认执行系统默认操作
  2. 返回值为void,参数为int类型函数的指针
static void int_handler(int s)
{
        write(1, "!", 1);
}
int main()
{
        signal(SIGINT, &int_handler);
        //现在每按下中断键就输出一个感叹号
        for (int i = 0; i < 10; i++)
        {
                write(1, "*", 1);
                sleep(1);
        }
        exit(0);
}

上面第三点的示范代码在运行中如果按着ctrl+c不放,将会非常快的完成进程,这是因为信号会打断阻塞的系统调用。比如open或者write时设备正忙而进入阻塞态,此时有个信号进来就会将这个系统调用打断。被信号打断并不是真的出现错误,还是可以继续打断之前的行为的。因此,很多程序需要这样改写:

while ((len = read(fds, buf, BUFSIZE)) < 0)
{
	if (errno == EINTR) //EINTR表示是被打断的错误
		continue; //如果是被打断,则继续执行read
	perror("read()");
	exit(1);
}

信号的不可靠

现在Linux 在SIGRTMIN实时信号之前的都叫不可靠信号,这里的不可靠主要是不支持信号队列,就是当多个信号发生在进程中的时候(收到信号的速度超过进程处理的速度的时候),这些没来的及处理的信号就会被丢掉,仅仅留下一个信号。可靠信号是多个信号发送到进程的时候(收到信号的速度超过进程处理信号的速度的时候),这些没来的及处理的信号就会排入进程的队列。等进程有机会来处理的时候,依次再处理,信号不丢失。
但是信号行为也存在不可靠,比如之前代码中signal(SIGINT, &int_handler);这段代码并不是人为调用int_handler这个函数,我们只是指定了信号到来时调用这个函数的动作而已。调用这个函数时的执行现场是内核布置的,如果信号在处理这个行为时又来了一个同样的信号,那么第二次的执行现场就会冲掉第一次的。早期的UNIX在第一次执行完自定义的信号动作后会将该信号还原为默认的动作来处理这种情况。

可重入函数

可重入函数就是第一次调用还没结束就发生第二次调用,但并不会出错,用于解决上面的信号不可靠问题。
所有的系统调用都是可重入的,一部分库函数也可重入,比如memcpy等还有名称_r结尾的函数

信号的响应过程

信号从收到到响应有一个不可避免的延迟,这个延迟怎么来的呢?
首先内核为每一个进程维护了两个位图:

  • mask(信号屏蔽字,一般32位):当前信号的状态,初始为1,信号行为执行完为0
  • pending(信号标志位,一般32位):初始为0 ,信号来了为1

先看两种中断处理的流程

没收到信号

时间片到程序会正常中断,此时会保存进程环境,其中肯定会包括一个地址address指向被打断的位置。重新调度到自己的时候将会切换到用户态,此时会进行mask & pending的运算,因为现在是没收到信号的情况,所以运算结果就全是0,也就是没有收到信号,接着就根据address回到中断的位置

收到信号

如果收到信号,pending的某个位就会被置1,但是进程还不知道这个信号是否存在!只有当程序中断->保存进程环境->在就绪队列中等待被调度->被调度到->切换到用户态->进行mask & pending运算后发现有一位是1才知道有信号存在,根据1的位置进行对照就能知道是哪种信号,然后执行signal函数规定信号对应的动作。这就解释了为什么信号从收到到响应有不可避免的时延的问题,因为如果中断一直不来,那么我就一直不知道有没有收到信号,这也解释了为什么信号是依赖中断的。
在收到信号后,会将信号对应的mask位和pending位置0,中断时保存的指向中断位置的地址address会替换为signal的第二个参数,也就是自定义行为的函数的地址,然后执行这个函数。执行完后会再切换到内核态,将address重新替换回中断位置,再将信号对应的mask位置回1。再从内核态切换回用户态会再进行mask & pending运算查看有没有信号,没有就回到中断处

SIG_IGN原理

看过上面两个就很好理解SIG_IGN是怎么忽视一个标准信号的了。很简单,将这个信号对应的mask位置成0就行了…这样mask & pending哪怕pending是1结果也是0

标准信号为什么会丢失

这个其实也好理解,因为mask和pending都是位图,pending被置1可能是被一个信号置的,也可能是被一百个相同信号置的。比如在处理完一个信号后,对应mask和pending都置0,此时又来了100个相同的信号,pending又被置1了。在一系列操作后内核态切换回用户态执行相与运算,发现又是这个地方有信号,所以又执行了一遍,但是后面的99遍都丢失了。

不能从信号处理函数中随意的往外跳

之前讲过用setjmp和longjmp可以从一个函数跳到另一个函数,但是轻易不要在信号处理函数中做这种事。我们知道信号处理函数结束后会切换到内核态,然后将信号对应的mask置为1,如果在信号处理函数中跳到另一个函数会略过这一过程!也就是该信号mask位就一直变成了0,这将导致以后一直屏蔽这个信号。

可以使用sigsetjmp和siglongjmp解决这一问题。如果savesigs非0,则sigsetjmp在env中保存进程的当前信号屏蔽字,在调用siglongjmp会从其中恢复保存的信号屏蔽字

int sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjmp_buf env, int val);

信号常用函数

kill

用于给进程发送一个信号

int kill(pid_t pid, int sig);

如果pid是个正数,则将信号发给pid这个进程
如果pid是0,则给本进程同组的所有进程发信号
当pid是-1,则给当前进程有权限发送信号的进程都发送信号(除了init)
当pid小于-1,则将当前信号发送给进程组id(pgid)是pid绝对值的所有进程(也就是组内所有进程)
如果sig是0,就不会发送信号,但是系统会执行错误检查,通常利用这一点来检验pid进程或进程组是否存在

如果成功(至少发送了一个信号)则返回0,否则返回-1,然后设置errno
errno有三种:

  • EINVAL
    指定了无效的信号
  • EPERM
    无权对目标进程或进程组发送信号
  • ESRCH
    目标进程或进程组不存在

raise

给当前进程(也就是自己)或者线程发送信号

int raise(int sig);

//给进程发送信号功能类似于
kill(getpid(), sig) //getpid得到自己进程号
//在多线程中类似于
pthread_kill(pthread_self(), sig);

alarm

在经过参数seconds秒数后向当前进程发送信号SIGALRM,如果未设置信号SIGALARM的处理函数,那么alarm()默认处理终止进程。

unsigned int alarm(unsigned int seconds);

使用样例:

int main()
{
	//signal(SIGALRM, sig_handler);
	//不设置处理函数,现在默认终止进程
	alarm(5);
	while (1);
	exit(0);
}

上面的程序在五秒后进程终止

如果在seconds秒内再次调用了alarm函数设置了新的闹钟,则后面定时器的设置将覆盖前面的设置(这也是为什么不推荐使用sleep函数的原因,因为有的sleep函数是通过alarm+sleep封装的,如果在sleep后再用alarm将会覆盖旧的时间)

小实现

static int loop = 1;
static void alrm_handler(int s)
{
	loop = 0;
}
int main()
{
	int64_t cnt = 0;

	signal(SIGALRM, alrm_handler);
	alarm(5);

	while (loop)
    	cnt++;

	printf("%d\n", cnt);
	exit(0);
}

这段代码可以得到在发出信号后cnt一共自增了多少。但是这段代码看似没有问题,如果在gcc编译时时加上选项-O1进行优化,代码会进入死循环。原因是优化会调整代码的逻辑。在while中循环条件是loop,而循环体没有用到loop,编译器就认为loop是不变的(一直从寄存器取loop的值,而不是从loop地址)…所以需要给loop加上volatile修饰

static volatile int loop = 1;

了解volatile

pause

调用该函数(系统调用)的进程将处于阻塞状态(主动放弃cpu),直到接收到信号且信号函数成功返回 后pause函数才会返回

int pause(void);

如果信号的默认处理动作是终止进程,则进程终止,pause函数没有机会返回。
如果信号的默认处理动作是忽略,进程继续处于挂起状态,pause函数不返回。
如果信号的处理动作是捕捉,则【调用完信号处理函数之后,pause返回-1】,errno设置为EINTR,进程被唤醒继续执行后面的程序

结合alarm和pause实现类似漏桶的程序

实现一个mycat,但是mycat是10个字节10个字节的显示一个文本

#define CPS     10
#define BUFSIZE CPS
//漏桶版
static volatile int loop = 0;
static void alrm_handler(int s)
{
	alarm(1);
	//每次收到信号后再在1秒之后发送信号
	//可以做到持续发送这个信号
	loop = 1;
}
/*令牌桶版
static volatile int token = 0;
static void alrm_handler(int s)
{
	alarm(1);
    token++;
    if (topken > BURST)
    	token = BURST; 维持一个上限
}
*/
int main(int argc, char *argv[])
{
	if (argc < 2)
	{
		fprintf(stderr, "Usage: %s  ", argv[0]);
		exit(1);
	}
	signal(SIGALRM, alrm_handler);
	alarm(1); //一秒钟之后发出一个SIGALRM信号
	int fds = open(argv[1], O_RDONLY), fdd = 1;
	if (fds < 0)
	{
		perror("open()");
		exit(1);
	}
	char buf[BUFSIZE];
	int pos;
	long int len = 0L, ret = 0L;
	while (1)
	{
		//漏桶版
		while (loop == 0) //loop等于0则挂起然后等待alarm的信号
			pause();
		//loop重新设0,等下面处理完后再挂起
		loop = 0;
		
		/*令牌桶版
		while (token <= 0)
        	pause();
        token--;
        */
        
		while ((len = read(fds, buf, BUFSIZE)) < 0)
		{
			if (errno == EINTR)
				continue;
			perror("read()");
			exit(1);
		}
		if (len == 0)
			break;
		pos = 0;
		while (len > 0)
		{
			ret = write(fdd, buf + pos, len);
			if (ret < 0)
			{
				perror("write()");
				exit(1);
			}
			pos += ret;
			len -= ret;
		}
	}

	close(fds);
	exit(0);
}

漏桶版就是固定一秒输出10个字符,但是假设读取的文件中没有数据,然后阻塞了三秒,漏桶版在这里就会浪费了三秒时间,如果是令牌桶则会攒下这三秒,之后等有数据了马上一次性的执行三次读写,每次读写10个字节,等于是把欠的三秒补上。
但是漏桶版这段代码有个非常隐蔽的问题:

while (token <= 0)
	pause();
token--;

第一个问题是,如果在while判断和pause中间来了个信号怎么办?while判断token确实满足小于等于0,在即将执行pause时来了信号,执行了token++,在token大于0的情况被pause挂起了…其实这个问题在本程序中还不是什么问题,别忘了我们写的就是“令牌桶”,专门处理“欠债情况”。在token等于1的时候被挂起,之后来了信号token又变成2,此时就会一次性的执行两次读写。
真正的问题出在token–,这个代码很有可能是由多条机器指令完成的,要先取token的值放入内存然后执行减一操作,最后放回去,如果在第一第二步中间来了两个信号,token变成2了,此时就会出现内存中执行了减1操作的token副本覆盖了原本的2。总结一下就是token–这个操作不原子。

setitimer

alarm只能精确到秒,setitimer可以更精确
在linux下如果定时如果要求不太精确的话,使用alarm()和signal()就行了(精确到秒),但是如果想要实现精度较高的定时功能的话,就要使用setitimer函数。

int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
				struct itimerval *old_value);

which指的是要设置哪种时钟,有以下三种:

  • ITIMER_REAL
    按照系统真实时间倒计时,倒计时到0发出SIGALRM
  • ITIMER_VIRTUAL
    根据进程在用户态消耗CPU时间倒计时,倒计时到0时发送SIGVTALRM信号
  • ITIMER_PROF
    以该进程在用户态下和内核态下所费的时间来计算,最后发送SIGPROF信号

第二个参数指定间隔时间,第三个参数可以保存指定间隔时间之前的间隔时间。

struct itimerval {
	struct timeval it_interval; //计时间隔
    struct timeval it_value; //延时时长
};
struct timeval {
	time_t      tv_sec; //秒
	suseconds_t tv_usec; //微秒
};

settimer工作机制是,先对it_value倒计时,当it_value为零时触发信号,然后it_interval会原子的赋给it_value。继续对it_value倒计时,一直这样循环下去。可以看到本身就能构成一条链,不用像alarm一样在信号处理函数中还要调用alarm来延续信号。

set成功返回0,否则返回-1

使用样例

写一个程序,第一次在一秒后打印一个点,之后都间隔5秒打印一个点

static void alrm_handler(int s)
{
	write(1, ".", 1);
}
int main()
{
	signal(SIGALRM, alrm_handler);

    struct itimerval itv, otv;
    itv.it_value.tv_sec = 1;
    itv.it_value.tv_usec = 0;
    itv.it_interval.tv_sec = 5;
    itv.it_interval.tv_usec = 0;
    setitimer(ITIMER_REAL, &itv, &otv);

    while (1)
    	pause();

    exit(0);
}

abort

给当前进程发送SIGABRT信号,然后终止进程

void abort(void);

system

调用shell来完成command指令

int system(const char *command);

在有信号存在的进程中想使用这个函数就需要阻塞SIGCHLD,并且忽略SIGINT和SIGQUIT

信号集

sigset_t是个信号集类型,信号集保存有0或多个信号

//把一个信号集清空
int sigemptyset(sigset_t *set);
//把某个信号集置为全集(包含所有信号)
int sigfillset(sigset_t *set);
//往一个信号集添加一个信号
int sigaddset(sigset_t *set, int signum);
//在一个集合中删掉一个信号
int sigdelset(sigset_t *set, int signum);
//判断某个信号signum是否在集合set中
int sigismember(const sigset_t *set, int signum);

信号屏蔽字

之前讲过信号的响应过程,其中就有个信号屏蔽字,这个信号屏蔽字可以由下面这个函数来操控mask

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

第一个参数用于指定信号修改的方式,有以下三种:

  • SIG_BLOCK
    将set中的信号都附加到当前进程的阻塞表(信号屏蔽字)中(相当于在mask中对应信号位都置为0),附加之前的内容放入oldset中
  • SIG_UNBLOCK
    从阻塞表中删除set中的信号,删除之前的内容放入oldset中
  • SIG_SETMASK
    替换当前阻塞表中内容为set中的内容,保存替换前的内容放入oldset中

举例

假设现在阻塞的信号表是{SIGSEGV,SIGSUSP},我们执行了以下代码:

sigset_t x, y;
sigemtyset(&x);
sigemtyset(&y);
//以上进行清空初始化
sigaddset(&x, SIGUSR1); //把信号SIGUSR1放入x
sigprocmask(SIG_BLOCK, &x, &y);

现在新的阻塞表是{SIGSEGV,SIGSUSP,SIGUSR1},y中保存的是{SIGSEGV,SIGSUSP}
如果我接着执行sigprocmask(SIG_UNBLOCK, &x, NULL),把x中的内容从阻塞表中去除
则新的阻塞表是{SIGSEGV, SIGSUSP}
如果我接着执行sigprocmask(SIG_SETMASK, &x, NULL),把阻塞表替换为x内容
则新的阻塞表是{SIGUSR1}

sigprocmask使用样例

写一个程序一秒输出一个*,输出5个就换行,并且每行输出过程中不能被ctrl+c打断,只能在换行的过程中被打断。

static void int_handler(int s)
{
	write(1, "!", 1);
}
int main()
{
	sigset_t set, oldset;
	signal(SIGINT, &int_handler);
	sigemptyset(&set);
	sigaddset(&set, SIGINT);
	//添加ctrl+c打断到信号集中

	for (int i = 0; i < 1000; i++)
    {
    	sigprocmask(SIG_BLOCK, &set, &oldset);
    	//将set中的信号附加到信号屏蔽字中,之前的内容放入oldset
        for (int j = 0; j < 5; j++)
        {
        	write(1, "*", 1);
            sleep(1);
        }
        write(1, "\n", 1);
        sigprocmask(SIG_SETMASK, &oldset, NULL);
        //复原成oldset中的内容
    }
    exit(0);
}
//输出:
**^C^C^C^C*^C^C^C**
!*****^C
!*****
*****

这里第一行ctrl+c了好几次,但是只输出了一个!是为什么呢?详情看信号为什么会丢失
总结一下sigprocmask不能决定信号什么时候来,但是可以决定信号什么时候被响应

扩展

sigsuspend

先看之前写的每行输出5个*,然后只有换行期间才能被打断的程序。稍作修改,让这个函数每输出一行之后等待用户的信号,信号到来才继续输出,成为一个信号驱动程序。其实很简单,加个pause就行,执行到pause后挂起,用于键入ctrl+c就能继续循环:

static void int_handler(int s)
{
	write(1, "!", 1);
}
int main()
{
	sigset_t set, oldset;
	signal(SIGINT, &int_handler);
	sigemptyset(&set);
	sigaddset(&set, SIGINT);

	for (int i = 0; i < 1000; i++)
    {
    	sigprocmask(SIG_BLOCK, &set, &oldset);
        for (int j = 0; j < 5; j++)
        {
        	write(1, "*", 1);
            sleep(1);
        }
        write(1, "\n", 1);
        sigprocmask(SIG_SETMASK, &oldset, NULL);
        pause();
    }
    exit(0);
}

但是这里出现了问题。如果在第一行输出的过程中键入ctrl+c,会被sigprocmask屏蔽。接着执行完sigprocmask(SIG_SETMASK, &oldset, NULL);后会开始处理这个信号,照理说这里pause中处理这个信号会输出!后接着输出一行,可是并没有!因为并不是在pause期间处理这个信号的,而是在sigprocmask和pause之间!也就是说处理完信号才pause的!换句话说,sigprocmask和pause不原子
这里就需要sigsuspend函数

int sigsuspend(const sigset_t *mask);
//信号屏蔽字设置成mask

sigsuspend函数的作用就是解除一个信号的阻塞后立即进入等待信号的状态,中间没有间隔。

sigprocmask(SIG_SETMASK, &oldset, NULL);
pause();
替换成
sigsuspend(&oldset)

sigaction

再来看个问题,假设有下面这处理函数,然后有三个信号都是用这个函数:

void sig_handler(int s)
{
	fclose(fp);
}
//...
signal(SIGINT, sig_handler);
signal(SIGQUIT, sig_handler);
signal(SIGTERM, sig_handler);

先说明一点:信号是可以嵌套调用的。如果有一个SIGINT到来,此时进行sig_handler,还没调用fclose时又来了个SIGQUIT,此时会像递归调用一样,先执行SIGQUIT的处理函数。问题出现了,SIGQUIT先于SIGINT执行了fclose(fp),接着SIGINT又执行了一次fclose(fp),同一个文件被关了两次,出现了内存泄漏问题。
这就是signal函数的缺陷,多个信号共用同一个处理函数时,响应某一个信号期间不能屏蔽其他信号。然后可以想到是用sigprocmask来解决,但是非常非常麻烦。所以还是得sigaction登场

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

对signum信号定义新的行为act,同时保存旧的行为到oldact。以下是sigaction结构体

struct sigaction {
	void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};

sa_handler就是信号处理函数,而sa_mask就是我们需要的,在响应某个信号的同时需要屏蔽的信号都存在该集合中。众所周知在响应某个信号的处理函数时,自己信号的mask也是置0的,所以sa_mask内容其实是算上自己还需要屏蔽哪些信号。

//利用sigaction进行改写
void sig_handler(int s)
{
	fclose(fp);
}
//...
struct sigaction sa;
sa.sa_handler = sig_handler;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGINT);
sigaddset(&sa.sa_mask, SIGQUIT);
sigaddset(&sa.sa_mask, SIGTERM);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGQUIT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);

signal函数还有个问题就是不能辨别信号是由谁发出的,如果一个信号是在用户态发出来的,那有可能会导致问题。sigaction可以解决,利用的就是sigaction结构体中三个参数的成员void (*sa_sigaction)(int, siginfo_t *, void *);。参数中int还是signum,第二个参数siginfo_t是关于信号所有属性信息的结构体,其中就包含一个成员si_code,记录了信号的来源
以后就用sigaction代替signal,用setitimer代替alarm

实时信号

实时信号和标准信号的区别是:

  1. 实时信号的多个实例可以被入队(译注:进入递送(即deliver)的queue)。相反,当一个标准信号被block的时候,如果它被递送多次,那么只有一个实例会入队。
  2. 实时信号被递送的顺序是被确定的。同一种类型的实时信号的多个实例被递送时的顺序和它们被发送的顺序一致。如果不同的实时信号被发送给同一个进程,那么信号数字越小的信号就会被越先递送,即小数字的实时信号有更高的优先级。相反,如果多个标准信号pending在一个进程上时,它们被递送的顺序是未定义的(译注:不同的系统可能有不同的实现,Linux也只是一种系统,其他还有各种Unix系统)。

综合令牌桶实现

现在要求可以指定不同流速的令牌桶,就是有的令牌桶一下输出十个字符,有的一下输出二十个。显然要将令牌桶封装成一个结构体,然后放进一个数组中。

//mytbf.h
#ifndef MYTBF_H_
#define MYTBF_H_
#define MYTBF_MAX       1024

typedef void mytbf_t;
mytbf_t *mytbf_init(int cps, int burst);
int mytbf_fetchtoken(mytbf_t *, int);
int mytbf_returntoken(mytbf_t *, int);
int mytbf_destroy(mytbf_t *);

#endif
//main.c
#define CPS     10
#define BUFSIZE CPS
#define BURST   100
int main(int argc, char *argv[])
{
	/*if (argc < 2)
	{
		fprintf(stderr, "Usage: %s  ", argv[0]);
		exit(1);
	}*/
	mytbf_t *tbf = mytbf_init(CPS, BURST);
	/*if (tbf == NULL)
	{
		fprintf(stderr, "mytbf_init() failed\n");
		exit(1);
	}*/
	int fds = open(argv[1], O_RDONLY), fdd = 1;
	/*if (fds < 0)
	{
		perror("open()");
		exit(1);
	}*/
	char buf[BUFSIZE];
	int pos;
	long int len = 0L, ret = 0L;
	while (1)
	{
		int get = mytbf_fetchtoken(tbf, BUFSIZE);
		//从tbf中最多要BUFSIZE个令牌,不够则有多少取多少
		
		/*if (get < 0)
		{
			fprintf(stderr, "mytbf_fetchtoken(): %s\n", strerror(-get));
			exit(1);
		}
		while ((len = read(fds, buf, get)) < 0)
		{
			if (errno == EINTR)
				continue;
			perror("read()");
			exit(1);
		}
		if (len == 0)
			break;*/
		
		if (get - len > 0)
			mytbf_returntoken(tbf, get - len);
		pos = 0;
		while (len > 0)
		{
			ret = write(fdd, buf + pos, len);
			if (ret < 0)
			{
				perror("write()");
				exit(1);
			}
			pos += ret;
			len -= ret;
		}
	}
	close(fds);
	mytbf_destroy(tbf);
	exit(0);
}
//mytbf.c
#include "mytbf.h"
static int inited = 0;
struct mytbf_st
{
	int cps;
	int burst;
	int token;
	int pos;
};
static struct mytbf_st *job[MYTBF_MAX];
static struct sigaction alrm_save;
void alrm_action(int s, siginfo_t *infop, void *unused)
{
	if (infop->si_code != SI_KERNEL)
		return;
	for (int i = 0; i < MYTBF_MAX; i++)
	{
		if (job[i] != NULL)
		{
			job[i]->token += job[i]->cps; //现在1token对应1字符
			if (job[i]->token > job[i]->burst)
				job[i]->token = job[i]->burst;
		}
	}
}
static void module_unload()
{
	sigaction(SIGALRM, &alrm_save, NULL);
	struct itimerval itv;
	itv.it_interval.tv_sec = 0;
	itv.it_interval.tv_usec = 0;
	itv.it_value.tv_sec = 0;
	itv.it_value.tv_usec = 0;
	setitimer(ITIMER_REAL, &itv, NULL);

	for (int i = 0; i < MYTBF_MAX; i++)
		free(job[i]);
}
static void module_load()
{
	//使用sigaction代替signal
	struct sigaction sa;
	sa.sa_sigaction = alrm_action;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_SIGINFO;
	sigaction(SIGALRM, &sa, &alrm_save);
	//使用setitimer代替alarm
	struct itimerval itv;
	itv.it_interval.tv_sec = 1;
	itv.it_interval.tv_usec = 0;
	itv.it_value.tv_sec = 1;
	itv.it_value.tv_usec = 0;
	setitimer(ITIMER_REAL, &itv, NULL);
	//钩子函数,最后释放
	atexit(module_unload);
}
static int get_free_pos()
{
	for (int i = 0; i < MYTBF_MAX; i++)
	{
		if (job[i] == NULL)
			return i;
	}
	return -1;
}
static int min(int a, int b) { return a > b ? b : a; }
mytbf_t *mytbf_init(int cps, int burst)
{
	struct mytbf_st *me;
	if (!inited)
	{
		module_load();
		inited = 1;
	}
	me = malloc(sizeof(*me));
	/*if (me == NULL)
		return NULL;*/
	int pos = get_free_pos();
	/*if (pos < 0)
		return NULL;*/
	me->token = 0;
	me->cps = cps;
	me->burst = burst;
	me->pos = pos;

	job[pos] = me;
	return me;
}
int mytbf_fetchtoken(mytbf_t *ptr, int size)
{
	if (size <= 0)
		return -EINVAL;

	struct mytbf_st *me = ptr;
	while (me->token <= 0)
		pause();
	int get = min(me->token, size);
	me->token -= get;
	return get;
}
int mytbf_returntoken(mytbf_t *ptr, int size)
{
	if (size <= 0)
		return -EINVAL;
	struct mytbf_st *me = ptr;
	me->token += size;
	if (me->token > me->burst)
		me->token = me->burst;

	return size;
}
int mytbf_destroy(mytbf_t *ptr)
{
	struct mytbf_st *me = ptr;

	job[me->pos] = NULL;
	free(ptr);

	return 0;
}

你可能感兴趣的:(#,APUE,c++,开发语言,后端)