UNIX下C语言----时钟与信号

一、时钟

  UNIX中存在三种格式的时间

  1)系统时间

  UNIX从出生到现在的秒数(生秒),表现为一个time_t类型的变量(大多数情形下time_h就是32位的整数)。

  2)高分辨率时间

  UNIX存在精确到微秒的时间,变现为一个timeval结构的变量。

  struct timeval
 {
     time_t tv_sec;        /* Seconds. */
     suseconds_t tv_usec;    /* Microseconds. */
 };
其中,tv_sec为Epoch到创建struct timeval时的秒数,tv_usec为微秒数,即秒后面的零头。

 3)日历时间

  UNIX使用结构tm来满足我们平常人的时间要求:"年、月、日、时、分、秒"

  tm至少包括以下成员:

  int    tm_sec   seconds [0,61]
  int    tm_min   minutes [0,59]
  int    tm_hour  hour [0,23]
  int    tm_mday  day of month [1,31]
  int    tm_mon   month of year [0,11]
  int    tm_year  years since 1900
  int    tm_wday  day of week [0,6] (Sunday = 0)
  int    tm_yday  day of year [0,365]
  int    tm_isdst daylight savings flag

 

1.1系统时间

 系统时间是UNIX中最基本的时间形式,任何其它时钟形式都是从系统时间中转化而来的。UNIX中用于系统时间的函数如下:

#include<time.h>

time_t time(time_h *tolc);/*调用失败返回-1*/

double difftime(time_t time2,time_t time1);

ex:一个读取系统时间的例子:

#include<time.h>

void main()

{

  time_t start ,finish;

  time(&start);

  sleep(6);

  time(&finish);

  printf("start=[%d],finis[%d],difftime=[%f]/n",start,finish,differ(start,finish));

}

 

1.2本地时间

UNIX中更改系统时间为本地日历时间的函数如下:

#include<time.h>

struct tm *localtime(const time_h *clock);

time_h mktime(struct tm*timeptr);/*实现localtime的反功能,返回tm结构对应的系统时间*/

ex:一个打印本地时间的例子,它读取系统时间,在转换为本地时间格式输出

/*--------time2.c--------*/ #include<time.h> #include<stdio.h> void main() { struct tm when; time_h now; time(&now); when=*localtime(&now); printf("now=[%d][%04d%02d%02d %02d:%02d:%02d]/n",now,when.tm_year+1900,when.moth+1,when.tm_mday,when.tm_hour,when.tm_min,when.tm_sec); }

 

二、信号的概念

信号是传送给进程的事件通知,它可以完成进程间异步事件的通信,比如用户按"ctr+C"组合键,UNIX内核将产生程序终止信号SIGINT,并通知前台进程组终止进程。UNIX为进程定义了一组事件,并分别为每个事件定义为"SIG"开头的信号宏。

1.信号的产生

导致信号的产生的原因有很多,总体来说有三种可能:

1)程序错误

当硬件出现异常、除数为0或者软件非法访问等情况而产生。

2)外部事件

当定时器到达、用户按键中断或进程abort等信号发送函数而产生

3)显示请求

当进程调用kill、raise等信号发送函数或者用户执行shell命令kill传递信号时产生。

每个进程都有一个域存储信号的接受情况,域中每个位bit对应一个专门信号。进程在接受信号时设置之,在处理信号时清楚之。故进程可以并行接受不通的信号,但如无特殊处理机制(如信号阻塞等),不能记载同一信号在处理前的接受次数。

 

1.2信号的处理

当进程受到信号时有如下三种处理方式;

1)系统默认

这是进程对信号的一般处理方式,由UNIX内核完成。系统对不同的信号采用不同的默认处理方式,如下描述了所有可能的默认处理机制:

终止        进程退出

忽略        丢弃信号

core-dump        进程退出,并将进程在内存的数据和存储器状态以某种特殊的格式转存到文件系统"core"文件中。本类信号在程序异常时产生,产生core文件供程序员参考

挂起        进程进入睡眠态

