一,pause函数实现sleep
int pause(void);//头文件<unistd.h>
该函数的功能是使调用进程挂起直到有信号递达。如果处理动作是终止进程,则进程终止,函数不再返回;若处理动作是忽略,则进程继续挂起;若是自定义行为,则捕捉信号后返回-1。所以该函数只有出错的返回值。
#include <unistd.h> unsigned int sleep(unsigned int seconds);
sleep函数的功能是使进程挂起seconds秒后醒来。如果在秒数为到达seconds之前进程被唤醒了,返回剩余的秒数。若是正常醒来,返回0。
要实现sleep的功能,首先要使进程挂起,这里就要用到pause函数了。而进程seconds秒后要醒来,就要有信号递达使pause函数出错返回(否则进程会继续挂起或终止)。要实现seconds秒后发送信号的功能,就要使用到alarm函数,它可以再seconds秒后发送SIGALRM信号,并将该信号的处理动作改为用户自定义行为,进而使pause出错返回,使进程醒来。若进程在SIGALRM信号为递达之前就被唤醒了,此时就要取消闹钟来返回剩余的秒数。所以,整个函数的步骤如下:
(1)自定义SIGALRM信号的处理动作
(2)alarm(seconds)设置闹钟,在seconds秒后发送SIGALRM信号
(3)pause挂起进程
(4)alarm(0)取消闹钟(在(3)中pause可能被其他信号唤醒,所以要取消闹钟返回闹钟剩余的秒数)
(5)恢复SIGALRM信号的原有处理动作
(6)返回剩余的秒数
代码如下:
#include<stdio.h> #include<unistd.h> #include<signal.h> //捕捉闹钟信号 void handler(int signo) { ; } int mysleep(int sec) { struct sigaction act,oact;//act为要设置闹钟信号的相关信息,oact保存闹钟信号的原有相关信息 act.sa_handler = handler;//自定义闹钟信号的处理动作 act.sa_flags = 0; sigemptyset(&act.sa_mask);//在执行闹钟信号的处理动作时,不屏蔽其他信号 sigaction(SIGALRM,&act,&oact);//捕捉闹钟信号 alarm(sec);//sec秒后向进程发送个闹钟信号 pause();//使进程挂起 int ret = alarm(0);//取消闹钟,返回剩余的秒数 sigaction(SIGALRM,&oact,NULL);//恢复闹钟信号的默认处理动作,验证有无该语句的对比 return ret; } int main() { while(1) { printf("hello world\n"); mysleep(1); } return 0; }
运行结果:
[admin@localhost pause_sleep]$ gcc mysleep.c [admin@localhost pause_sleep]$ ./a.out hello world hello world ^C
在运行结果中,1s输出一条语句,Ctrl+C后进程结束。
下面代码验证mysleep的返回值功能,在上述代码的基础上设置2号信号的自定义行为,并修改main函数:
//捕捉其他信号 void handler1(int signo) { printf("catch signo %d\n",signo); } int main() { signal(2,handler1);//自定义2号信号的处理动作 printf("pid %d\n",getpid()); int ret = mysleep(20); printf("ret:%d\n",ret); return 0; }
运行结果:
[admin@localhost pause_sleep]$ ./a.out pid 12516 ^Ccatch signo 2 //大约1秒后按Ctrl+C发送2号信号 ret:19
结果显示mysleep的返回值正确。
注意:
(1)在mysleep程序中注册SIGALRM的处理函数,是为了使pause出错返回,使进程从挂起状态醒来。如果不注册该函函数,因为SIGALRM信号的默认处理动作是终止进程,所以在seconds秒后,进程会直接终止而不是醒来继续执行后面的语句。
而在处理函数中什么都没做,是因为sleep函数挂起结束后也什么都没做
(2)mysleep中再返回前恢复SIGALRM信号的原有处理动作,是因为在mysleep结束后,进程结束前,如果再次发送SIGALRM信号本意是想终止进程,但因为SIGALRM信号是自定义处理动作,所以不会终止进程。
(3)mysleep函数的返回值与sleep函数的返回值作用相同。
二,可重入函数
上面有提及,main函数和handler函数是不同的执行流。当main函数在调用一个函数如insert函数还未返回时,由于信号中断在执行信号的自定义行为时,再次调用insert函数,这就叫重入。如果insert函数访问的是一个全局链表,有可能因为重入导致结果错乱,这就叫不可重入函数。反之,如果insert函数只访问自己的局部变量或参数,则成为可重入函数。因为局部变量会保存在各自的栈帧结构中,而不会相互影响。
如果一个函数符合下述条件之一则是不可重入的:
(1)调用了malloc或free,因为malloc是用全局链表来管理堆的
(2)调用了I/O库函数,标准I/O库的很多实现都是以不可重入的方式使用全局数据结构的。
(3)函数中有很多的全局变量
三,SIGCHLD信号
在前面的进程控制中有提到子进程退出时,如果父进程不回收子进程的资源,就会造成内存泄漏。所以父进程通过等待的方式来回收子进程资源。父进程若以阻塞的方式等待,子进程不结束,父进程除了等待也不能干其他事。父进程以非阻塞的方式等待,就需要在做其他事的同时一直询问子进程是否结束。这两种方式都是父进程的工作效率变得很低。
实际上,子进程在结束的时候会给父进程发送一个SIGCHLD信号,通知父进程它现在要退出了。所以,父进程可以一直做自己的事情直到收到子进程发送的信号SIGCHLD,在以等待的方式回收子进程资源。因此,可以父进程可以将SIGCHLD信号进行捕捉,在自定义函数中以等待的方式回收子进程资源。这样做便可以提高父进程的工作效率。
如果在同一时刻有多个子进程退出,即收到多个SIGCHLD信号。如果父进程不及时处理,可能会使一些SIGCHLD信号被忽略,从而无法回收子进程资源造成内存泄漏。所以,在自定义函数中,要以循环的方式进行等待,如果有子进程结束,就立即等待回收资源,如果没有则返回父进程的执行流中继续执行父进程的工作。所以,这里父进程还需要以非阻塞的方式进行等待,否则会一直阻塞在自定义函数中,同样会使父进程效率低下。handler函数代码如下:
void handler(int signo) { pid_t pid; while(pid = waitpid(-1,NULL,WNOHANG) > 0) { printf("child exit. child pid is %d\n",ret); } }
在linux中,如果将SIGCHLD的处理动作设置为SIG_IGN,此时fork出来的子进程在退出时会自己清理掉,而不会通知父进程也不会产生僵尸进程。用以下代码来演示:
int main() { struct sigaction act,oact; act.sa_handler = SIG_IGN;//将SIGCHLD信号的处理动作设置为默认 act.sa_flags = 0; sigaction(SIGCHLD,&act,&oact);//对比该语句设置前后子进程退出后的状态变化 pid_t pid = fork(); if(pid < 0) { perror("fork"); exit(1); } else if(pid == 0) { printf("i am child pid is %d\n",getpid()); int count = 3; while(count--) { printf("hello world\n"); sleep(1); } exit(2); } while(1) //父进程没有等待 { printf("i am father\n"); sleep(1); } return 0; }
运行结果:
[admin@localhost SIGCHLD]$ while :; do ps axj | grep a.out | grep -v grep;sleep 1;echo "################";done
10912 11922 11922 10912 pts/0 13395 T 500 0:00 ./a.out
10912 13395 13395 10912 pts/0 13395 S+ 500 0:00 ./a.out
13395 13396 13395 10912 pts/0 13395 S+ 500 0:00 ./a.out
################
10912 11922 11922 10912 pts/0 13395 T 500 0:00 ./a.out
10912 13395 13395 10912 pts/0 13395 S+ 500 0:00 ./a.out
13395 13396 13395 10912 pts/0 13395 S+ 500 0:00 ./a.out
################
10912 11922 11922 10912 pts/0 13395 T 500 0:00 ./a.out
10912 13395 13395 10912 pts/0 13395 S+ 500 0:00 ./a.out
13395 13396 13395 10912 pts/0 13395 S+ 500 0:00 ./a.out
################
10912 11922 11922 10912 pts/0 13395 T 500 0:00 ./a.out
10912 13395 13395 10912 pts/0 13395 S+ 500 0:00 ./a.out
在代码中父进程并没有等待子进程,但子进程退出时也没有产生僵尸进程。
当取消上述代码中的sigaction语句时,结果如下:
[admin@localhost SIGCHLD]$ while :; do ps axj | grep a.out | grep -v grep;sleep 1;echo "################";done
10912 11922 11922 10912 pts/0 13463 T 500 0:00 ./a.out
10912 13463 13463 10912 pts/0 13463 S+ 500 0:00 ./a.out
13463 13464 13463 10912 pts/0 13463 S+ 500 0:00 ./a.out
################
10912 11922 11922 10912 pts/0 13463 T 500 0:00 ./a.out
10912 13463 13463 10912 pts/0 13463 S+ 500 0:00 ./a.out
13463 13464 13463 10912 pts/0 13463 S+ 500 0:00 ./a.out
################
10912 11922 11922 10912 pts/0 13463 T 500 0:00 ./a.out
10912 13463 13463 10912 pts/0 13463 S+ 500 0:00 ./a.out
13463 13464 13463 10912 pts/0 13463 S+ 500 0:00 ./a.out
################
10912 11922 11922 10912 pts/0 13463 T 500 0:00 ./a.out
10912 13463 13463 10912 pts/0 13463 S+ 500 0:00 ./a.out
13463 13464 13463 10912 pts/0 13463 Z+ 500 0:00 [a.out] <defunct>
此时,父进程也没有等待子进程,但子进程在退出后变成僵尸进程了。