Linux C/C++ Notebook

Linux C/C++ · 01-31 · 1620 人浏览
Linux C/C++ Notebook

Linux C/C++ Notebook

  • @author Bill
  • @date 2024-01-29
说明
  • _public.h头文件汇集了C++常用操作类和方法

文件写入的正确操作

  • CFile类的OpenForRename()方法打开文件,这个方法会先打开一个临时文件,然后把数据写入到临时文件中(比如最终生成的文件名是city.csv,则临时文件名为city.csv.tmp
  • CFile类的CloseAndRename()方法关闭文件,这个方法会先关闭临时文件,然后把临时文件重命名为正式文件
  • 如果程序异常退出,临时文件会被删除,正式文件不会被生成
  • 如果程序正常退出,临时文件会被重命名为正式文件
  • 如果程序正常退出,但是正式文件已经存在,那么正式文件会被删除,临时文件会被重命名为正式文件

提示:实际操作中不一定是CFile类,根据所用框架和上述操作步骤调整代码。CFile类在头文件_public.h中有定义。

Linux信号

在Linux中,信号是一种异步通知机制,用于在进程间或由内核向进程发送事件通知。信号可以用于处理各种事件,例如错误、中断、外部事件等。在C语言中,可以使用信号处理函数来捕获和处理信号。

以下是一些常见的信号及其含义:

  1. SIGABRT(0):在Unix/Linux系统中,发送信号0的主要作用是检查进程是否仍然存在。发送信号0时,系统不会真正给进程发送一个信号,而是检查进程是否仍然存在。如果进程存在,系统会执行相应的检查并返回成功;如果进程不存在,系统返回失败。

    这一机制通常用于检查进程是否处于活动状态,而无需实际干扰其正常运行。这在编写一些脚本或程序时很有用,以确保进程在继续其他操作之前仍然存在。

    例如,可以使用kill -0命令来检查指定进程是否存在,如下所示:

    kill -0 <pid>

    这里,<pid>是要检查的进程的进程ID。如果进程存在,该命令将返回成功,否则返回失败。

  2. SIGINT (2):由终端键盘中断字符(通常是Ctrl+C)产生,用于中断当前进程。
  3. SIGTERM (15):用于请求进程正常终止,通常由kill命令发送。
  4. SIGKILL (9):用于强制终止进程,进程无法捕获和忽略这个信号
  5. SIGSEGV (11):表示进程访问了无效的内存地址,通常是因为编程错误导致的段错误。
  6. SIGALRM (14):定时器超时信号,可以通过alarmsetitimer设置定时器后产生。
  7. SIGHUP (1):挂起信号,通常在终端关闭时发送给与该终端关联的进程。
  8. SIGCHLD (17):子进程状态变化,通常在子进程终止时由父进程接收。
  9. SIGUSR1 (10)SIGUSR2 (12):用户定义的信号,可以由进程自定义使用。

在C语言中,使用signal函数来注册信号处理函数。signal函数的原型如下:

#include <signal.h>

void (*signal(int signum, void (*handler)(int)))(int);

其中,signum 是要处理的信号编号,handler 是一个指向处理函数的指针。处理函数的原型通常是 void handler(int signum)

以下是一个简单的例子,演示如何使用 signal 函数捕获并处理 SIGINT 信号:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>

// 信号处理函数
void handle_sigint(int signum) {
    printf("Caught SIGINT, exiting...\n");
    exit(EXIT_SUCCESS);
}

int main() {
    // 注册信号处理函数
    signal(SIGINT, handle_sigint);

    printf("Press Ctrl+C to send SIGINT...\n");

    // 模拟程序运行
    while (1) {
        // 无限循环等待信号
    }

    return 0;
}

在这个例子中,程序注册了对 SIGINT 信号的处理函数 handle_sigint。当用户按下 Ctrl+C 时,程序将收到 SIGINT 信号,然后执行相应的处理函数。请注意,信号处理函数应该是尽可能简短和可靠的,以避免在信号处理期间发生不可预测的行为。

Linux多进程

写入缓冲区的问题

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 多进程支持
#include <unistd.h>

int main()
{

  FILE *fp = fopen("./test.txt", "w+");
  fprintf(fp, "FILE *fp = fopen(\"./test.txt\", \"w+\")\n");

  int pid = fork();

  if (pid == 0)
  {
    printf("child process, pid=%d\n", getpid());
    fprintf(fp, "child process, pid=%d\n", getpid());
  }

  if (pid > 0)
  {
    printf("parent process, pid=%d\n", getpid());
    fprintf(fp, "parent process, pid=%d\n", getpid());
  }

  fclose(fp);

  return 0;
}

运行上述代码,结果如下:

[bill@localhost c]$ g++ -o process process1.cpp
[bill@localhost c]$ ./process
parent process, pid=104181
child process, pid=104182

打开test.txt文件内容如下:

FILE *fp = fopen("./test.txt", "w+")
parent process, pid=104181
FILE *fp = fopen("./test.txt", "w+")
child process, pid=104182

Q&A

Q:主进程明明只写入了一次FILE *fp = fopen("./test.txt", "w+"),但是文本文件中却有两行?

A:FILE *fp = fopen("./test.txt", "w+")文本长度过短,文本实际上只放到了程序的缓冲区(内存)中,只要在11行代码下一行插入fflush(fp),在fork()之前刷新缓冲区即可解决。

Q:如何避免僵尸进程的出现?

A:

  • 方法一:忽略SIGCHLD信号

    #include <signal.h>
    int main()
    {
        signal(SIGCHLD, SIG_IGN);
        // 业务代码
        return 0;
    }
  • 方法二:父进程等待子进程结束,但是此方法会让父进程一直阻塞等待

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    // 多进程支持
    #include <unistd.h>
    #include <signal.h>
    // wait
    #include <sys/types.h>
    #include <sys/wait.h>
    
    int main()
    {
      int pid = fork();
    
      // 业务代码
    
      if (pid > 0)
      {
        printf("parent process, pid=%d\n", getpid());
        fprintf(fp, "parent process, pid=%d\n", getpid());
        int status;
        wait(&status); // 等待子进程结束
      }
    
      // 业务代码
      return 0;
    }
  • 方法三:结合方法一、二,在捕捉到SIGCHLD信号后调用wait()方法,这样可以解决父进程阻塞等待的问题

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    // 多进程支持
    #include <unistd.h>
    #include <signal.h>
    
    #include <sys/types.h>
    #include <sys/wait.h>
    
    void waitHandler(int sig)
    {
      int status;
      wait(&status); // 等待子进程结束
    }
    
    int main()
    {
      signal(SIGCHLD, waitHandler);
      int pid = fork();
      if (pid == 0)
      {
        printf("child process, pid=%d\n", getpid());
      }
      if (pid > 0)
      {
        printf("parent process, pid=%d\n", getpid());
      }
      return 0;
    }

说明

  • 子进程获得了父进程的数据空间、堆栈的副本,不是共享
  • 父进程中打开的文件描述符也被复制到子进程中
  • 如果父进程先退出,子进程会变成孤儿进程
  • 如果子进程先退出,内核向父进程发送SIGCHLD信号,如果父进程不处理这个信号,子进程会变成僵尸进程
  • 如果子进程在父进程之前终止,内核为每个子进程保留了一个数据结构,包括进程编号、终止状态和使用cpu时间等,父进程如果处理了子进程退出的信息,内核就会释放这个数据结构,如果父进程没处理子进程退出的信息,内核就不会释放这个数据结构,子进程进程编号就会一直被占用,但是系统可用的进程号是有限的,如果大量的产生僵尸死进程,将因为没有可用的进程号而导致系统不能产生新的进程,这就是僵尸进程的危害。
  • 如果父进程先退出,子进程会变成孤儿进程,会被init进程(1号进程)接管,由init进程回收
  • 如果子进程先退出,父进程会收到SIGCHLD信号,可以通过wait函数回收子进程,也可以通过signal函数注册信号处理函数回收子进程,否则子进程会变成僵尸进程

服务程序的调度

  • [x] 周期性的启动后台程序
  • [x] 常驻内存中的服务程序异常终止,在短时间内重启

execl

#include <unistd.h>

int execl(const char *path, const char *arg0, ... /*, (char *) NULL */ );

其中的参数意义如下:

  • path: 要执行的新程序的路径。
  • arg0, arg1, ...: 新程序的命令行参数,以字符串形式传递。arg0 一般用于传递新程序的名称,而后续的参数用于传递命令行参数。参数列表必须以一个空指针 (char *) NULL 结尾。

简单来说,execl 的参数是一个以 path 开始的字符串序列,最后以 NULL 结尾。

例如,如果你想在当前进程中执行 /bin/ls 程序,并传递参数 -l,可以使用如下的调用:

execl("/usr/bin/ls", "/usr/bin/ls", "-l", (char *) NULL);

在这个调用中,"/usr/bin/ls" 是新程序的路径,"/usr/bin/ls" 是新程序的名称(在 argv[0] 中),"-l" 是新程序的参数。最后的 (char *) NULL 表示参数列表的结束。

请注意,execl 函数的参数是一个变长参数列表,因此实际的参数数量是可变的,但是必须以 NULL 结尾。在传递参数时,需要按照参数在命令行中的顺序逐个列出。

  • 为什么execl()前两个参数值相同?

execl 中,argv[0] 通常用于指定新程序的名称。如果你不在参数列表中明确指定 argv[0],那么系统会假设 argv[0] 为新程序的路径(在这个例子中就是 "/usr/bin/ls")。因此,如果你只关心执行的程序是什么而不关心在 argv[0] 中是什么,那么 execl("/usr/bin/ls", "-l", (char *) NULL); 就足够了。

实际上,很多时候,程序员会选择使用 execl("/usr/bin/ls", "/usr/bin/ls", "-l", (char *) NULL); 来显式指定 argv[0],以提高代码的可读性。这样做可以清晰地表达新程序的名称,使代码更易于理解。

总体而言,两者都是合法的,可以根据个人偏好来选择。

exec是用参数中的命令替换当前进程(的正文段、数据段、堆栈),替换成功后,当前进程就变成了新的进程,不会再执行exec后面的代码。如果exec执行失败,当前进程还是原来的进程,会继续执行exec后面的代码。
  • 执行正确的exec命令
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{
  printf("before exec\n");
  // 正确的exec命令
  execl("/usr/bin/ls", "ls", "-l", (char *)NULL);
  printf("after exec\n", ret);
  return 0;
}

运行结果

[bill@localhost c]$ ./procctl1
before exec
总用量 20
-rw-rw-r--. 1 bill bill   91 1月  29 10:35 makefile
-rwxrwxr-x. 1 bill bill 8416 1月  29 10:54 procctl1
-rw-rw-r--. 1 bill bill  580 1月  29 10:54 procctl1.cpp
  • 执行错误的exec命令
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main()
{
  printf("before exec\n");
  // 错误的exec命令
  int ret = execl("/usr/bin/lss", "ls", "-l", (char *)NULL);
  printf("after exec, ret=%d\n", ret);
  return 0;
}

运行结果

[bill@localhost c]$ ./procctl1
before exec
after exec, ret=-1

使用fork()进行进程调度

  • 1、先执行fork()函数创建子进程,在子进程中调用execl()函数执行新的程序
  • 2、execl()会替换子进程的正文段、数据段、堆栈,子进程会变成新的进程,不会再执行fork()后面的代码
  • 3、在父进程中调用wait()函数等待子进程的运行结果
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
  while (true)
  {
    // 子进程中调用execl()函数执行新的程序
    if (fork() == 0)
    {
      execl("/usr/bin/ls", "ls", "-l", (char *)NULL);
    }
    else
    {
      // 父进程
      int status;
      wait(&status);
      sleep(10);
    }
  }
  return 0;
}

