守护进程(Daemon)

前言

Linux常用于服务器,程序通常不运行在前台。运行于前台的进程和终端关联,一旦终端关闭,进程也随之退出。因为守护进程不和终端关联,因此它的标准输出和标准输入也无法工作,调试信息应该写入到普通文件中,以便将来进行错误定位和调试。而且守护进程通常以root权限运行。

编程规则

  • 设置umask为0

  • 调用fork,并让父进程退出

  • 调用setuid创建新会话

  • 重新设置但前目录

  • 关闭不需要的文件描述符

  • 重定向标准输入/标准输出/标准错误到/dev/null

    1. #include <stdio.h>
    2. #include <stdlib.h>
    3. #include <unistd.h>
    4. #include <fcntl.h>
    5. #include <sys/types.h>
    6. #include <syslog.h>
    7. int main()
    8. {
    9. pid_t pid = fork();
    10. if(pid == 0)
    11. {
    12. pid = fork();
    13. if(pid == 0)
    14. {
    15. // daemon process
    16. umask(0); // 设置掩码
    17. setsid(); // 让自己变成session leader
    18. chdir("/"); // 修改当前目录
    19. chroot("/");
    20.  
    21. // 获取最大的已经打开的文件描述符
    22. int maxfd = 1024; // 演示
    23. // 把所有文件关闭
    24. int i;
    25. for(i=0; i<=maxfd; ++i)
    26. {
    27. close(i);
    28. }
    29.  
    30. // 重定向0、1、2文件到/dev/null
    31. open("/dev/null", O_RDONLY); // 标准输入
    32. open("/dev/null", O_WRONLY); // 标准输出
    33. open("/dev/null", O_WRONLY); // 标准错误
    34.  
    35. // printf(""); // --> aaa.txt 效率低下
    36. //
    37. syslog(LOG_ERR|LOG_KERN, "haha, this is syslog....\n");
    38.  
    39. // 后台进程不退出
    40. while(1)
    41. sleep(1);
    42.  
    43. }
    44. }
    45. }

出错处理

由于不能再使用标准输入和输出,因此需要调用以下函数来输出调试信息。

单例

守护程序往往只有一个实例,而不允许多个,可以用文件锁来实现单例。

惯例

惯例是指大家都这么做,不这么做显得不专业的事情。

  • 单例文件路径在/var/run目录下,内容为该进程ID

  • 配置文件应该在/etc目录下

  • 守护的启动脚本通常放在/etc/init.d目录下

高级IO

前言

在文件IO中,学习了如何通过read和write来实现文件的读写。在这一章讨论一些高级的IO方式。

非阻塞IO

IO通常是阻塞的,比如读鼠标文件,如果鼠标未产生数据,那么读操作会阻塞,一直到鼠标移动,才能返回。这种阻塞的IO简化了程序设计,但是导致性能下降。

使用O_NONBLOCK标记打开文件,那么read行为就是非阻塞的了。如果read不到数据,read调用会返回-1,errno被标记为EAGAIN。

如果open时没有带上O_NONBLOCK,那么可以通过fcntl设置这个模式。

记录锁

如果多个进程/线程同时写文件,那么使用O_APPEND,可以保证写操作是原子操作,但是O_APPEND只写到文件末尾。

如果需要修改文件内容,则无法使用O_APPEND了,需要使用记录锁来锁定文件,保证写操作的原子性。

#include "../h.h"
 
int main()
{
    int fd = open("a.txt", O_RDWR);
 
    // lock it
    struct flock l;
    l.l_type = F_WRLCK;
    l.l_whence = SEEK_SET;
    l.l_start = 0;
    l.l_len = 128;
 
    int ret = fcntl(fd, F_SETLKW, &l);
    if(ret == 0)
    {
        printf("lock success\n");
    }
    else
    {
        printf("lock failure\n");
    }
 
    getchar();
    l.l_type = F_UNLCK;
    fcntl(fd, F_SETLKW, &l);
 
}

9.4 IO多路转接