继续        唤醒睡眠中的进程。"挂起"和"继续"处理方式主要应用于程序调试过程中,如进程单步调试等。

 

2)忽略信号

信号接收后,理解丢弃。注意信号SIGSTOP和SIGKIL不能忽略,否则会出现超级用户无法停止的进程。

3)捕捉信号

进程接收信号,并且调用自定义的代码响应值。比如守护进程生成函数InitServer就采用函数ClearChild相应信号SIGCLD.

 

四、信号操作

函数signal更改信号的默认处理方式,函数kill或raise可以向进程发送信号。{在实践应用中最常见的信号处理有:忽略SIGINT等进程终止类信号,屏蔽用户终止进程、忽略或捕捉子进程结束信号SIGC(H)LD,释放进程表项、预防僵死进程、捕捉定时器信号,完成进程定时或并时操作、捕捉自定义信号,完成进程特定操作}

1.信号的忽略与捕捉

函数signal设置对信号的操作动作,它的原型如下:

#include<signal.h>

void(* signal (int sig,void(*f)(int)))(int);

这是一个复杂的函数原型,可以分解为:

typedef void (*func)(int);

func signal(int sig,func f)

首先,定义函数指针类型func,它是一个指向参数为整形(int)且无返回值的函数指针,然后定义函数signal为具有int类型和func类型参数、返回值为func类型的函数。

函数signal更改进程对信号的处理方式(信号SIGKILL 和SIGSTOP除外)

信号忽略实例:(忽略信号SIGINT的例子)

#include<signal.h> #include<stdio.h> void main() { signal(SIGINT,SIG_IGN);/*屏蔽信号SIGINT*/ sleep(100); }

在程序运行的100秒内,无论用户如何键入终端命令(通常是"ctr+C"或delete)进程均不退出

 

信号捕捉实例

一个捕捉自定义信号的例子

#include<signal.h> #include<stdio.h> int usr1 = 0; usr2 = 0; void func(int); void main() { signal(SIGUSR1,func); signal(SIGUSR2,func); for(;;) sleep(1); } void func(int sig) { if(sig == SIGUSR1)usr1++; if(sig =-SIGUSR2) usr2++; fprintf(stderr,"SIGUSR1[%d];SIGUSR2[%d]/n",usr1,usr2); fprintf(SIGUSR1,func); fprintf(SIGUSR2,func); }

 

2.信号的显示发送

UNIX应用程序可以向进程发送任何信号,这些信号发送函数的原型如下:

#include<sys/types.h>

#include<signal.h>

int kill(pid_t pid,int signo);

int raise(int signo);

函数kill发送信号signo到参数pid决定的特定进程中,其中pid的取值含义如表

取值    含义

>0      发送信号signo到进程pid中

0        发送信号signo到与调用进程同组进程中

-1       发送信号signo到实际用户id等于调用进程的有效用户id的进程中

<-1    发送信号signo到进程组id等于pid绝对值的进程中

 

函数kill调用成功时返回0,失败返回-1

函数raise向进程自身发送信号signo,成功时返回0,失败时返回-1

 

ex:一个发送和捕捉SIGTERM终止信号的例子

#include<stdio.h> #inclue<signal.h> #include<unistd.h> void childfunc(int sig) { fprintf(stderr,"Get Sig/n"); exit(6); } void main() { pid_t pid; int status; if((pid=fork())<0) exit(1); else if(pid==0)/*子进程*/ { signal(SIGTERM,childfunc);/*捕捉信号SIGTERM*/ sleep(30); exit(0); } fprintf(stderr,"Parent [%d] Fork child pid=[%d]/n",getpid(),pid); sleep(1); kill(pid,SIGTERM); wait(&status); fprintf(stderr,"Kill child pid=[%d],exit status[%d]/n",pid,status>>8); }

 

五、定时器设置

在UNIX中,定时器的处理是通过信号来完成的:程序使用系统调用向UNIX内核设置定时器信息-->定时完成,UNIX

内核向进程发送定时器信号--->进程捕捉定时器信号,并且执行相关处理操作。