运行结果

[bill@localhost c]$ ./procctl1
总用量 20
-rw-rw-r--. 1 bill bill   91 1月  29 10:35 makefile
-rwxrwxr-x. 1 bill bill 8512 1月  29 11:09 procctl1
-rw-rw-r--. 1 bill bill 1300 1月  29 11:08 procctl1.cpp
总用量 20
-rw-rw-r--. 1 bill bill   91 1月  29 10:35 makefile
-rwxrwxr-x. 1 bill bill 8512 1月  29 11:09 procctl1
-rw-rw-r--. 1 bill bill 1300 1月  29 11:08 procctl1.cpp
# ...(每10s输出一次)...

execv

#include <unistd.h>

int execv(const char *path, char *const argv[]);

其中:

  • path: 要执行的新程序的路径
  • argv: 一个以空指针 (char *) NULL 结尾的字符串数组,用于传递命令行参数
  • 当传入参数不固定时,可以使用execv()方法,execv支持不确定个数的参数
  • 当使用execl()时,可能遇到如下情况:
if (argc == 3)
    execl(argv[2], argv[2], (char *)NULL);
if (argc == 4)
    execl(argv[2], argv[2], argv[3], (char *)NULL);
if (argc == 5)
    execl(argv[2], argv[2], argv[3], argv[4], (char *)NULL);
if (argc == 6)
    execl(argv[2], argv[2], argv[3], argv[4], argv[5], (char *)NULL);

由于不确定外部传入多少个参数,代码过于冗余,此时可以使用execv

execv(argv[2], argv + 2);

实现定时调度器

  • 使用示例:

    ./procctl 5 /usr/bin/ls -lt /home/bill
  • Usage: ./procctl interval(sec) program [arg1] [arg2] ...
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
  if (argc < 3)
  {
    printf("usage: ./procctl interval(sec) program [arg1] [arg2] ...\n");
    printf("example: /home/bill/project/tools1/bin/procctl 5 /usr/bin/ls -lt /home/bill\n\n");

    printf("本程序是服务程序的调度程序,周期性启动服务程序或shell脚本。\n");
    printf("timetvl 运行周期,单位:秒。被调度的程序运行结束后,在timetvl秒后会被procctl重新启动。\n");
    printf("program 被调度的程序名,必须使用全路径。\n");
    printf("argvs   被调度的程序的参数。\n");
    printf("注意,本程序不会被kill杀死,但可以用kill -9强行杀死。\n\n\n");
    return -1;
  }

  // 忽略掉全部信号,关掉IO
  for (int i = 0; i < 64; i++)
  {
    signal(i, SIG_IGN);
    // 关掉IO
    close(i);
  }

  if (fork() > 0)
  {
    // 父进程退出
    exit(0);
  }

  signal(SIGCHLD, SIG_DFL);

  while (true)
  {
    // 子进程中调用execl()函数执行新的程序
    if (fork() == 0)
    {
      execv(argv[2], argv + 2);
      exit(0); // 如果execv执行失败,子进程会变成僵尸进程,所以这里要退出
    }
    else
    {
      // 父进程
      int status;
      wait(&status);
      sleep(atoi(argv[1]));
    }
  }
  return 0;
}