如果一个进程,同时要响应多路IO数据,那么这个程序设计将会很麻烦。一般程序都是需要响应多路IO的,比如GUI程序都需要处理鼠标和键盘文件。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <sys/select.h>
  5. #include <sys/types.h>
  6. #include <fcntl.h>
  7. #include <errno.h>
  8.  
  9. //void FD_CLR(int fd, fd_set *set);
  10. // 将fd从set中拿掉
  11. //
  12. //int FD_ISSET(int fd, fd_set *set);
  13. //判断fd是否在集合中
  14. //
  15. //void FD_SET(int fd, fd_set *set);
  16. //将fd加入到集合中
  17. //
  18. //void FD_ZERO(fd_set *set);
  19. //将集合清空
  20.  
  21. // int select(int nfds, fd_set *readfds, fd_set *writefds,
  22. // fd_set *exceptfds, struct timeval *timeout);
  23. // int nfds: 要求是集合中最大的文件描述符+1
  24. // fd_set* readfds: 想读取的文件描述符集合,这个参数既是输入,也是输出参数
  25. // fd_set* writefds: 想写的文件描述符集合,一般为NULL
  26. // fd_set* execptfds:出错,异常的文件描述符集合,一般为NULL
  27. // struct timeval* timeout: 因为select是阻塞的调用,这个参数表示超过这个时间,无论文件描述符是否有消息,都继续往下执行
  28. // 返回值:-1表示失败,0表示超时,而且没有任何的事件,大于0表示有事件的文件描述符的数量
  29.  
  30. int main()
  31. {
  32. int fd_key;
  33. int fd_mice;
  34.  
  35. fd_key = open("/dev/input/event1", O_RDONLY);
  36. fd_mice = open("/dev/input/mice", O_RDONLY);
  37. if(fd_key < 0 || fd_mice < 0)
  38. {
  39. perror("open key mice");
  40. return 0;
  41. }
  42.  
  43. // fd_set 文件描述符集合类型
  44. fd_set set;
  45. FD_ZERO(&set);
  46. FD_SET(fd_key, &set);
  47. FD_SET(fd_mice, &set);
  48.  
  49. // 此时set中有两个文件描述符,分别是鼠标和键盘
  50. int nfds = fd_key > fd_mice ? fd_key : fd_mice;
  51. nfds ++;
  52.  
  53. struct timeval tv;
  54. tv.tv_sec = 1; // 秒
  55. tv.tv_usec = 0; // 微秒 1/1000000 秒
  56. int ret;
  57. RESELECT:
  58. ret = select(nfds, &set, NULL, NULL, &tv); // 阻塞一秒
  59.  
  60. if(ret < 0)
  61. {
  62. if(errno == EINTR) // 被中断打断
  63. {
  64. // 补救
  65. goto RESELECT;
  66. }
  67. return 0;
  68. }
  69.  
  70. if(ret == 0)
  71. {
  72.  
  73. }
  74.  
  75. if(ret > 0)
  76. {
  77. // 用户动了鼠标或者键盘,从而鼠标文件描述符或者键盘文件描述符可读
  78. if(FD_ISSET(fd_key, &set))
  79. {
  80. printf("keyboard message\n");
  81. // 键盘有消息
  82. }
  83. if(FD_ISSET(fd_mice, &set))
  84. {
  85. printf("mice message\n");
  86. // 鼠标有消息
  87. }
  88. }
  89. }
 

9.4.1 select

select的作用是,让内核监听一个fd集合,当集合中的fd有事件时,select会返回有消息的fd子集。

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <sys/select.h>
  5. #include <sys/types.h>
  6. #include <fcntl.h>
  7. #include <errno.h>
  8.  
  9. // fd_set最多能容纳1024个文件
  10. //
  11. // unsigned int data[32]; 32x32 = 1024
  12.  
  13. int main()
  14. {
  15. int fd_key;
  16. int fd_mice;
  17.  
  18. fd_key = open("/dev/input/event1", O_RDONLY);
  19. fd_mice = open("/dev/input/mice", O_RDONLY);
  20.  
  21. int nfds = fd_key > fd_mice ? fd_key : fd_mice;
  22. nfds ++;
  23.  
  24. // 文件描述符集合的拷贝
  25. fd_set set1;
  26. fd_set set2; // set1 --> set2
  27. memcpy(&set2, &set1, sizeof(set1));
  28.  
  29. while(1)
  30. {
  31. fd_set set;
  32. FD_ZERO(&set);
  33. FD_SET(fd_key, &set);
  34. FD_SET(fd_mice, &set);
  35.  
  36. struct timeval tv;
  37. tv.tv_sec = 1; // 秒
  38. tv.tv_usec = 0; // 微秒 1/1000000 秒
  39.  
  40. int ret = select(nfds, &set, NULL, NULL, &tv);
  41. if(ret < 0)
  42. {
  43. if(errno == EINTR)
  44. continue;
  45. return 0;
  46. }
  47.  
  48. if(ret > 0)
  49. {
  50. if(FD_ISSET(fd_key, &set))
  51. {
  52. // 既然鼠标有消息,就应该把数据都读出
  53. char buf[1024];
  54. read(fd_key, buf, sizeof(buf));
  55. printf("key event\n");
  56. }
  57. if(FD_ISSET(fd_mice, &set))
  58. {
  59. char buf[1024];
  60. read(fd_mice, buf, sizeof(buf));
  61. printf("mice event\n");
  62. }
  63. }
  64. }
  65. }