1.普通定时器设置

UNIX中定时器设置函数alarm的原型如下:

#include<unistd.h>

unsigned int alarm(unsigned int seconds);

函数alarm在UNIX内核中调用进程设置一个定时器,参数seconds决定了定时的长度,进程将在调用alarm后seconds秒,接受到UNIX内核发送的SIGALRM信号。如果在定时未完成内重复调用函数alarm,后一次的定时器设置将覆盖前面的。如果参数seconds取值为0,定时器将被取消。函数alarm每调用一次只能产生一次定时操作,如果需要反复定时,就要多次调用alarm。调用fork后,子进程中的定时器将被取消,但调用exec后,定时器仍然有效。

函数alarm总能调用成功,它返回上次定时器剩余的定时时间,如果是第一次设置定时器,返回0;

在UNIX中使用普通定时器的三个步骤如下:

步骤一:设置捕捉定时信号

更改信号SIGALRM的默认处理机制,并设置捕捉相应函数。如下:设置函数timefunc为信号捕捉函数

signal(SIGALRM,timefunc);

步骤二:在需要定时的代码处调用函数alarm即可。

步骤三:编写响应定时信号函数

捕捉定时信号函数模型如下:

void timefunc(int sig)

{

  ......

  signal(SIGALRM,timefunc);/*再次设置捕捉定时信号*/

  alarm(1);/*再次定时*/

}

 

由于在部分UNIX版本中,信号的捕捉只响应一次,故函数中需要增加信号捕捉设置。同理,函数alarm设置的定时器也只能定时一次,故函数中需要重新定时。

 

ex:定时器的例子,它每隔一秒向进程发送定时信号SIGALRM,进程在接收到信号时将被打印定时的次数,用户可以键入"ctr+C"或"delete"结束程序。如下:

/*---------函数alarm定时器实例 time.c---------*/ #include<unistd.h> #include<stdio.h> #include<signal.h> int n = 0; void timefunc(int sig)/*定时时间代码*/ { fprintf(stderr,"ALARM %d/n",n++); signal(SIGALRM,timefunc); alarm(1); } void main() { int status; signal(SIGALRM,timefunc); alarm(1); while(1); }

 

2.精通定时器设置

函数alarm设置的定时器只能精确到秒,而以下函数理论上可以精确到微秒

#include<sys/select.h>

include<sys/itimer.h>

int getitimer(int which,struct itimerval *value);

int setitimer(int which,struct itimerval *vlalue, struct itimerval *ovalue);

 

函数setitimer提供了三种定时器,它们相互独立,任意一个定时完成都将发送定时信号到进程,并且自动重新计时。参数which确定了定时器的类型。

ITIMER_REAL  定时真实时间,与alarm类型相同      SIGALRM

ITIMER_VIRT   定时进程在用户态下的实际执行时间   SIGVTALRM

ITIMER_PROF  定时进程在用户态和核心态下的实际执行时间 SIGPROF

这三种定时器定时完成给进城发送的信号各不相同.

 

函数alarm本质上设置的是低精确、非重载的ITIMER_REAL类定时器,它只能精确到秒,并且每次设置只能产生一次定时,函数setimer设置的定时器则不同,它们不但可以计时到微秒,还能自动循环定时。在UNIX进程中,不能同时使用alarm和ITIMER_REAL类定时器。

 

结构itimerval描述了定时器的组成

struct itimerval
{

   struct timeval it_interval; /*下次定时取值*/

  struct timeval it_value;  /*本次定时设置值*/

};

 

结构time描述了一个精确到微秒的时间

struct timeval

{

   long tv_sec;

   long tv_usec;

};

 

函数setitimer设置一个定时器,参数value指向一个itimerval结构,该结构决定了设置的定时器信息,结构成员it_value指定首次定时的时间,结构成员it_interval指定下次定时的时间。定时器工作时,先将it_value的时间值减到0,发送一个信号,再将it_value赋值为it_interval的值,重新开始定时,如此反复。如果it_value值被设置为0,则定时器停止定时;如果it_value值不为0但it_interval值为0,则定时器在一次定时后终止。

