• signal()函数


    转自:http://blog.csdn.net/sddzycnqjn/article/details/7285760

    1. 信号概念 
    信号是进程在运行过程中,由自身产生或由进程外部发过来的消息(事件)。信号是硬件中断的软件模拟(软中断)。每个信号用一个整型常量宏表示,以SIG开头,比如SIGCHLD、SIGINT等,它们在系统头文件中定义,也可以通过在shell下键入kill –l查看信号列表,或者键入man 7 signal查看更详细的说明。
    信号的生成来自内核,让内核生成信号的请求来自3个地方:
    l         用户:用户能够通过输入CTRL+c、Ctrl+,或者是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号;
    l         内核:当进程执行出错时,内核会给进程发送一个信号,例如非法段存取(内存访问违规)、浮点数溢出等;
    l         进程:一个进程可以通过系统调用kill给另一个进程发送信号,一个进程可以通过信号和另外一个进程进行通信。
    由进程的某个操作产生的信号称为同步信号(synchronous signals),例如除0;由象用户击键这样的进程外部事件产生的信号叫做异步信号。(asynchronous signals)。
           进程接收到信号以后,可以有如下3种选择进行处理:
    l         接收默认处理:接收默认处理的进程通常会导致进程本身消亡。例如连接到终端的进程,用户按下CTRL+c,将导致内核向进程发送一个SIGINT的信号,进程如果不对该信号做特殊的处理,系统将采用默认的方式处理该信号,即终止进程的执行;
    l         忽略信号:进程可以通过代码,显示地忽略某个信号的处理,例如:signal(SIGINT,SIGDEF);但是某些信号是不能被忽略的,
    l         捕捉信号并处理:进程可以事先注册信号处理函数,当接收到信号时,由信号处理函数自动捕捉并且处理信号。
     
    有两个信号既不能被忽略也不能被捕捉,它们是SIGKILL和SIGSTOP。即进程接收到这两个信号后,只能接受系统的默认处理,即终止线程。
    2. signal信号处理机制 
    可以用函数signal注册一个信号捕捉函数。原型为:
    #include 
    typedef void (*sighandler_t)(int);
    sighandler_t signal(int signum, sighandler_t handler);
     
    signal 的第1个参数signum表示要捕捉的信号,第2个参数是个函数指针,表示要对该信号进行捕捉的函数,该参数也可以是SIG_DEF(表示交由系统缺省处理,相当于白注册了)或SIG_IGN(表示忽略掉该信号而不做任何处理)。signal如果调用成功,返回以前该信号的处理函数的地址,否则返回 SIG_ERR。
    sighandler_t是信号捕捉函数,由signal函数注册,注册以后,在整个进程运行过程中均有效,并且对不同的信号可以注册同一个信号捕捉函数。该函数只有一个参数,表示信号值。
    示例:

    1、  捕捉终端CTRL+c产生的SIGINT信号:
    #include <unistd.h>
    #include <stdio.h>
    #include <sys/wait.h>
    #include <sys/types.h>
     
    void SignHandler(int iSignNo)
    {
        printf("Capture sign no:%d/n",iSignNo); 
    }
     
    int main()
    {
        signal(SIGINT,SignHandler); 
        while(true) 
            sleep(1); 
        return 0; 
    }
    该程序运行起来以后,通过按 CTRL+c将不再终止程序的运行。应为CTRL+c产生的SIGINT信号已经由进程中注册的SignHandler函数捕捉了。该程序可以通过 Ctrl+/终止,因为组合键Ctrl+/能够产生SIGQUIT信号,而该信号的捕捉函数尚未在程序中注册。
    2、  忽略掉终端CTRL+c产生的SIGINT信号:
    #include <unistd.h>
    #include <stdio.h>
    #include <sys/wait.h>
    #include <sys/types.h>
     
    int main()
    {
        signal(SIGINT,SIG_IGN); 
        while(true) 
            sleep(1); 
        return 0; 
    }
    该程序运行起来以后,将CTRL+C产生的SIGINT信号忽略掉了,所以CTRL+C将不再能是该进程终止,要终止该进程,可以向进程发送SIGQUIT信号,即组合键CTRL+/
     
    3、  接受信号的默认处理,接受默认处理就相当于没有写信号处理程序:
     
    #include <unistd.h>
    #include <stdio.h>
    #include <sys/wait.h>
    #include <sys/types.h>
     
    int main()
    {
        signal(SIGINT,DEF); 
        while(true) 
            sleep(1); 
        return 0; 
    }
    3. sigaction信号处理机制 
    3.1. 信号处理情况分析 
    在signal处理机制下,还有许多特殊情况需要考虑:
    1、  册一个信号处理函数,并且处理完毕一个信号之后,是否需要重新注册,才能够捕捉下一个信号;
    2、  如果信号处理函数正在处理信号,并且还没有处理完毕时,又发生了一个同类型的信号,这时该怎么处理;
    3、  如果信号处理函数正在处理信号,并且还没有处理完毕时,又发生了一个不同类型的信号,这时该怎么处理;
    4、  如果程序阻塞在一个系统调用(如read(...))时,发生了一个信号,这时是让系统调用返回错误再接着进入信号处理函数,还是先跳转到信号处理函数,等信号处理完毕后,系统调用再返回。
     
    示例:
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <signal.h>
     
    int g_iSeq=0;
     
    void SignHandler(int iSignNo)
    {
        int iSeq=g_iSeq++; 
        printf("%d Enter SignHandler,signo:%d./n",iSeq,iSignNo); 
        sleep(3); 
        printf("%d Leave SignHandler,signo:%d/n",iSeq,iSignNo); 
    }
     
    int main()
    {
        char szBuf[8]; 
        int iRet; 
        signal(SIGINT,SignHandler); 
        signal(SIGQUIT,SignHandler); 
        do{ 
            iRet=read(STDIN_FILENO,szBuf,sizeof(szBuf)-1); 
            if(iRet<0){ 
                perror("read fail."); 
                break; 
            } 
          szBuf[iRet]=0; 
            printf("Get: %s",szBuf); 
        }while(strcmp(szBuf,"quit/n")!=0); 
        return 0; 
    }
    程序运行时,针对于如下几种输入情况(要输入得快),看输出结果:
    1、  CTRL+c] [CTRL+c] [CTRL+c]
    2、  [CTRL+c] [CTRL+/]
    3、  hello [CTRL+/] [Enter]
    4、  [CTRL+/] hello [Enter]
    5、  hel [CTRL+/] lo[Enter]
     
    针对于上面各种情况,不同版本OS可能有不同的响应结果。
    3.2. sigaction信号处理注册 
    如果要想用程序控制上述各种情况的响应结果,就必须采用新的信号捕获机制,即使用sigaction信号处理机制。
    函数原型:
    #include <signal.h>
    int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
    sigaction也用于注册一个信号处理函数。
    参数signum为需要捕捉的信号;
    参数 act是一个结构体,里面包含信号处理函数地址、处理方式等信息。
    参数oldact是一个传出参数,sigaction函数调用成功后,oldact里面包含以前对signum的处理方式的信息。
    如果函数调用成功,将返回0,否则返回-1
    结构体 struct sigaction(注意名称与函数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);     // 保留,不要使用。
    }
           该结构体的各字段含义及使用方式:
    1、字段sa_handler是一个函数指针,用于指向原型为void handler(int)的信号处理函数地址,       即老类型       的信号处理函数;
    2、字段sa_sigaction也是一个函数指针,用于指向原型为:
    void handler(int iSignNum,siginfo_t *pSignInfo,void *pReserved);
    的信号处理函数,即新类型的信号处理函数。
    该函数的三个参数含义为:
                  iSignNum :传入的信号
                  pSignInfo :与该信号相关的一些信息,它是个结构体
                  pReserved :保留,现没用
    3、字段sa_handler和sa_sigaction只应该有一个生效,如果想采用老的信号处理机制,就应该让sa_handler指向正确的信号处理函数;否则应该让sa_sigaction指向正确的信号处理函数,并且让字段 sa_flags包含SA_SIGINFO选项。
    4、字段sa_mask是一个包含信号集合的结构体,该结构体内的信号表示在进行信号处理时,将要被阻塞的信号。针对sigset_t结构体,有一组专门的函数对它进行处理,它们是:
                  #include <signal.h> 
            int sigemptyset(sigset_t *set);                                   // 清空信号集合set
            int sigfillset(sigset_t *set);                                 // 将所有信号填充进set中
            int sigaddset(sigset_t *set, int signum);               // 往set中添加信号signum
            int sigdelset(sigset_t *set, int signum);                // 从set中移除信号signum
            int sigismember(const sigset_t *set, int signum); // 判断signnum是不是包含在set中
           例如,如果打算在处理信号SIGINT时,只阻塞对SIGQUIT信号的处理,可以用如下种方法:
                  struct sigaction act; 
                  sigemptyset(&act.sa_mask); 
                  sigaddset(&act_sa_mask,SIGQUIT); 
                  sigaction(SIGINT,&act,NULL); 
    5、  字段sa_flags是一组掩码的合成值,指示信号处理时所应该采取的一些行为,各掩码的含义为:
     
    掩码 描述 
    SA_RESETHAND 处理完毕要捕捉的信号后,将自动撤消信号处理函数的注册,即必须再重新注册信号处理函数,才能继续处理接下来产生的信号。该选项不符合一般的信号处理流程,现已经被废弃。 
    SA_NODEFER 在处理信号时,如果又发生了其它的信号,则立即进入其它信号的处理,等其它信号处理完毕后,再继续处理当前的信号,即递规地处理。如果sa_flags包含了该掩码,则结构体sigaction的sa_mask将无效! 
    SA_RESTART 如果在发生信号时,程序正阻塞在某个系统调用,例如调用read()函数,则在处理完毕信号后,接着从阻塞的系统返回。该掩码符合普通的程序处理流程,所以一般来说,应该设置该掩码,否则信号处理完后,阻塞的系统调用将会返回失败! 
    SA_SIGINFO 指示结构体的信号处理函数指针是哪个有效,如果sa_flags包含该掩码,则sa_sigactiion指针有效,否则是sa_handler指针有效。

     
     
           练习与验证:
    针对于先前的5种输入情况,给下面代码再添加一些代码,使之能够进行如下各种形式的响应:
           1 、[CTRL+c] [CTRL+c]时,第1个信号处理阻塞第2个信号处理;
           2 、[CTRL+c] [CTRL+c]时,第1个信号处理时,允许递规地第2个信号处理;
           3 、[CTRL+c] [CTRL+/]时,第1个信号阻塞第2个信号处理;
           4 、read不要因为信号处理而返回失败结果。
     
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <signal.h>
     
    int g_iSeq=0;
     
    void SignHandlerNew(int iSignNo,siginfo_t *pInfo,void *pReserved)
    {
        int iSeq=g_iSeq++; 
        printf("%d Enter SignHandlerNew,signo:%d./n",iSeq,iSignNo); 
        sleep(3); 
        printf("%d Leave SignHandlerNew,signo:%d/n",iSeq,iSignNo); 
    }
     
    int main()
    {
        char szBuf[8]; 
        int iRet; 
        struct sigaction act; 
        act.sa_sigaction=SignHandlerNew; 
        act.sa_flags=SA_SIGINFO; 
                  // 
        sigemptyset(&act.sa_mask); 
       sigaction(SIGINT,&act,NULL); 
        sigaction(SIGQUIT,&act,NULL); 
        do{ 
            iRet=read(STDIN_FILENO,szBuf,sizeof(szBuf)-1); 
            if(iRet<0){ 
                perror("read fail."); 
                break; 
            } 
            szBuf[iRet]=0; 
            printf("Get: %s",szBuf); 
        }while(strcmp(szBuf,"quit/n")!=0); 
        return 0; 
    }
     
    3.3. sigprocmask信号阻塞 
    函数sigaction中设置的被阻塞信号集合只是针对于要处理的信号,例如
    struct sigaction act;
                      sigemptyset(&act.sa_mask); 
                  sigaddset(&act.sa_mask,SIGQUIT); 
                      sigaction(SIGINT,&act,NULL); 
           表示只有在处理信号SIGINT时,才阻塞信号SIGQUIT;
           函数sigprocmask是全程阻塞,在sigprocmask中设置了阻塞集合后,被阻塞的信号将不能再被信号处理函数捕捉,直到重新设置阻塞信号集合。
           原型为:
           #include <signal.h> 
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
    参数how的值为如下3者之一:
           a :SIG_BLOCK ,将参数2的信号集合添加到进程原有的阻塞信号集合中
           b :SIG_UNBLOCK ,从进程原有的阻塞信号集合移除参数2中包含的信号
           c :SIG_SET,重新设置进程的阻塞信号集为参数2的信号集
    参数set为阻塞信号集
    参数oldset是传出参数,存放进程原有的信号集。
    示例:
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <signal.h>
     
    int g_iSeq=0;
     
    void SignHandlerNew(int iSignNo,siginfo_t *pInfo,void *pReserved)
    {
        int iSeq=g_iSeq++; 
        printf("%d Enter SignHandlerNew,signo:%d./n",iSeq,iSignNo); 
        sleep(3); 
        printf("%d Leave SignHandlerNew,signo:%d/n",iSeq,iSignNo); 
    }
     
    int main()
    {
        char szBuf[8]; 
        int iRet; 
        struct sigaction act; 
        act.sa_sigaction=SignHandlerNew; 
        act.sa_flags=SA_SIGINFO; 
        // 屏蔽掉SIGINT 信号,SigHandlerNew 将不能再捕捉SIGINT 
    sigset_t sigSet; 
        sigemptyset(&sigSet); 
        sigaddset(&sigSet,SIGINT); 
        sigprocmask(SIG_BLOCK,&sigSet,NULL); 
                  // 
        sigemptyset(&act.sa_mask); 
        sigaction(SIGINT,&act,NULL); 
        sigaction(SIGQUIT,&act,NULL); 
        do{ 
            iRet=read(STDIN_FILENO,szBuf,sizeof(szBuf)-1); 
            if(iRet<0){ 
                perror("read fail."); 
                break; 
            } 
            szBuf[iRet]=0; 
            printf("Get: %s",szBuf); 
        }while(strcmp(szBuf,"quit/n")!=0); 
        return 0; 
    }
     
    4. 用程序发送信号 
    4.1. kill信号发送函数 
    原型为:
    #include <sys/types.h>
        #include <signal.h> 
    int kill(pid_t pid, int sig);
           参数pid为将要接受信号的进程的pid
           参数sig为要发送的信号
           如果成功,返回0,否则为-1。
           示例,输入结束后,将通过发送信号SIGQUIT把自己杀掉:
    #include <stdio.h>
    #include <signal.h>
    #include <unistd.h>
    #include <sys/types.h>
    int main()
    {
                      while(true){ 
                   if(getchar()==EOF) 
                kill(getpid(),SIGQUIT); 
                      } 
        return 0; 
    }
    4.2. sigqueue信号发送函数 
    sigqueue也可以发送信号,并且能传递附加的信息。
    原型为:
    #include <signal.h>
        int sigqueue(pid_t pid, int sig, const union sigval value); 
    参数pid为接收信号的进程;
    参数sig为要发送的信号;
    参数value为一整型与指针类型的联合体:
           union sigval { 
    int   sival_int; 
    void *sival_ptr;
        }; 
    由sigqueue函数发送的信号的第3个参数value的值,可以被进程的信号处理函数的第2个参数info->si_ptr接收到。
    示例1,进程给自己发信号,并且带上附加信息:
    #include <signal.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <stdio.h>
     
    void SignHandlerNew(int signum,siginfo_t *info,void *myact)
    {
                      char *pszInfo=(char *)(info->si_ptr); 
        printf("Get:%d info:%s/n",signum,pszInfo); 
    }
     
    int main(int argc,char**argv)
    {
                      struct sigaction act;   
        union sigval mysigval; 
                      int sig; 
        char data[]="other info"; 
                      // 
                      if(argc<2){ 
                          printf("usage: SIGNNUM/n"); 
            return -1; 
                      } 
        mysigval.sival_ptr=data; 
                      sig=atoi(argv[1]); 
        sigemptyset(&act.sa_mask); 
              act.sa_sigaction=SignHandlerNew; 
                      act.sa_flags=SA_SIGINFO; 
                      sigaction(sig,&act,NULL); 
                      while(true){ 
                          printf("wait for the signal/n"); 
                          sigqueue(getpid(),sig,mysigval); 
                          sleep(2); 
                      } 
    }
     
    示例2:一个进程向另外一个进程发送信号。注意:发送进程不要将自己进程空间的地址发送给接收进程,因为接收进程接收到地址也访问不到发送进程的地址空间的。
     
    示例2信号接收程序:
           #include <signal.h> 
    #include <sys/types.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <stdio.h>
     
    void SignHandlerNew(int signum,siginfo_t *info,void *myact)
    {
                      printf("Get:%d info:%d/n",signum,info->si_int); 
    }
     
    int main(int argc,char**argv)
    {
                      struct sigaction act; 
        // 
                      if(argc<2){ 
                          printf("usage: signnum/n"); 
            return -1; 
                      } 
        sigemptyset(&act.sa_mask); 
        act.sa_sigaction=SignHandlerNew; 
                      act.sa_flags=SA_SIGINFO; 
        sigaction(atoi(argv[1]),&act,NULL); 
                      while(1) 
        { 
                          printf("wait for the signal/n"); 
                   sleep(2); 
        } 
                      return 0; 
    }
           
    示例2信号发送程序:
    #include <signal.h>
    #include <sys/types.h>
    #include <unistd.h>
    #include <string.h>
    #include <stdlib.h>
    #include <stdio.h>
     
    int main(int argc,char**argv)
    {
                      union sigval mysigval; 
        int iPid,iSignNo,iData; 
                      // 
        if(argc<4){ 
                          printf("usage: pid signnum data/n"); 
                          return -1; 
    }
        iPid=atoi(argv[1]); 
                      iSignNo=atoi(argv[2]); 
        iData=atoi(argv[3]); 
        mysigval.sival_int=iData; 
                      if(sigqueue(iPid,iSignNo,mysigval)<0) 
                          perror("Send signal fail."); 
        return 0; 
    }     
           
    5. 计时器与信号 
    5.1. 睡眠函数 
    Linux下有两个睡眠函数,原型为:
           #include <unistd.h> 
            unsigned int sleep(unsigned int seconds); 
                  void usleep(unsigned long usec); 
           函数sleep让进程睡眠seconds秒,函数usleep让进程睡眠usec毫秒。
           sleep 睡眠函数内部是用信号机制进行处理的,用到的函数有:
                  #include <unistd.h> 
    unsigned int alarm(unsigned int seconds);     // 告知自身进程,要进程在seconds秒后自动产生一个//SIGALRM的信号,
    int pause(void);                       // 将自身进程挂起,直到有信号发生时才从pause返回
           
           示例:模拟睡眠3秒:
                  #include <signal.h> 
    #include <stdio.h>
    #include <unistd.h>
     
    void SignHandler(int iSignNo)
    {
        printf("signal:%d/n",iSignNo); 
    }
     
    int main()
    {
        signal(SIGALRM,SignHandler); 
        alarm(3); 
        printf("Before pause()./n"); 
        pause(); 
        printf("After pause()./n"); 
        return 0; 
    }
    注意:因为sleep在内部是用alarm实现的,所以在程序中最好不要sleep与alarm混用,以免造成混乱。
    5.2. 时钟处理   
    Linux为每个进程维护3个计时器,分别是真实计时器、虚拟计时器和实用计时器。
    l         真实计时器计算的是程序运行的实际时间;
    l         虚拟计时器计算的是程序运行在用户态时所消耗的时间(可认为是实际时间减掉(系统调用和程序睡眠所消耗)的时间);
    l         实用计时器计算的是程序处于用户态和处于内核态所消耗的时间之和。
    例如:有一程序运行,在用户态运行了5秒,在内核态运行了6秒,还睡眠了7秒,则真实计算器计算的结果是18秒,虚拟计时器计算的是5秒,实用计时器计算的是11秒。
    用指定的初始间隔和重复间隔时间为进程设定好一个计时器后,该计时器就会定时地向进程发送时钟信号。3个计时器发送的时钟信号分别为:SIGALRM,SIGVTALRM和SIGPROF。
    用到的函数与数据结构:
    #include <sys/time.h>
     
    //获取计时器的设置
    //which指定哪个计时器,可选项为ITIMER_REAL(真实计时器)、ITIMER_VITUAL(虚拟计时器、ITIMER_PROF(实用计时器))
    //value为一结构体的传出参数,用于传出该计时器的初始间隔时间和重复间隔时间
    //如果成功,返回0,否则-1
    int getitimer(int which, struct itimerval *value);
     
    //设置计时器
    //which指定哪个计时器,可选项为ITIMER_REAL(真实计时器)、ITIMER_VITUAL(虚拟计时器、ITIMER_PROF(实用计时器))
    //value为一结构体的传入参数,指定该计时器的初始间隔时间和重复间隔时间
    //ovalue为一结构体传出参数,用于传出以前的计时器时间设置。
    //如果成功,返回0,否则-1
    int setitimer(int which, const struct itimerval *value, struct itimer val *ovalue);
        
    struct itimerval {
    struct timeval it_interval; /* next value */            // 重复间隔
    struct timeval it_value;    /* current value */     // 初始间隔     
    };
    struct timeval {
    long tv_sec;                /* seconds */                    // 时间的秒数部分
    long tv_usec;               /* microseconds */        // 时间的微秒部分
    };
     
    示例:启用真实计时器的进行时钟处理
                #include <stdio.h> 
    #include <unistd.h>
    #include <signal.h>
    #include <sys/time.h>
     
    void TimeInt2Obj(int imSecond,timeval *ptVal)
    {
    ptVal->tv_sec=imSecond/1000;
            ptVal->tv_usec=(imSecond%1000)*1000; 
    }
     
    void SignHandler(int SignNo)
    {
    printf("Clock/n");
    }
     
    int main()
    {
    signal(SIGALRM,SignHandler);
        itimerval tval; 
        TimeInt2Obj(1,&tval.it_value);            // 设初始间隔为1毫秒,注意不要为0
                  TimeInt2Obj(1500,&tval.it_interval);    // 设置以后的重复间隔为1500毫秒
        setitimer(ITIMER_REAL,&tval,NULL); 
                   while(getchar()!=EOF); 
        return 0; 
    }

    SIGHUP     终止进程     终端线路挂断
    SIGINT     终止进程     中断进程
    SIGQUIT   建立CORE文件终止进程,并且生成core文件
    SIGILL   建立CORE文件       非法指令
    SIGTRAP   建立CORE文件       跟踪自陷
    SIGBUS   建立CORE文件       总线错误
    SIGSEGV   建立CORE文件       段非法错误
    SIGFPE   建立CORE文件       浮点异常
    SIGIOT   建立CORE文件       执行I/O自陷
    SIGKILL   终止进程     杀死进程
    SIGPIPE   终止进程     向一个没有读进程的管道写数据
    SIGALARM   终止进程     计时器到时
    SIGTERM   终止进程     软件终止信号
    SIGSTOP   停止进程     非终端来的停止信号
    SIGTSTP   停止进程     终端来的停止信号
    SIGCONT   忽略信号     继续执行一个停止的进程
    SIGURG   忽略信号     I/O紧急信号
    SIGIO     忽略信号     描述符上可以进行I/O
    SIGCHLD   忽略信号     当子进程停止或退出时通知父进程
    SIGTTOU   停止进程     后台进程写终端
    SIGTTIN   停止进程     后台进程读终端
    SIGXGPU   终止进程     CPU时限超时
    SIGXFSZ   终止进程     文件长度过长
    SIGWINCH   忽略信号     窗口大小发生变化
    SIGPROF   终止进程     统计分布图用计时器到时
    SIGUSR1   终止进程     用户定义信号1
    SIGUSR2   终止进程     用户定义信号2
    SIGVTALRM 终止进程     虚拟计时器到时

  • 相关阅读:
    硅谷机场安装第一个咖啡机器人
    美国第一夫人,发推特更新她刚刚装饰好的白宫
    DAO设计模式
    高科技公司名字的由来
    项目管理的经验
    Shanghai InfoSys .NET engineer telephone interview
    {objccn.io}学习笔记-并发编程-常见的后台实践
    升级Xcode7之后VVDocumenter-Xcode不能用的解决办法
    推荐大家看的开发者博客
    iOS8 Layout Margins
  • 原文地址:https://www.cnblogs.com/lp3318/p/5542551.html
Copyright © 2020-2023  润新知