9.4.2 epoll

epoll的作用和select差不多,但是操作接口完全不同。

 
  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <errno.h>
  4. #include <sys/epoll.h>
  5. #include <sys/types.h>
  6. #include <fcntl.h>
  7.  
  8. // 通过epoll来实现多路io复用
  9. int main()
  10. {
  11. int fd_key = open("/dev/input/event1", O_RDONLY);
  12. int fd_mice = open("/dev/input/mice", O_RDONLY);
  13.  
  14. if(fd_key < 0 || fd_mice < 0)
  15. {
  16. perror("open mice and keyboard");
  17. return -1;
  18. }
  19.  
  20. // 创建epoll对象,创建epoll的参数已经废弃了,随便填
  21. int epollfd = epoll_create(512);
  22. if(epollfd < 0)
  23. {
  24. perror("epoll");
  25. return -1;
  26. }
  27.  
  28. // 把鼠标和键盘的文件描述符,加入到epoll集合中
  29. struct epoll_event ev;
  30. ev.data.fd = fd_key; // 联合体,这个联合体用来保存和这个文件描述符相关的一些数据,用于将来通知时,寻找文件描述符
  31. ev.events = EPOLLIN | EPOLLONESHOT; // epoll要监听的事件,读或者写
  32. epoll_ctl(epollfd, EPOLL_CTL_ADD, fd_key, &ev);
  33.  
  34. ev.data.fd = fd_mice;
  35. epoll_ctl(epollfd, EPOLL_CTL_ADD, fd_mice, &ev);
  36. // 调用epoll_ctl时,第四个参数被epoll_ctl拷贝走
  37.  
  38. struct epoll_event ev_out[2];
  39.  
  40. while(1)
  41. {
  42. int ret = epoll_wait(epollfd, ev_out, 2, 2000);
  43. if(ret < 0)
  44. {
  45. if(errno == EINTR)
  46. continue;
  47. return -2;
  48. }
  49.  
  50. if(ret > 0)
  51. {
  52. int i;
  53. for(i=0; i<ret; ++i)
  54. {
  55. if(ev_out[i].data.fd == fd_mice)
  56. {
  57. // 鼠标有消息
  58. // char buf[1024];
  59. // read(fd_mice, buf, sizeof(buf));
  60. printf("mice\n");
  61. }
  62. else if(ev_out[i].data.fd == fd_key)
  63. {
  64. // char buf[1024];
  65. // read(fd_key, buf, sizeof(buf));
  66. printf("key\n");
  67. }
  68. }
  69. }
  70. }
  71. }

select和epoll的区别

select epoll
出现早
大规模文件描述符效率低 大规模文件描述符效率高
小规模是select效率高  
使用位域来表示描述符集合 使用红黑树来保存文件集合

存储映射IO

10.1 前言

进程间通信(IPC)方式有许多种。包括匿名管道、命名管道、socketpair、信号、信号量、锁、文件锁、共享内存等等。

由于进程之间的虚拟地址无法相互访问,但是在实际的系统中,经常要涉及进程间的通信,所以在Unix的发展中,人们创造了多种进程间通信的方式,而这些通信方式,都被Linux继承了过来。

进程间通信的原理,是在进程外的公共区域申请内存,然后双方通过某种方式去访问公共区域内存。

按照分类,进程间通信涉及三个方面:

  • 小数据量通信(管道/socketpair)

  • 大数据量通信(共享内存)

  • 进程间同步(socketpair/管道/锁/文件锁/信号量)

10.2 匿名管道

用于有亲缘关系的进程间通信,匿名管道是单工通信方式。