在这个守护进程的设计中,涉及到了三层进程关系:

  1. 父进程(Original Parent): 初始启动的由用户发起的进程。在第一次调用 fork() 后,这个进程退出,但是留下了子进程1(守护进程)。
  2. 子进程1(Daemon Process): 由父进程生成的第一个子进程,在 fork() 之后立即退出。成为守护进程,负责周期性地启动子进程2执行指定的程序。
  3. 子进程2(Executed Program): 由子进程1通过 fork() 生成的进程,执行指定的程序(例如通过 execv 启动的外部程序)。当这个程序执行完成后,子进程2退出,发送 SIGCHLD 信号给父进程(子进程1)。

整个进程关系如下:

  • 初始启动:父进程

    • 第一次fork():生成子进程1(守护进程),并立即退出

      • 循环中的fork():生成子进程2(执行指定的程序),执行完成后退出,发送SIGCHLD信号

        • 父进程(子进程1):捕获 SIGCHLD 信号,等待子进程2终止,然后继续循环

这种三层关系确保了父进程(子进程1)的退出不影响守护进程的运行,而守护进程可以在后台周期性地启动并等待子进程2的终止。

Linux共享内存

实现步骤:

  • 1、使用shmget()函数创建共享内存
  • 2、使用shmat()函数将共享内存映射到进程的地址空间
  • 3、使用shmdt()函数将共享内存从进程的地址空间中分离
  • 4、使用shmctl()函数可以使用cmd=IPC_RMID删除共享内存

shmget

shmget 是一个用于获取共享内存段的系统调用,通常用于进程间通信(IPC)。在Unix/Linux系统中,共享内存是一块被多个进程共享的内存区域,允许它们直接读写其中的数据,从而实现进程之间的协作。

#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  • key:共享内存段的关键字,用于标识共享内存段。
  • size:共享内存段的大小(以字节为单位)。
  • shmflg:标志位,用于指定共享内存的访问权限和行为。

函数返回值:

  • 成功时返回一个正整数,表示共享内存段的标识符(ID)。
  • 失败时返回 -1,并设置 errno 表示具体的错误类型。

在Linux系统中可以使用ipcsipcrm查看和删除共享内存:

bill@205356368e4f:~/project/other-demo/c$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00005005 0          bill       640        56         0

bill@205356368e4f:~/project/other-demo/c$ ipcrm -m 0
bill@205356368e4f:~/project/other-demo/c$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
  • ipcrm -m shmid可以删除对应的共享内存
  • key:共享内存段的键值,这是用于标识 IPC 对象的关键字。
  • shmid:共享内存段的标识符,是一个唯一的整数,由系统分配。
  • owner:拥有该共享内存段的进程或用户的用户名。
  • perms:共享内存段的权限,以八进制表示。例如,640 表示读写权限为 6(读权限)和 4(写权限),权限数字之间按位组合而成。
  • bytes:共享内存段的大小,以字节为单位。
  • nattch:当前附加到共享内存段的进程数(即使用该共享内存的进程数)。
  • status:表示共享内存段的状态。通常是 0 或者其他状态码,表示共享内存段是否被使用等信息。

shmat

shmat 是一个用于将共享内存段附加到进程地址空间的系统调用,通常用于进程间共享内存的操作。它允许进程直接访问共享内存区域中的数据。

#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • shmid:共享内存段的标识符,即通过 shmget 获得的标识符。
  • shmaddr:指定共享内存段附加到进程地址空间的地址。通常设置为 NULL,由系统自动选择合适的地址。
  • shmflg:标志位,用于指定共享内存的访问权限和行为。

函数返回值:

  • 成功时返回一个 void 指针,指向共享内存段附加到进程地址空间的起始地址。
  • 失败时返回 (void *) -1,并设置 errno 表示具体的错误类型。

shmdt

shmdt 函数用于将共享内存段从当前进程的地址空间中分离。它接受一个参数,即指向共享内存段附加到进程地址空间的起始地址的指针。

#include <sys/shm.h>

int shmdt(const void *shmaddr);
  • shmaddr:指向共享内存段附加到进程地址空间的起始地址。

函数返回值:

  • 成功时返回 0。
  • 失败时返回 -1,并设置 errno 表示具体的错误类型。

shmctl

shmctl 是一个用于控制共享内存段的系统调用。它可以执行多种操作,例如获取共享内存信息、设置共享内存权限、删除共享内存等。

#include <sys/shm.h>

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • shmid:共享内存段的标识符,是通过 shmget 获取的。
  • cmd:控制命令,指定 shmctl 要执行的操作。
  • buf:指向 shmid_ds 结构的指针,用于传递或获取共享内存段的信息。

函数返回值:

  • 成功时返回操作相关的信息或 0。
  • 失败时返回 -1,并设置 errno 表示具体的错误类型。

下面是一些常用的 cmd 值和相应的操作:

  • IPC_STAT:获取共享内存段的状态信息,将信息存储在 buf 中。
  • IPC_SET:设置共享内存段的状态,使用 buf 中的信息。
  • IPC_RMID:删除共享内存段。

Demo

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

struct st_pid
{
  int pid;       // 进程编号
  char name[51]; // 进程名称
};

int main(int argc, char *argv[])
{
  if (argc < 2)
  {
    printf("Usage: ./share-memory memoryname\n");
    return -1;
  }

  // 共享内存标志
  int shmid;

  if ((shmid = shmget(0x5005, sizeof(struct st_pid), 0640 | IPC_CREAT)) == -1)
  {
    printf("shmget(0x5005) failed\n");
    return -1;
  }

  // 指向共享内存地址的变量
  struct st_pid *stpid;

  if ((stpid = (struct st_pid *)shmat(shmid, 0, 0)) == (void *)-1)
  {
    printf("shmat failed\n");
    return -1;
  }

  // 读取共享内存
  printf("stpid->pid = %d\n", stpid->pid);
  printf("stpid->name = %s\n", stpid->name);

  // 操作共享内存
  stpid->pid = getpid();
  strcpy(stpid->name, argv[1]);

  // 把共享内存从当前进程中分离
  shmdt(stpid);

  // 删除共享内存
  // if (shmctl(shmid, IPC_RMID, NULL) == -1)
  // {
  //   printf("shmctl failed\n");
  // }

  return 0;
}
  • 为了方便展示,把删除共享内存代码做了注释
  • 编译运行,最后手动删掉共享内存