函数setitimer调用成功时返回0,否则返回-1,参数ovalue指定了读取的定时器类型,参数value返回定时器状态。

 

精确定时器实例

进程每个1.5秒发送定时信号SIGPROF,在接收到信号时将打印定时的次数,用户可以键入"CTR+C"或"DELETE"结束程序,代码如下:

#include<sys/select .h> #include<itimer.h> #include<stdio.h> #include<unistd.h> #include<signal.h> int n = 0; void timefunc(int sig) { fprintf(stderr,"ITIMER_PROF[%d]/n",n++); signal(SIGPROF,timefunc); } void main() { struct itimerval value; value.it_value.tv_sec=1; value.it_value.tv_usec=500000; value.it_interval.tv_sec=1; value.it_interval.tv_usec=500000; signal(SIGPROF,timefunc); setitimer(ITIMER_PROF,&value,NULL); while(1); }

 

六、全局跳转

UNIX下C语言中,具有一对特殊的调用,跳转函数,原型如下:

#include<setjmp.h>

int setjmp(jmp_buf env);

void longjmp(jmp_buf env,int val);

函数setjmp存储当前的堆栈环境(包括程序的当前执行位置)到参数env中,当函数正常调用成功时返回0。函数longjmp恢复保存在env中的堆栈信息,并使程序转移到env中保存的位置处重新执行。这两个函数联合使用,可以实现程序的重复执行。

函数longjmp调用成功后,程序转移到函数setjmp处执行,函数setjmp返回val。如果参数val取值为0,为了与上次正常调用setjmp相区别,函数setjmp将自动返回-1。

在UNIX中使用全局跳转的步骤如下:

1st step:定义jmp_buf变量

#include<setjmp.h>

jmp_buf env;

 

2nd step:在需要程序重复执行的位置调用setjmp:

i=setjmp(env);

 

3rd:在调用setjmp后增加判断代码,确认程序的执行次数,避免产生死循环。例如下面语句实现程序回退一次后退出的功能:

if(i != 0) exit(0);

 

4th step:在程序开始回退的位置调用longjmp

longjmp(env,1);

 

ex:使用跳转的语句,它跳转两次后退出,

#include<setjmp.h> int j = 0; jmp_buf env; void main() { int i,k = 0; i = setjmp(env); printf("setjmp =[%d];j=[%d];k=[%d]/n",i,j++,k++); if(j>2)exit(0); sleep(1); longjmp(env,1); }

 

 

七、实践经验:单进程I/O超时处理

UNIX的I/O超时处理是一个常见的问题,它通常的做法为:接收输入(或发送输出)后立刻返回,如果无输入(或输出)则n秒后定时返回。比如银行柜台程序等待客户输入密码时,如果有输入则立刻返回,否则一分钟后超时返回,程序继续。

一般情况下,处理UNIX中I/O操作的方式有终端方式、信号跳转方式和多路复用方式等多种。本节设计了一个定时I/O超时的例子:它从文件描述符0(标准输入文件流)中读取一个字符,当有输入时继续,或者3秒后超时退出,并打印超时信息。

1.终端I/O超时方式

利用ioctl函数,设置文件描述符对应的标准输入文件属性为"接受输入后立刻返回,如果无输入则10秒钟后定时返回"模式

/*----终端方式超时处理timeout1.c----*/ #include<unistd.h> #include<termio.h> #include<fcntl.h> void main() { struct termio old,new; char c=0; ioctl(0,TCGETA,&old); new = old; new.c_lflag &= ~ICANON; new.c_cc[VMIN] = 0; new.c_cc[VTIMe] = 30; ioctl(0,TCSETA,&new); if((read(0,&c,1)) !=1 ) { printf("timeout/n"); } else printf("/n %d/n",c); ioctl(0,TCSETA,&old); }

 

 

 

你可能感兴趣的:(UNIX下C语言----时钟与信号)