内核的buffer究竟有多大?一个内存页尺寸。实际在Ubuntu下测试是64K。当缓冲区满的时候,write是阻塞的。

read管道时,如果管道中没有数据,那么阻塞等待。
read管道时,如果此时write端已经关闭,而此时管道有数据,就读数据,如果没有数据,那么返回0表示文件末尾。

write管道时,如果此时所有的read端已经关闭,那么内核会产生一个SIGPIPE给进程,SIGPIPE的默认会导致进程退出,如果此时进程处理了SIGPIPE信号,那么write会返回-1,错误码是EPIPE。

10.2.1 创建

pipe函数
pipe函数产生两个文件描述符来表示管道两端(读和写)。
 

10.2.2 读写

read:
1. 如果管道有数据,读数据
2. 如果管道没有数据
   此时写端已经关闭,返回0
     如果写端没有关闭,阻塞等待
 
write:
1. 如果管道有空间,写数据,写入的数据长度依赖管道的buffer剩余的空间。如果剩余空间>=写入长度,那么数据全部写入,如果剩余空间<写入长度,那么写入剩余空间长度,并且write立即返回,返回值为写入的长度。
2. 如果管道没有剩余空间,那么阻塞。
3. 如果write时,读端已经关闭,那么程序产生一个SIGPIPE信号,导致程序终止。如果程序有处理SIGPIPE信号,那么程序不会终止,此时write返回-1,错误码标记为EPIPE。

10.2.3 应用

ps axu | grep a.out

单工:只能单方向通信
半双工:可以两个方向通信,但是同一时刻只能有一个方向通信
全双工:可以同时双方通信

10.3 命名管道

命名管道也是单工通信,但是比匿名相比,它可以用于非亲缘关系的进程。

10.3.1 创建

mkfifo 创建管道文件

10.3.2 打开读端

open("管道文件名",O_RDONLY);
如果此时没有其他进程打开写端,那么该open阻塞
 

10.3.3 打开写端

open("管道文件名", O_WRONLY);
 
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <fcntl.h>
  4. #include <sys/types.h>
  5.  
  6. int main()
  7. {
  8. // 打开文件时,添加非阻塞属性
  9. //int fd = open("/dev/input/mice", O_RDONLY | O_NONBLOCK);
  10.  
  11. // 先打开文件,再通过fcntl设置O_NONBLOCK属性
  12. int fd = open("/dev/input/mice", O_RDONLY);
  13.  
  14. int flags = fcntl(fd, F_GETFL);
  15. flags |= O_NONBLOCK;
  16. fcntl(fd, F_SETFL, flags);
  17.  
  18. while(1)
  19. {
  20. char buf[1024];
  21. int ret = read(fd, buf, sizeof(buf));
  22. if(ret == -1) // 错误发生
  23. {
  24. if(errno == EAGAIN || errno == EWOULDBLOCK) // EAGAIN错误码表示:底层没有数据,应该继续再尝试读 EWOULDBLOCK
  25. {
  26. //鼠标并没有移动,底层并没有数据可以读,这种不算真的错误
  27. printf("mouse not move\n");
  28. }
  29. else // 真的有错误发生了
  30. {
  31. return -1;
  32. }
  33. }
  34. }
  35. }

10.4 socketpair

socketpair和匿名管道类似,但是它是全双工的。

10.4.1 创建

int fd[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, fd);

10.5 mmap实现共享内存

unix提供了一些内存共享机制,但是还是习惯使用mmap进行内存共享。

man 7 shm_overview

10.5.1 有亲缘关系的进程之间mmap共享

有亲缘的关系的父子进程,可以使用匿名映射,直接将虚拟地址映射到内存。

10.5.2 无亲缘关系的进程之间mmap共享

如果进程之间没有亲缘关系,那么就需要一个文件来进行内存共享。

但是如果使用了硬盘文件,那么效率相对底下。最好使用内存文件来映射,效率更加高。

10.5.3 使用shm_open打开共享内存文件

shm_open:创建内存文件,路径要求类似/somename,以/起头,然后文件名,中间不能带/