bill@205356368e4f:~/project/other-demo/c$ g++ -o share-memory share-memory.cpp
bill@205356368e4f:~/project/other-demo/c$ ./share-memory aaa
stpid->pid = 0
stpid->name =
bill@205356368e4f:~/project/other-demo/c$ ./share-memory bbb
stpid->pid = 110540
stpid->name = aaa
bill@205356368e4f:~/project/other-demo/c$ ./share-memory ccc
stpid->pid = 110553
stpid->name = bbb
bill@205356368e4f:~/project/other-demo/c$ ./share-memory ddd
stpid->pid = 110584
stpid->name = ccc
bill@205356368e4f:~/project/other-demo/c$ ./share-memory eee
stpid->pid = 110603
stpid->name = ddd
bill@205356368e4f:~/project/other-demo/c$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00005005 2          bill       640        56         0

bill@205356368e4f:~/project/other-demo/c$ ipcrm -m 2
bill@205356368e4f:~/project/other-demo/c$ ipcs -m

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

共享内存唯一标识

  • int shmid = shmget(key, 1024, IPC_CREAT | 0666),可以看到共享内存的唯一标识符是key,如果两个不相干的程序使用了相同的 key 值创建共享内存区域,就会发生冲突,导致数据错乱或不可预测的行为。为了避免这种情况,需要确保在使用 ftok() 函数生成 key 值时,提供的文件路径和字符是唯一的。通常建议使用程序自己的文件路径和一个唯一的字符来生成 key 值,这样可以确保不同程序生成的 key 值是唯一的。另外,为了进一步确保不同程序之间的 key 值不冲突,可以在程序中定义一个特定的 key 值,而不是依赖于动态生成的 key 值。这样就可以避免由于不同程序生成的 key 值相同而导致的冲突问题。
  • ftok() 函数是用于生成一个 System V IPC(Inter-Process Communication,进程间通信)所需的键值的函数。这个键值通常用于标识共享内存、消息队列和信号量等 IPC 对象。
  • ftok() 函数的原型如下所示:
key_t ftok(const char *pathname, int proj_id);

其中,pathname 是一个指向文件路径名的指针,而 proj_id 是一个用户定义的整数,通常是一个小于或等于 255 的正整数。

ftok() 函数通过将 pathname 参数所指向的文件的索引节点号和 proj_id 参数组合成一个唯一的键值。这个键值用于创建或获取 IPC 对象。

ftok() 函数返回一个键值,如果发生错误,则返回 -1,并设置 errno 来指示错误类型。

值得注意的是,ftok() 函数的生成键值的方法并不是十分安全,特别是在多线程环境或者频繁创建文件的情况下,可能会导致冲突。因此,应该谨慎使用,并确保在生成键值时,pathname 参数指向的文件是唯一的,并且 proj_id 参数是固定的且不易冲突的。

Linux信号量

  • 使用信号量给共享资源加锁,防止出现线程不安全问题
  • 信号量本质上是一个非负数(>0)的计数器
  • 给共享资源建立一个标志,表示该共享内存被占用的情况
  • P操作(资源-1)、V操作(资源+1)

CSEM信号量操作类使用说明

1. 创建 CSEM 对象

CSEM sem;

2. 初始化信号量

// key: 信号量的键值
// value: 信号量的初始值,默认为1
// sem_flg: 信号量的标志,默认为SEM_UNDO
bool initSuccess = sem.init(key, value, sem_flg);
if (!initSuccess) {
    // 处理初始化失败的情况
    perror("Semaphore initialization failed");
    return -1;
}

3. 使用 P 操作(获取信号量)

// sem_op: P 操作的值,通常为-1
bool pSuccess = sem.P(sem_op);
if (!pSuccess) {
    // 处理 P 操作失败的情况
    perror("P operation failed");
    return -1;
}

4. 使用 V 操作(释放信号量)

// sem_op: V 操作的值,通常为1
bool vSuccess = sem.V(sem_op);
if (!vSuccess) {
    // 处理 V 操作失败的情况
    perror("V operation failed");
    return -1;
}

5. 获取信号量的当前值

int semValue = sem.value();
if (semValue == -1) {
    // 处理获取信号量值失败的情况
    perror("Failed to get semaphore value");
    return -1;
}

6. 销毁信号量

bool destroySuccess = sem.destroy();
if (!destroySuccess) {
    // 处理销毁信号量失败的情况
    perror("Semaphore destruction failed");
    return -1;
}

7. 注意事项

  • 信号量的键值 key 应该在进程间共享,并保证唯一性。
  • 在使用信号量前需要初始化,可以使用 init 方法。
  • P 操作用于获取信号量,V 操作用于释放信号量。
  • 使用完信号量后,应该通过 destroy 方法来销毁信号量。

这个类主要用于在多进程之间实现临界区的同步控制,确保在共享资源的访问中只有一个进程可以进行。在使用时要特别注意初始化和销毁的时机,以及合适地使用 P 操作和 V 操作。

CSEM信号量操作类源码

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>

// 信号量。
class CSEM
{
private:
  union semun // 用于信号量操作的共同体。
  {
    int val;
    struct semid_ds *buf;
    unsigned short *arry;
  };

  int m_semid; // 信号量描述符。

  // 如果把sem_flg设置为SEM_UNDO,操作系统将跟踪进程对信号量的修改情况,
  // 在全部修改过信号量的进程(正常或异常)终止后,操作系统将把信号量恢
  // 复为初始值(就像撤消了全部进程对信号的操作)。
  // 如果信号量用于表示可用资源的数量(不变的),设置为SEM_UNDO更合适。
  // 如果信号量用于生产消费者模型,设置为0更合适。
  // 注意,网上查到的关于sem_flg的用法基本上是错的,一定要自己动手多测试。
  short m_sem_flg;

public:
  CSEM();
  // 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
  bool init(key_t key, unsigned short value = 1, short sem_flg = SEM_UNDO);
  bool P(short sem_op = -1); // 信号量的P操作。
  bool V(short sem_op = 1);  // 信号量的V操作。
  int value();               // 获取信号量的值,成功返回信号量的值,失败返回-1。
  bool destroy();            // 销毁信号量。
  ~CSEM();
};

CSEM::CSEM()
{
  m_semid = -1;
  m_sem_flg = SEM_UNDO;
}

// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
bool CSEM::init(key_t key, unsigned short value, short sem_flg)
{
  if (m_semid != -1)
    return false;

  m_sem_flg = sem_flg;

  // 信号量的初始化不能直接用semget(key,1,0666|IPC_CREAT),因为信号量创建后,初始值是0。

  // 信号量的初始化分三个步骤:
  // 1)获取信号量,如果成功,函数返回。
  // 2)如果失败,则创建信号量。
  // 3) 设置信号量的初始值。

  // 获取信号量。
  if ((m_semid = semget(key, 1, 0666)) == -1)
  {
    // 如果信号量不存在,创建它。
    if (errno == 2)
    {
      // 用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取。
      if ((m_semid = semget(key, 1, 0666 | IPC_CREAT | IPC_EXCL)) == -1)
      {
        if (errno != EEXIST)
        {
          perror("init 1 semget()");
          return false;
        }
        if ((m_semid = semget(key, 1, 0666)) == -1)
        {
          perror("init 2 semget()");
          return false;
        }

        return true;
      }

      // 信号量创建成功后,还需要把它初始化成value。
      union semun sem_union;
      sem_union.val = value; // 设置信号量的初始值。
      if (semctl(m_semid, 0, SETVAL, sem_union) < 0)
      {
        perror("init semctl()");
        return false;
      }
    }
    else
    {
      perror("init 3 semget()");
      return false;
    }
  }

  return true;
}

bool CSEM::P(short sem_op)
{
  if (m_semid == -1)
    return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;     // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = sem_op; // P操作的sem_op必须小于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid, &sem_b, 1) == -1)
  {
    perror("p semop()");
    return false;
  }

  return true;
}

bool CSEM::V(short sem_op)
{
  if (m_semid == -1)
    return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;     // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = sem_op; // V操作的sem_op必须大于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid, &sem_b, 1) == -1)
  {
    perror("V semop()");
    return false;
  }

  return true;
}

// 获取信号量的值,成功返回信号量的值,失败返回-1。
int CSEM::value()
{
  return semctl(m_semid, 0, GETVAL);
}

bool CSEM::destroy()
{
  if (m_semid == -1)
    return false;

  if (semctl(m_semid, 0, IPC_RMID) == -1)
  {
    perror("destroy semctl()");
    return false;
  }

  return true;
}

CSEM::~CSEM()
{
}

关于SEM_UNDO

当在信号量上执行 P(wait)和 V(signal)操作时,如果进程在执行这些操作的过程中由于某些原因(如收到信号、进程异常终止等)被中断,可能会导致信号量的值发生变化,但系统无法自动恢复信号量到操作前的状态。这就是为什么引入 SEM_UNDO 标志的原因。

SEM_UNDO 标志用于启用信号量的撤销机制。当设置了 SEM_UNDO 标志后,在进程对信号量执行 P 操作时,系统将会记住进程的 PID,并在进程终止时自动执行 V 操作,以恢复信号量的值。

具体来说,使用 SEM_UNDO 标志时,在进程执行 P 操作时,系统将会为该进程关联一个撤销值(undo value)。如果该进程异常终止,系统会根据该撤销值自动执行 V 操作,恢复信号量的值。

这样做的目的是确保在进程异常退出的情况下,系统能够自动撤销该进程对信号量的操作,防止因为进程异常退出而导致信号量值的错误。在多进程环境中,信号量的撤销机制有助于维护系统的稳定性和一致性。

使用CMES类对共享内存进行加锁

// share-memory.cpp

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <_public.h>

CSEM sem; // 给共享内存加锁的信号量

struct st_pid
{
  int pid;       // 进程编号
  char name[51]; // 进程名称
};

int main(int argc, char *argv[])
{
  if (argc < 2)
  {
    printf("Usage: ./share-memory memoryname\n");
    return -1;
  }

  // 共享内存标志
  int shmid;

  if ((shmid = shmget(0x5005, sizeof(struct st_pid), 0640 | IPC_CREAT)) == -1)
  {
    printf("shmget(0x5005) failed\n");
    return -1;
  }

  // 如果信号量已存在,获取信号量;信号量不存在,则创建并初始化为value,缺省值为1
  if (sem.init(0x5005) == false)
  {
    printf("sem.init(0x5005) failed\n");
    return -1;
  }

  // 指向共享内存地址的变量
  struct st_pid *stpid;

  if ((stpid = (struct st_pid *)shmat(shmid, 0, 0)) == (void *)-1)
  {
    printf("shmat failed\n");
    return -1;
  }

  printf("Before sem.P(), time=%ld, sem.value=%d\n", time(0), sem.value());
  // P操作,加锁
  sem.P();
  printf("After sem.P(), time=%ld, sem.value=%d\n", time(0), sem.value());

  // 读取共享内存
  printf("stpid->pid = %d\n", stpid->pid);
  printf("stpid->name = %s\n", stpid->name);

  // 操作共享内存
  stpid->pid = getpid();
  sleep(10); // 为了测试加锁
  strcpy(stpid->name, argv[1]);

  printf("Before sem.V(), time=%ld, sem.value=%d\n", time(0), sem.value());
  // V操作
  sem.V();
  printf("After sem.V(), time=%ld, sem.value=%d\n", time(0), sem.value());

  // 把共享内存从当前进程中分离
  shmdt(stpid);

  // 删除共享内存
  if (shmctl(shmid, IPC_RMID, NULL) == -1)
  {
    printf("shmctl failed\n");
  }

  return 0;
}
  • 只运行一个进程
$ ./share-memory mn
Before sem.P(), time=1707071697, sem.value=1
After sem.P(), time=1707071697, sem.value=0
stpid->pid = 24904
stpid->name = mn
Before sem.V(), time=1707071707, sem.value=0
After sem.V(), time=1707071707, sem.value=1
  • 打开两个终端,先后(相差10s以内)运行两个进程

    • 终端1(先运行)

      $ ./share-memory mn
      Before sem.P(), time=1707078282, sem.value=1
      After sem.P(), time=1707078282, sem.value=0
      stpid->pid = 0
      stpid->name =
      Before sem.V(), time=1707078292, sem.value=0
      After sem.V(), time=1707078292, sem.value=0
    • 终端2(后运行)

      ./share-memory mn
      Before sem.P(), time=1707078283, sem.value=0
      After sem.P(), time=1707078292, sem.value=0
      stpid->pid = 64858
      stpid->name = mn
      Before sem.V(), time=1707078302, sem.value=0
      After sem.V(), time=1707078302, sem.value=1
      shmctl failed
    • 先运行的终端1,Before sem.P(), time=1707078282, sem.value=1,,由于终端1正在占用共享内存,因此终端2启动时,Before sem.P(), time=1707078283, sem.value=0
    • 终端1释放锁信号量时,After sem.V(), time=1707078292, sem.value=0,是因为释放的信号量立刻被终端2占用了

进程的心跳机制

在下一章节的守护进程中会用到心跳机制来判定服务程序的状态

// 进程心跳信息的结构体
struct st_procinfo
{
  int pid;        // 进程ID
  char pname[64]; // 进程名
  int timeout;    // 超时时间
  time_t atime;   // 最后一次心跳时间
};
  • 服务程序在共享内存中维护自己的心跳信息
  • 开发守护程序,终止已经死机的服务程序

PAction心跳工具类类使用说明

心跳类(PAction)是一个用于实现守护进程心跳功能的C++类。该类利用共享内存和信号量机制,实现了守护进程之间的心跳信息共享和更新。

公有成员函数

bool AddPinfo(const int timeout, const char *pname)

  • 描述:

    • 添加心跳信息到共享内存中。
  • 参数:

    • timeout: 心跳超时时间。
    • pname: 进程名。
  • 返回值:

    • 成功返回 false,失败返回 true

bool UptATime()

  • 描述:

    • 更新共享内存中当前进程的心跳信息的时间。
  • 返回值:

    • 成功返回 false