10.6 文件锁

  1. #include <fcntl.h>
  2. #include <sys/types.h>
  3. #include <sys/file.h>
  4. #include <stdio.h>
  5. int main()
  6. {
  7. int fd = open("a.txt", O_RDWR);
  8.  
  9. // flock(fd, LOCK_SH); // 共享
  10. flock(fd, LOCK_EX); // 排他锁
  11.  
  12. // 可以对文件进行读操作
  13. sleep(10);
  14.  
  15. flock(fd, LOCK_UN); // 解锁
  16.  
  17. close(fd);
  18. }
  1. #include <fcntl.h>
  2. #include <sys/types.h>
  3. #include <sys/file.h>
  4. #include <stdio.h>
  5. int main()
  6. {
  7. int fd = open("a.txt", O_RDWR);
  8.  
  9. // flock(fd, LOCK_EX); // 排他锁
  10. int ret = flock(fd, LOCK_SH|LOCK_NB); // 共享锁
  11. if(ret == 0)
  12. {
  13.  
  14. printf("get lock\n");
  15. // flock(fd, LOCK_EX); // 排他锁
  16.  
  17. // 可以对文件进行读操作
  18. sleep(1);
  19.  
  20. flock(fd, LOCK_UN); // 解锁
  21. }
  22. else
  23. {
  24. printf("can not get lock\n");
  25. }
  26.  
  27. close(fd);
  28. }
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <fcntl.h>
  5. #include <sys/types.h>
  6. #include <sys/file.h>
  7.  
  8. int main()
  9. {
  10. int fd = open("a.txt", O_RDWR);
  11. pid_t pid = getpid();
  12. printf("process id is %d\n", (int)pid);
  13.  
  14. // 锁文件开始位置的4K内容
  15. struct flock l;
  16. l.l_type = F_WRLCK;
  17. l.l_whence = SEEK_SET;
  18. l.l_start = 0;
  19. l.l_len = 4096;
  20. fcntl(fd, F_SETLKW, &l); // F_SETLKW:锁文件,如果锁不上(原因:别人上锁了),就等
  21.  
  22. printf("get lock\n");
  23.  
  24. sleep(10);
  25.  
  26. // 解锁
  27. l.l_type = F_UNLCK;
  28. fcntl(fd, F_SETLKW, &l);
  29.  
  30. close(fd);
  31. }
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <fcntl.h>
  5. #include <sys/types.h>
  6. #include <sys/file.h>
  7.  
  8. int main()
  9. {
  10. int fd = open("a.txt", O_RDWR);
  11.  
  12. // 锁文件开始位置的4K内容
  13. struct flock l;
  14. l.l_type = F_WRLCK;
  15. l.l_whence = SEEK_SET;
  16. l.l_start = 1024;
  17. l.l_len = 4096;
  18.  
  19. fcntl(fd, F_GETLK, &l);
  20.  
  21. printf("pid = %d\n", (int)l.l_pid);
  22.  
  23. #if 0
  24. fcntl(fd, F_SETLKW, &l); // F_SETLKW:锁文件,如果锁不上(原因:别人上锁了),就等
  25.  
  26. printf("get lock\n");
  27.  
  28. sleep(10);
  29.  
  30. // 解锁
  31. l.l_type = F_UNLCK;
  32. fcntl(fd, F_SETLKW, &l);
  33. #endif
  34. close(fd);
  35. }

10.7 锁

pthread_mutex_init的锁,可以用于进程间同步,但是要求锁变量在共享内存中。

10.8 信号量

信号量用于计数,而不用考虑进程竞争问题。