调用Demo

#include <unistd.h>

int main(int argc, char *argv[])
{
  PAction action;                        // 创建一个心跳对象
  action.AddPinfo(30, "test-heartbeat"); // 添加心跳信息,超时时间为30秒,进程名为test-heartbeat
  while (true)
  {
    action.UptATime(); // 更新心跳时间
    sleep(2);          // 模拟进程的其他工作
  }

  return 0;
}

PAction类源码

  • 信号量用到了上个章节写的CSEM
#include <stdio.h>
#include <unistd.h>
#include <_public.h>

#define MAXNUMP_ 1000   // 最大守护进程个数,1000个已经足够了
#define SHMKEYP_ 0x1234 // 共享内存的key
#define SEMKEYP_ 0x1234 // 信号量的key

// 进程心跳信息的结构体
struct st_pinfo
{
  int pid;        // 进程ID
  char pname[64]; // 进程名
  int timeout;    // 超时时间
  time_t atime;   // 最后一次心跳时间
};

class PAction
{
private:
  CSEM m_sem;             // 给共享内存加锁的信号量
  int m_shmid;            // 共享内存ID
  struct st_pinfo *m_shm; // 共享内存指针
  int m_pos;              // 当前进程在共享内存中的位置

public:
  PAction();
  bool AddPinfo(const int timeout, const char *pname); // 添加心跳信息
  bool UptATime();                                     // 更新心跳时间
  ~PAction();                                          // 析构函数,删除共享内存中的心跳信息
};

PAction::PAction()
{
  m_shm = nullptr;
  m_pos = -1;
  m_shmid = 0;
}

bool PAction::AddPinfo(const int timeout, const char *pname)
{
  // 创建/获取共享内存,大小为n*sizeof(st_pinfo),n为要守护的进程个数
  if ((m_shmid = shmget(SHMKEYP_, MAXNUMP_ * sizeof(st_pinfo), 0640 | IPC_CREAT)) == -1)
  {
    printf("shmget(%x) failed\n", SHMKEYP_);
    return -1;
  }

  // 初始化共享内存信号量
  CSEM m_sem;
  if (m_sem.init(SEMKEYP_) == false)
  {
    printf("m_sem.init(%x) error\n", SEMKEYP_);
    return -1;
  }

  // 获取共享内存的指针(内存地址)
  m_shm = (struct st_pinfo *)shmat(m_shmid, 0, 0);

  // 创建当前进程心跳信息
  struct st_pinfo *pinfo = new struct st_pinfo;
  memset(pinfo, 0, sizeof(st_pinfo));                     // 初始化心跳信息
  pinfo->pid = getpid();                                  // 当前进程ID
  STRNCPY(pinfo->pname, sizeof(pinfo->pname), pname, 64); // 安全的字符串拷贝
  pinfo->timeout = timeout;                               // 超时时间为30秒
  pinfo->atime = time(NULL);                              // 当前时间

  // 进程pid是循环使用的,如果曾经有一个进程异常退出,没有清理自己的心跳信息,
  // 它的心跳信息会残留在共享内存中。如果不巧,新启动的进程pid恰好与异常退出的进程pid相同,
  // 这样会在共享内存中找到两条相同pid的心跳信息,
  // 当守护进程检查到残留进程的心跳信息后,会kill掉残留进程的pid,这个pid恰好是新启动的进程的pid,
  // 会导致新启动进程会被kill

  // 为了解决这个问题,在共享内存中如果查到相同pid的心跳信息(pid相同),就复用这块地址
  for (int i = 0; i < MAXNUMP_; i++)
  {
    if ((m_shm + i)->pid == pinfo->pid)
    {
      m_pos = i;
      break;
    }
  }

  // 如果没有找到正好与当前进程相同pid的残留心跳信息,就在共享内存中查找空位置,将当前进程心跳信息写入
  if (m_pos == -1)
  {
    for (int i = 0; i < MAXNUMP_; i++)
    {
      if ((m_shm + i)->pid == 0)
      {
        m_pos = i;
        break;
      }
    }
  }

  // 这里加锁比较合适
  m_sem.P();

  if (m_pos == -1)
  {
    m_sem.V(); // 没有空间了也要释放信号量
    printf("out of share mem, too many processes\n");
    return -1;
  }

  memcpy(m_shm + m_pos, pinfo, sizeof(st_pinfo));

  // 这里解锁比较合适
  m_sem.V();
  // 打印当前进程的心跳信息
  printf("Create heartbeat success, pid=%d, pname=%s, timeout=%d, atime=%ld\n", pinfo->pid, pinfo->pname, pinfo->timeout, pinfo->atime);
  // 释放pinfo内存
  delete pinfo;
  return false;
}

bool PAction::UptATime()
{
  // 更新共享内存中当前进程的心跳信息
  // 这里不需要加锁,不会有其他进程抢占这个位置
  (m_shm + m_pos)->atime = time(NULL);
  // 打印当前进程的心跳信息
  printf("Update heartbeat success, pid=%d, pname=%s, timeout=%d, atime=%ld\n", (m_shm + m_pos)->pid, (m_shm + m_pos)->pname, (m_shm + m_pos)->timeout, (m_shm + m_pos)->atime);
  return false;
}

PAction::~PAction()
{
  // 把当前进程的心跳信息从共享内存中删除
  // 这里不需要加锁,释放之后任意进程都可以抢占这个位置
  (m_shm + m_pos)->pid = 0;
  // memset((m_shm + m_pos), 0, sizeof(st_pinfo));
  // 释放共享内存的指针
  shmdt(m_shm);
  // 打印删除心跳信息的日志
  printf("Delete heartbeat success, pid=%d, pname=%s, timeout=%d, atime=%ld\n", (m_shm + m_pos)->pid, (m_shm + m_pos)->pname, (m_shm + m_pos)->timeout, (m_shm + m_pos)->atime);
}

守护进程

  • 服务程序由调度程序启动(procctl)
  • 如果服务程序死机(挂起),守护进程将终止它
  • 服务程序被终止后,调度程序(procctl)将重新启动它
  • 应该使用root用户启动,防止被其它程序误杀

exit函数与析构函数

  • exit不会调用局部对象的析构函数
  • exit会调用全局对象的析构函数
  • return会调用全局+局部对象的析构函数
  • 例如下面的程序,把CPActive active作为局部对象放在了main()函数中:
#include <_public.h>

void EXIT(int sig)
{
  printf("sig = %d \n", sig);
  exit(0);
}

int main(int argc, char *argv[])
{

  signal(SIGINT, EXIT);  // 信号2,ctrl+c
  signal(SIGTERM, EXIT); // 信号15,killall -15(正常结束程序)

  // 定时发送心跳
  CPActive active;
  active.AddPInfo(atoi(argv[2]), argv[1]);

  while (true)
  {
    active.UptATime();
    sleep(10);
  }

  return 0;
}
  • 当键盘输入ctrl+c信号退出程序时,并不会调用active对象的析构函数,导致该进程的心跳信息并没有从共享内存中删除
  • 下面把CPActive active作为全局对象放在了main()函数外:
#include <_public.h>

// 需要定义成全局对象。如果是局部对象,ctrl+c后,exit(0)不会调用析构函数
// return 会调用全局和局部对象的析构函数
CPActive active;

void EXIT(int sig)
{
  printf("sig = %d \n", sig);
  exit(0);
}

int main(int argc, char *argv[])
{

  signal(SIGINT, EXIT);  // 信号2,ctrl+c
  signal(SIGTERM, EXIT); // 信号15,killall -15(正常结束程序)

  // 定时发送心跳
  active.AddPInfo(atoi(argv[2]), argv[1]);

  while (true)
  {
    active.UptATime();
    sleep(10);
  }

  return 0;
}
  • 当把CPActive active设置为全局对象后,退出程序后,会自动调用CPActive 的析构函数,移除共享内存中的进程心跳信息

守护进程代码编写

  • 守护进程业务逻辑

    • 1)遍历共享内存中全部的进程心跳记录
    • 2)找到有效的进程心跳记录(pid!=0),向进程发送信号0,判断进程是否存在
    • 3)若不存在则从共享内存中删除该进程心跳记录,continue
    • 4)如果进程未超时,直接continue
    • 5)如果已超时,向进程发送SIGTERM(15)信号,尝试正常关闭进程
    • 6)等待一段时间后如果进程仍没有退出,再次发送SIGKILL(9)信号,强制终止进程
    • 7)从共享内存中删除该进程心跳记录
    • 8)循环1)
// checkproc.cpp
// Date: 2024/02/06
#include <_public.h>

// 程序运行日志
CLogFile logFile;

int main(int argc, char *argv[])
{
  // 程序使用帮助
  if (argc != 2)
  {
    printf("\n");
    printf("Using:./checkproc logfilename\n");

    printf("Example:/home/bill/project/tools1/bin/procctl 10 /home/bill/project/tools1/bin/checkproc /home/bill/project/runtime/log/checkproc.log\n\n");

    printf("本程序用于检查后台服务程序是否超时,如果已超时,就终止它。\n");
    printf("注意:\n");
    printf("  1)本程序由procctl启动,运行周期建议为10秒。\n");
    printf("  2)为了避免被普通用户误杀,本程序应该用root用户启动。\n");
    printf("  3)如果要停止本程序,只能用killall -9 终止。\n\n\n");

    return 0;
  }

  // 守护进程没有任何关心的信号,所以屏蔽所有信号
  // for (int i = 1; i <= 64; i++)
  // {
  //   signal(i, SIG_IGN);
  // }
  // 关闭全部信号输入输出
  CloseIOAndSignal(true);

  // 打开日志文件
  if (logFile.Open(argv[1], "a+") == false)
  {
    printf("logFile.Open(%s) failed. \n", argv[1]);
    return -1;
  }

  // 获取信号量
  CSEM m_sem;
  int semid;
  if ((semid = m_sem.init(SEMKEYP)) == false)
  {
    logFile.Write("m_sem.init(%d) failed. \n", SEMKEYP);
    return -1;
  }

  // 创建/读取共享内存
  int m_shmid;
  if ((m_shmid = shmget(SHMKEYP, MAXNUMP * sizeof(struct st_procinfo), 0640 | IPC_CREAT)) == -1)
  {
    logFile.Write("shmget(%d) failed. \n", SHMKEYP);
    return -1;
  }

  // 将共享内存连接到当前进程的地址空间
  struct st_procinfo *procinfo = (struct st_procinfo *)shmat(m_shmid, 0, 0);

  // 遍历共享内存中全部的进程心跳记录
  for (int i = 0; i < MAXNUMP; i++)
  {
    // 找到有效的进程心跳记录(pid!=0),向进程发送信号0,判断进程是否存在
    // 若不存在则从共享内存中删除该进程心跳记录,continue
    if ((procinfo + i)->pid == 0)
      continue;

    // 仅仅为了调试方便
    // logFile.Write("checkproc: pos=%d, pid=%d, timeout=%d, atime=%d\n", i, (procinfo + i)->pid, (procinfo + i)->timeout, (procinfo + i)->atime);

    int iret = kill((procinfo + i)->pid, 0);
    if (iret == -1)
    {
      logFile.Write("process pid=%d(%s) had not existed. \n", (procinfo + i)->pid, (procinfo + i)->pname);
      memset(procinfo + i, 0, sizeof(struct st_procinfo));
      continue;
    }

    time_t now = time(NULL);
    // 如果进程未超时,直接continue
    if (now - (procinfo + i)->atime < (procinfo + i)->timeout)
      continue;

    // 如果已超时,向进程发送SIGTERM(15)信号,尝试正常关闭进程
    logFile.Write("process pid=%d(%s) had not existed. \n", (procinfo + i)->pid, (procinfo + i)->pname);
    kill((procinfo + i)->pid, SIGTERM); // 发送信号15,尝试正常关闭进程

    // 等待一段时间后如果进程仍没有退出,再次发送SIGKILL(9)信号,强制终止进程
    for (int i = 0; i < 5; i++)
    {
      sleep(1);
      iret = kill((procinfo + i)->pid, 0); // 向进程发送信号0,判断进程是否存在
      if (iret == -1)                      // 进程已退出
        break;
    }
    if (iret == -1) // 进程未退出
    {
      logFile.Write("process pid=%d(%s) had exited. \n", (procinfo + i)->pid, (procinfo + i)->pname);
    }
    else
    {
      kill((procinfo + i)->pid, SIGKILL); // 发送信号9,强制终止进程
      logFile.Write("process pid=%d(%s) had force killed. \n", (procinfo + i)->pid, (procinfo + i)->pname);
    }

    // 从共享内存中删除该进程心跳记录
    memset(procinfo + i, 0, sizeof(struct st_procinfo));
  }

  // 把共享内存从当前进程的地址空间分离
  shmdt(procinfo);

  return 0;
}

FTP文件传输协议

FTP(File Transfer Protocol)是一种用于在网络上进行文件传输的协议。它允许用户通过网络从一个计算机向另一个计算机传输文件。FTP通常用于将文件上传到服务器或从服务器下载文件。

FTP协议的主要应用场景是内网中不同业务系统之间进行数据交换,效率不高也不安全,但是使用起来简单方便。

常用的FTP命令包括:

  1. connect: 连接到FTP服务器。通常使用 ftp 命令加上目标主机名或IP地址来连接到FTP服务器。

    ftp hostname_or_ip
  2. login: 登录到FTP服务器。在连接到FTP服务器后,您需要使用用户名和密码进行登录。

    user username
    password password
  3. cd: 切换FTP服务器上的当前工作目录。

    cd directory_name
  4. ls: 列出FTP服务器上当前工作目录中的文件和目录。

    ls
  5. get: 从FTP服务器下载文件到本地计算机。

    get filename
  6. put: 将文件从本地计算机上传到FTP服务器。

    put filename
  7. mkdir: 在FTP服务器上创建新的目录。

    mkdir directory_name
  8. delete: 删除FTP服务器上的文件。

    delete filename
  9. quit: 断开与FTP服务器的连接并退出FTP会话。

    quit