Linux基础守护进程、高级IO、进程间通信的更多相关文章

  1. python实现Linux启动守护进程

    python实现Linux启动守护进程 DaemonClass.py代码: #/usr/bin/env python # -*- coding: utf-8 -*- import sys import ...

  2. linux C守护进程编写

    linux编程-守护进程编写 守护进程(Daemon)是运行在后台的一种特殊进程.它独立于控制终端并且周期性地执行某种任务或等待 处理某些发生的事件.守护进程是一种很有用的进程. Linux的大多数服 ...

  3. linux 创建守护进程的相关知识

    linux 创建守护进程的相关知识 http://www.114390.com/article/46410.htm linux 创建守护进程的相关知识,这篇文章主要介绍了linux 创建守护进程的相关 ...

  4. asp.net core2.0 部署centos7/linux系统 --守护进程supervisor(二)

    原文:asp.net core2.0 部署centos7/linux系统 --守护进程supervisor(二) 续上一篇文章:asp.net core2.0 部署centos7/linux系统 -- ...

  5. 【Linux】- 守护进程的启动方法

    转自:Linux 守护进程的启动方法 Linux中"守护进程"(daemon)就是一直在后台运行的进程(daemon). 本文介绍如何将一个 Web 应用,启动为守护进程. 一.问 ...

  6. 深入理解Linux操作系统守护进程的意义

    Linux服务器在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户.提供这些服务的程序是由运行在后台的守护进程(daemons)来执行的.守护进程 ...

  7. linux下守护进程的创建

    最近在学习linux c编程 看到了守护进程的创建,感觉很好玩, 测试环境ubuntu 15.04 下面贴出测试代码 #include <stdio.h> #include <std ...

  8. hadoop不能互相访问和linux防火墙守护进程

    前言——作为装过几次集群的菜鸟,对于hadoop集群的安装还是比较有心得的:只要配置文件够好,集群配置就非常容易,否则也容易出现莫名其妙的问题!总结了一份3台机器搭建较完好的集群的一份配置文件. 在我 ...

  9. Linux Supervisor 守护进程基本配置

    supervisor:C/S架构的进程控制系统,可使用户在类UNIX系统中监控.管理进程.常用于管理与某个用户或项目相关的进程. 组成部分supervisord:服务守护进程supervisorctl ...

  10. Linux之守护进程

    一.守护进程概述 在linux或者unix操作系统中在系统的引导的时候会开启很多服务,这些服务就叫做守护进 程.为了增加灵活性,root可以选择系统开启的模式,这些模式叫做运行级别,每一种运行级别以一 ...

随机推荐

  1. 关于aws cli命令的exit/return code分析

    最近总是收到一个备份脚本的失败邮件,脚本是之前同事写的,没有加入任何有调试信息,及有用的日志 于是去分析 ,脚本中有一条 aws s3 sync $srclocal  $dsts3 命令,然后根据这条 ...

  2. MatrixOne从入门到实践02——源码编译

    MatrixOne从入门到实践--源码编译 ​ 在部署MatrixOne前,我们可能会比较纠结使用哪个版本合适,MatrixOne在github上有各个版本的Releases,包含源码包和适用于Lin ...

  3. python2与python区别汇总

    目录 输入与输出 range使用区别 字符编码区别 输入与输出 python2与python3中两个关键字的区别 python2中 input方法需要用户自己提前指定数据类型 写什么类型就是什么类型 ...

  4. Docker | redis集群部署实战

    前面已经简单熟悉过redis的下载安装使用,今天接着部署redis集群(cluster),简单体会一下redis集群的高可用特性. 环境准备 Redis是C语言开发,安装Redis需要先将Redis的 ...

  5. 通过URL保存文件

    1 <?php 2 3 function dlfile($file_url, $save_to) 4 { 5 $content = file_get_contents($file_url); 6 ...

  6. dp优化 | 各种dp优化方式例题精选

    前言 本文选题都较为基础,仅用于展示优化方式,如果是要找题单而不是看基础概念,请忽略本文. 本文包含一些常见的dp优化("√"表示下文会进行展示,没"√"表示暂 ...

  7. webRTC demo

    准备: 信令服务 前端页面用于视频通话 demo github 地址. 前端页面 为了使 demo 尽量简单,功能页面如下,即包含登录.通过对方手机号拨打电话的功能.在实际生成过程中,未必使用的手机号 ...

  8. onps栈移植说明(2)——编译器及os适配层移植

    2. 字节对齐及基础数据类型定义 协议栈源码(码云/github)port/include/port/datatype.h中根据目标系统架构(16位 or 32位)及所使用的编译器定义基础数据类型及字 ...

  9. 动态爱心-详细教程(小白也会)(HTML)

    动态爱心 超级超级超级简单!!!赶紧做给你们的"Ta"看吧! (最后有详细步骤) 视频效果: 话不多说直接上代码 点击查看代码 <!DOCTYPE HTML PUBLIC & ...

  10. 还在为数据库事务一致性检测而苦恼?让Elle帮帮你,以TDSQL为例我们测测 | DB·洞见#7

    数据库用户通常依赖隔离级别来确保数据一致性,但很多数据库却并未达到其所表明的级别.主要原因是:一方面,数据库开发者对各个级别的理解有细微差异:另一方面,实现层面没有达到理论上的要求. 用户在使用或开发 ...