增量下载流程

  • 通过4个vector容器来实现增量下载,每次启动同步程序可以根据上次的状态,确定本次需要同步的文件列表,不会重复下载本地已有的文件。容器中存放到是文件信息,包含文件名修改时间两个属性。
文件信息结构体
文件名
文件修改时间
struct st_fileinfo
{
  char filename[512]; // 文件名
  char mtime[32];     // 文件的修改时间
};
  • 创建4个容器,存储不同状态的文件信息列表
  • listfilename文件
SURF_ZH_20240210224640_11597.csv
SURF_ZH_20240210224640_11597.json
SURF_ZH_20240210224640_11597.xml
SURF_ZH_20240210224740_12088.csv
SURF_ZH_20240210224740_12088.json
SURF_ZH_20240210224740_12088.xml
SURF_ZH_20240210224840_12587.csv
SURF_ZH_20240210224840_12587.json
SURF_ZH_20240210224840_12587.xml

用于存储远程目录的文件列表

  • okfilename文件
<filename>SURF_ZH_20240210224640_11597.xml</filename><mtime>20240211064640</mtime>
<filename>SURF_ZH_20240210224740_12088.xml</filename><mtime>20240211064740</mtime>
<filename>SURF_ZH_20240210224840_12587.xml</filename><mtime>20240211064840</mtime>

每一行对应一条文件信息,文件信息包含文件名、修改时间,XML格式。该文件用于存储相对于远程目录下本次不需要下载的文件列表,换句话说,也就是基于上一次从远程目录下载的文件列表,去除远程目录不存在的文件名。这样可以尽量减少okfilename文件的大小。

  • 这里只需要对上面两个文件有印象即可,下面会详细介绍这两个文件和4个vector的关系。

程序第一次运行

vfilelist1vfilelist2vfilelist3vfilelist4
vector <struct st_fileinfo>vector <struct st_fileinfo>vector <struct st_fileinfo>vector <struct st_fileinfo>
已下载1nlist2已下载3待下载4

  • 模拟客户端、服务端文件情况。此时客户端目录为空,远程目录有1.txt2.txt3.txt4.txt5.txt 5个文件

  • 首先从本地(okfilename文件)加载上次已下载的文件列表(由于是第一次运行,vfilelist1为空)
  • 再向FTP发送指令,列出远程目录文件列表,存储到本地文件(listfilename),而后再读取listfilename文件,过滤掉条件不匹配的文件名,加载到vfilelist2中。(此处有一个疑惑为什么要读取远程目录文件列表,然后存储到本地文件,再过滤掉不匹配的文件名加载到vfilelist2中,而不是读取远程目录文件列表,直接加载到vfilelist2中)

  • 通过比对vfilelist1vfilelist2,得出本次无需下载的文件列表(vfilelist3已下载3)和本次待下载(vfilelist4待下载4)
  • vfilelist3(本次无需下载文件列表)中的内容,覆盖写入到okfilename文件中,okfilename文件只需存储上一个状态、相对于远程目录下载成功的文件列表即可,防止okfilename文件数据积累。

  • 遍历vfilelist4调用FTP对象逐个下载文件,每下载成功一个文件,向okfilename文件写入一行

程序第二次运行

  • 从本地的okfilename文件加载vfilelist1容器

  • 此时客户端目录已经放着第一次运行程序下载好的文件,服务端删除了1.txt2.txt,新增了6.txt7.txt

  • 通过FTP远程命令列出远程目录中的文件列表,先存放到listfilename文件中,再读取listfilename文件,过滤掉不匹配的文件名,加载到vfilelist2中。
  • 通过比对vfilelist1(上次已下载)、vfilelist2(远程目录文件列表),得到vfilelist3(本次无需下载)、vfilelist4(本次待下载)。
  • okfilename文件中只需存储相对于远程目录已经下载成功的文件即可,不需要全部的本地已下载全部的文件列表,因此需要计算出vfilelist3容器,再把vfilelist3中的内容覆写到okfilename文件中。
  • 依照vfilelist4从远程目录逐个下载文件,每下载成功一个文件,便向okfilename文件中追加一个文件信息。

  • 第二次运行程序,下载开始前

    • okfilename文件内容如下

      <filename>1.txt</filename><mtime>20240211064640</mtime>
      <filename>2.txt</filename><mtime>20240211064740</mtime>
      <filename>3.txt</filename><mtime>20240211064840</mtime>
      <filename>4.txt</filename><mtime>20240211065840</mtime>
      <filename>5.txt</filename><mtime>20240211066840</mtime>
    • listfilename文件内容如下

      3.txt
      4.txt
      5.txt
      6.txt
      7.txt
  • vfilelist2中的内容是根据远程目录文件列表,过滤掉不匹配的文件名得来的,在这里过滤尽量减少后续判断的性能消耗。
  • 第二次程序运行,下载完成后

    • okfilename文件内容如下

      <filename>3.txt</filename><mtime>20240211064840</mtime>
      <filename>4.txt</filename><mtime>20240211065840</mtime>
      <filename>5.txt</filename><mtime>20240211066840</mtime>
      <filename>6.txt</filename><mtime>20240211067740</mtime>
      <filename>7.txt</filename><mtime>20240211068940</mtime>

程序第三次运行

  • 从本地加载okfilename文件到vfilelist1
  • vfilelist2中存放到的远程目录的所有文件(过滤掉不匹配的文件名)
  • vfilelist3vfilelist4是通过vfilelist1vfilelist2比对而来

  • 远程目录只有6.txt7.txt8.txt3个文件

  • 第三次运行下载前

    • okfilename文件内容

      <filename>3.txt</filename><mtime>20240211064840</mtime>
      <filename>4.txt</filename><mtime>20240211065840</mtime>
      <filename>5.txt</filename><mtime>20240211066840</mtime>
      <filename>6.txt</filename><mtime>20240211067740</mtime>
      <filename>7.txt</filename><mtime>20240211068940</mtime>
    • listfilename文件内容

      6.txt
      7.txt
      8.txt
  • 第三次运行下载后

    • okfilename文件内容

      <filename>6.txt</filename><mtime>20240211067740</mtime>
      <filename>7.txt</filename><mtime>20240211068940</mtime>
      <filename>8.txt</filename><mtime>20240211069940</mtime>

加入修改时间比对

  • 如果文件名相同,但是修改时间发生变化,也可以纳入判断范围,在运行的时候可以指定是否checktime。要修改的地方就是在比对vfilelist1vfilelist2时,除了比对文件名外,还应比对modifyTime,得出vfilelist4

image-20240212185454690

增量上传流程

与增量下载逻辑相同,过程逆转过来即可

程序死机(挂死)

  • 涉及网络通信、数据库操作的程序容易挂死
  • 比较复杂的程序可能会挂死
  • 简单的程序基本上不会挂死

因此需要在可能会超时的代码附近,加上心跳更新代码,例如在列出远程目录、每个文件下载成功等地方更新心跳。

TCP文件传输系统


c/c++ Linux Linux信号量 Linux共享内存 Linux进程心跳 Linux守护进程
Theme Jasmine by Kent Liao And Bill

本站由提供云存储服务