Linux - 网络套接字

一、网络编程

1)地址结构

1. IP地址结构
  • struct in_addr:是用于表示 IPv4 地址 的结构体,定义在头文件 <netinet/in.h> 中。它的主要作用是存储一个 32 位的 IPv4 地址,通常与 struct sockaddr_in 一起使用。

    cpp 复制代码
    struct in_addr {
        uint32_t s_addr; // IP 地址,使用网络字节序存储
    };
    • s_addr
      • 类型:uint32_t(32 位无符号整数)。
      • 用于存储 IPv4 地址,以 网络字节序(大端序)表示。
      • 可以通过函数(如 inet_pton()inet_addr())将人类可读的字符串形式的 IP 地址(如 "192.168.1.1")转换为 s_addr
2. 套接字地址
  • struct sockaddr_in:是一个用于描述 IPv4 套接字地址 的结构体,常用于网络编程中与套接字函数(如 bind()connect() 等)交互。它是 struct sockaddr 的具体实现之一,专用于 IPv4 协议族

    cpp 复制代码
    struct sockaddr_in {
        sa_family_t    sin_family;   // 地址族(Address Family),AF_INET 表示 IPv4
        in_port_t      sin_port;     // 16位的端口号,网络字节序
        struct in_addr sin_addr;     // IP 地址(网络字节序)
        unsigned char  sin_zero[8];  // 填充字段,保持与 struct sockaddr 的大小一致
    };
    • sin_family

      • 描述地址族(protocol family)。

      • 对于 IPv4 地址,sin_family 的值为 AF_INET

    • sin_port

      • 用于存储端口号(16 位)。

      • 必须是 网络字节序 ,可以通过 htons() 函数将主机字节序转换为网络字节序。

    • sin_addr

      • 表示 IPv4 地址。

      • 类型为 struct in_addr,实际上是一个 32 位整数。

      • 常用的值:

        • INADDR_ANY:绑定到本地所有可用接口。
        • INADDR_LOOPBACK:表示回环地址(127.0.0.1)。
    • sin_zero

      • 保留字段,不做实际使用。

      • 用于确保 struct sockaddr_in 的大小与 struct sockaddr 相同。

      • 通常用 memset() 将其置为 0。

2)地址转换

1.字符串转In_addr
  • inet_addr():是一个用于将 点分十进制的 IPv4 地址字符串 转换为 32 位网络字节序的整数in_addr_t 类型)的函数。

    in_addr_t inet_addr(const char *cp)

    cpp 复制代码
    in_addr_t addr = inet_addr("192.168.1.1");
    • cp :指向一个点分十进制表示的 IPv4 地址字符串(如 "192.168.1.1")。

    • 返回值:

      • 成功 :返回转换后的 32 位网络字节序的 IPv4 地址(in_addr_t 类型)。

      • 失败 :如果 cp 是无效的 IPv4 地址字符串,返回 INADDR_NONE (通常是 0xFFFFFFFF)。

  • inet_aton():将点分十进制格式的 IPv4 地址转换为网络字节顺序的二进制形式。

    int inet_aton(const char *cp, struct in_addr *inp)

    cpp 复制代码
    struct in_addr addr;
    inet_aton("192.168.1.1", &addr)
    • cp:指向以字符串形式表示的 IPv4 地址(如 "192.168.1.1")。

    • inp:指向 struct in_addr 的指针,struct in_addr 是一个包含 IPv4 地址的结构体。

    • 返回值:如果转换成功,返回 1;如果失败,返回 0。

  • inet_pton():处理 IPv4 和 IPv6 地址,将它们从点分十进制或标准文本形式转换为网络字节顺序的二进制表示。

    int inet_pton(int af, const char *src, void *dst)

    cpp 复制代码
    struct in_addr addr;
    inet_pton(AF_INET, "192.168.1.1", &addr)
    • af:地址族,通常是 AF_INET(IPv4)或 AF_INET6(IPv6)。
    • src:指向源 IP 地址的字符串(如 "192.168.1.1""::1")。
    • dst:指向目标内存区域的指针,用来存储转换后的二进制地址。对于 IPv4 是 struct in_addr,对于 IPv6 是 struct in6_addr
    • 返回值:成功时返回 1,失败时返回 0,传入无效地址族时返回 -1。
函数 支持的协议 输入格式 返回类型 错误返回值
inet_aton IPv4 点分十进制 IP 地址 int 0(失败)
inet_addr IPv4 点分十进制 IP 地址 in_addr_t INADDR_NONE(失败)
inet_pton IPv4、IPv6 点分十进制或标准文本 二进制地址 0(失败),-1(错误)
2. In_addr转字符串
  • inet_ntoa():将网络字节顺序的 IPv4 地址(即 in_addr 类型)转换为点分十进制的字符串格式(例如:"192.168.1.1")。

    char *inet_ntoa(struct in_addr in)

    cpp 复制代码
    struct in_addr addr;
    addr.s_addr = htonl(0xC0A80101); // 192.168.1.1
    char *ip = inet_ntoa(addr);
    printf("IP Address: %s\n", ip); // 输出 "192.168.1.1"
    • in:类型为 struct in_addr,它包含了一个网络字节顺序的 IPv4 地址。

    • **返回值:**返回一个指向字符数组的指针,字符数组中是点分十进制格式的字符串表示。需要注意的是,这个返回值是一个静态缓冲区的指针,因此在多线程环境中使用时需要小心。

  • inet_ntop():一个更通用的函数,支持同时处理 IPv4 和 IPv6 地址,并将它们转换为点分十进制或标准文本表示的字符串格式。

    const char *inet_ntop(int af, const void *src, char *dst, socklen_t size)

    cpp 复制代码
    struct in_addr addr;
    addr.s_addr = htonl(0xC0A80101); // 192.168.1.1
    char str[INET_ADDRSTRLEN];
    if (inet_ntop(AF_INET, &addr, str, sizeof(str))) {
        printf("IP Address: %s\n", str); // 输出 "192.168.1.1"
    }
    • af:地址族,指定地址的类型,通常为 AF_INET(IPv4)或 AF_INET6(IPv6)。
    • src:指向源 IP 地址的指针,它是以网络字节顺序存储的二进制地址。对于 IPv4 是 struct in_addr,对于 IPv6 是 struct in6_addr
    • dst:指向缓冲区的指针,存储转换后的字符串。
    • sizedst 缓冲区的大小,确保它足够容纳转换后的字符串(IPv4 地址需要 16 字节,IPv6 地址需要 46 字节)。
    • 返回值:成功时,返回指向目标缓冲区的指针;如果失败,返回 NULL
2. 字节序转换
  • htons():是一个用于将主机字节序(Host Byte Order)的 16 位整数 转换为网络字节序(Network Byte Order)的函数。

    uint16_t htons(uint16_t hostshort)

    • hostshort:要转换的 16 位主机字节序整数(一般是端口号)。

    • 返回值:返回 转换为网络字节序的 16 位整数

3)套接字接口

1. 创建套接字
  • int socket():是在网络编程中创建套接字(socket)的一种方式。具体来说,socket 是一个系统调用函数,用于创建一个新的套接字,用于在应用程序和操作系统的网络协议栈之间进行数据传输。

    int socket(int domain, int type, int protocol)

    cpp 复制代码
    // 创建一个流式套接字(使用 IPv4 协议)
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    • domain(协议族):

      • AF_INET:IPv4 地址族。

      • AF_INET6:IPv6 地址族。

      • AF_UNIX:本地通信的 Unix 域套接字(用于进程间通信)。

    • type(套接字类型):

      • SOCK_STREAM:流式套接字,通常用于 TCP 协议,提供面向连接的可靠通信。

      • SOCK_DGRAM:数据报套接字,通常用于 UDP 协议,提供无连接的非可靠通信。

    • protocol(协议):

      • 一般情况下为 0,让操作系统自动选择与给定协议族和类型匹配的默认协议。对于 SOCK_STREAM,默认选择的是 TCP;对于 SOCK_DGRAM,默认选择的是 UDP

      • 如果指定具体的协议,则需要使用对应协议的编号,例如,IPPROTO_TCPIPPROTO_UDP

    • 返回值:

      • 成功时,socket() 返回一个整数值,表示新创建的套接字的文件描述符。

      • 失败时,返回 -1,并且可以通过 errno 获取错误码。

2. 绑定套接字
  • bind():是一个系统调用函数,用于将套接字(socket)与本地地址(如 IP 地址和端口号)进行绑定。在服务器端,通常会使用 bind() 来指定监听端口和 IP 地址,以便接收来自客户端的连接请求。

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

    cpp 复制代码
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    string ip = "192.168.0.1";
    struct sockaddr_in local;
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(port_);
    local.sin_addr.s_addr = inet_addr(ip.c_str())
    int n = bind(sockfd, (const struct sockaddr*)&local, sizeof(local));
    • sockfd :传入的套接字文件描述符,通常由 socket() 创建。

    • addr :一个指向 struct sockaddr 结构体的指针,表示要绑定的本地地址。具体的结构体类型(如 struct sockaddr_instruct sockaddr_in6)取决于协议族(如 IPv4 或 IPv6)。

    • addrlenaddr 结构体的长度。通常使用 sizeof(struct sockaddr_in) 来获取。

    • 返回值:

      • 成功时,返回 0

      • 失败时,返回 -1,并设置 errno,可以通过 perror()strerror() 来查看错误信息。

3. 监听
  • listen():用于在服务器端创建一个侦听套接字(listening socket),它让套接字处于等待客户端连接的状态。通常在网络编程中,服务器会使用 listen 来监听特定端口上的连接请求。

    int listen(int sockfd, int backlog)

    cpp 复制代码
     // 进入监听状态,最多 3 个连接请求排队
    int server_fd;
    listen(server_fd, 3)
    • sockfd:要进行监听的套接字描述符,它通常由 socket() 创建,类型是 SOCK_STREAM(TCP协议)或 SOCK_DGRAM(UDP协议)。在 TCP 的场景中,sockfd 是一个连接端点,用来监听客户端的连接请求。

    • backlog:最大连接请求队列的长度,指定当服务器应用程序无法及时接受客户端连接时,操作系统内核允许的最大排队连接数。如果这个队列满了,后续的连接请求会被拒绝,或者客户端会收到一个连接失败的错误。通常该值设置为 5 到 128 之间。

      • 当该连接队列已满,服务端会将连接放到一个临时的半连接队列中,并将连接设置为SYN_RCVD状态,这个半连接队列不会长时间存在,当一定时间没有响应,会直接丢弃,如果被丢弃后客户端发送了数据,那么客户端和服务端会先重新进行三次握手再发送数据。
  • 返回值:成功时返回 0。出错时返回 -1,并设置 errno 以指示错误。

4. 接受
  • accept():是服务器端套接字函数,用于接受一个客户端的连接请求。当客户端请求连接时,服务器通过调用 accept() 来接受连接,并返回一个新的套接字,用于与客户端进行数据交换。accept() 是一个阻塞调用,意味着服务器会在调用此函数时等待,直到有客户端连接到来。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

    cpp 复制代码
    // 等待客户端连接请求
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;  // 绑定所有接口
    address.sin_port = htons(8080);        // 监听端口 8080
    new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
    • sockfd :这是由 socket() 创建并通过 bind()listen() 函数设置为监听状态的套接字描述符。

    • addr :一个指向 struct sockaddr 结构体的指针,用于存储客户端的地址信息。通常会传入 struct sockaddr_in,它包含客户端的 IP 地址和端口号。

    • addrlen :这是一个指向 socklen_t 类型的指针,用于表示传入的 addr 缓存的大小。在调用 accept() 时,addrlen 会被更新为客户端地址结构的实际大小。

    • 返回值:

      • 成功时,返回一个新的套接字描述符(new_socket),用于与客户端进行通信。

      • 失败时,返回 -1,并设置 errno 来表示错误原因。

5. 连接
  • connect():是客户端用于建立与服务器的连接的函数。在客户端调用 connect() 后,它会尝试与指定的服务器(通过 IP 地址和端口号)建立连接。此函数通常是客户端程序的一部分,在发起通信之前,客户端需要先与服务器建立一个连接。

    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

    cpp 复制代码
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8080);
    connect(sock, (struct sockaddr *)&server_addr, sizeof(server_addr))
    • sockfd :客户端通过 socket() 创建的套接字描述符。

    • addr :一个指向 struct sockaddr 结构体的指针,包含目标服务器的 IP 地址和端口号。通常会使用 struct sockaddr_in 来指定目标服务器的地址信息。

    • addrlen :地址结构的长度(通常是 sizeof(struct sockaddr_in))。

    • 返回值:成功时返回 0。出错时返回 -1,并设置 errno 以指示错误。

函数 TCP UDP
socket() 使用 使用
bind() 服务器使用 客户端和服务器都可以使用
listen() 服务器使用 不使用
accept() 服务器使用 不使用
connect() 客户端使用 客户端和服务器都可以使用
6. 设置套接字
  • setsockopt():用于设置套接字选项的函数,可以用于配置套接字的行为和特性。通过这个函数,你可以设置多种套接字选项,比如设置缓冲区大小、超时、协议相关的设置等。

    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen)

    cpp 复制代码
    // 创建 TCP 套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    int optval = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval))
    • sockfd :套接字描述符,是通过 socket() 函数创建的。

    • level :协议层级,通常是 SOL_SOCKET,表示对套接字层进行操作,也可以是其他协议特定的层级(例如 IPPROTO_TCPIPPROTO_UDP)。

    • optname:要设置的选项名称。

      • SO_REUSEADDR:允许重用本地地址和端口。在关闭一个已绑定的套接字后,能允许快速重新绑定到相同的地址和端口。
      • SO_RCVBUF:设置接收缓冲区的大小。
      • SO_RCVBUF:设置发送缓冲区的大小。
      • SO_KEEPALIVE:启用或禁用连接保持活动。
      • SO_BROADCAST:允许发送广播。
      • SO_LINGER:设置 linger 选项,用来指定连接在关闭时的延迟行为。
    • optval:指向存储选项值的缓冲区。具体内容和类型取决于选项。

    • optlenoptval 缓冲区的大小。

    • 返回值:

      • 成功返回 0

      • 失败返回 -1,并且设置 errno 来指示错误原因。

4)传输数据

1. 接受数据
  • recvfrom() 是一个用于接收数据的函数,通常在基于 UDP 的无连接套接字通信中使用。它可以接收来自特定发送者或任意发送者的数据,并且可以选择性地获取发送方的地址信息。

    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen );

    • sockfd

      • 接收数据的套接字文件描述符。
      • 必须是一个已经绑定到本地地址(使用 bind)的套接字。
    • buf

      • 接收数据的缓冲区指针。
      • 函数会将接收到的数据存入该缓冲区。
    • len

      • buf 缓冲区的大小(以字节为单位)。
      • 接收数据时,如果数据大于缓冲区,数据会被截断。
    • flags

      • 控制行为的标志位,可以为以下值的组合:
        • 0:默认行为。
        • MSG_PEEK:仅查看数据而不从队列中移除。
        • MSG_WAITALL:等待接收到足够数据(填满缓冲区)。
        • MSG_DONTWAIT:非阻塞模式(无数据时立即返回)。
    • src_addr

      • 指向 struct sockaddr 类型的结构体,用于存储发送方的地址信息(可为 NULL)。
      • 如果不关心发送方地址,可设置为 NULL
    • addrlen

      • 指向 socklen_t 的指针,用于存储 src_addr 的大小。
      • 在调用前,需初始化为 src_addr 缓冲区的大小;返回后,表示实际存储的地址大小。
      • 如果 src_addrNULL,此参数也应为 NULL
    • 返回值

      • 成功:返回接收到的字节数。
      • 失败:返回 -1,并设置 errno 以指示错误原因。
2. 发送数据
  • sendto():用于 UDP 套接字的发送数据的函数,它允许向指定的目标地址发送数据。对于 UDP,它是一个无连接的协议,因此你需要明确地指定目标地址和端口。

    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);

    cpp 复制代码
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    const char* message = "Hello, UDP!";
    // 2. 设置目标地址结构
    struct sockaddr_in dest_addr;
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(12345);  // 目标端口
    dest_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 目标IP地址
    ssize_t sent_len = sendto(sockfd, message, strlen(message), 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
    • sockfd:发送数据的套接字描述符,通常是通过 socket() 创建的,适用于无连接的 UDP 套接字。

    • buf:指向存储要发送数据的缓冲区的指针。

    • len:缓冲区的长度,指定要发送的数据的字节数。

    • flags:控制发送操作的标志,可以是以下值之一(可以组合使用):

      • MSG_CONFIRM:请求对方确认收到数据(适用于可靠传输协议)。
      • MSG_DONTROUTE:不走路由表,直接发送。
      • MSG_NOSIGNAL:如果发送数据时产生了信号,则不触发信号。
    • dest_addr:指向 sockaddr 结构体的指针,包含目标主机的地址信息,通常是 sockaddr_in(对于 IPv4)或 sockaddr_in6(对于 IPv6)。

    • addrlen:目标地址的长度,通常是 sizeof(struct sockaddr_in)sizeof(struct sockaddr_in6)

    • 返回值:

      • 成功时,返回发送的字节数。

      • 失败时,返回 -1,并设置 errno 以指示错误。

二、守护进程化

1)进程相关标识符

1. PID(Process ID,进程ID)
  • 每个正在运行的进程都会分配一个唯一的进程标识符(PID),用于区分不同的进程。
  • PID 是操作系统内部用来管理和调度进程的关键标识符。当我们执行命令或启动程序时,操作系统会分配一个 PID 来标识该进程。
2. PPID(Parent Process ID,父进程ID)
  • 每个进程都有一个父进程,也就是创建它的进程。PPID 就是父进程的 PID。
  • PPID 表示当前进程的父进程,用于构建进程树。通过 PPID 可以追踪到某个进程是由哪个父进程创建的。
3. PGID(Process Group ID,进程组ID)
  • 进程组是一个或多个进程的集合,它们共享一个进程组 ID。PGID 是进程组的标识符。
  • 进程组在 Unix 系统中用于信号的发送。例如,可以向整个进程组发送一个信号,而不仅仅是单个进程。
4. SID(Session ID,会话ID)
  • 会话是一组进程的集合,它通常由一个登录会话或某个程序的启动进程所创建。SID 是会话的标识符。
  • 会话用于管理进程的生命周期,特别是在控制终端的上下文中。例如,登录系统后,所有的进程都属于同一个会话。
5. TGID(Thread Group ID,线程组ID)
  • 线程组是指多个线程共享相同的进程 ID 和资源。TGID 是线程组的标识符。
  • 在 Linux 系统中,线程和进程都被视为任务。线程组 ID 用于标识一组共享资源的线程。
6. UID(User ID,用户ID)
  • UID 是操作系统用于标识用户的唯一标识符。
  • 操作系统使用 UID 来管理文件的访问权限、资源分配等。每个用户都有一个唯一的 UID,通常在多用户系统中,UID 用于控制每个用户的操作权限。

进程组与任务的关系

  • 任务包含进程 :在 Linux 中,任务通常是指单个的进程或线程。每个进程都有一个唯一的 PID,并且是操作系统调度的基本单位。
  • 进程组包含任务(进程) :多个进程可以组成一个进程组。进程组由一个或多个任务(进程)组成,这些任务共享相同的 PGID。一个进程属于一个进程组,而进程组内的所有进程会共享一些属性和资源(如信号的发送)。
  • 进程组 ID :每个进程组都有一个唯一的 PGID ,它通常是该进程组内某个进程的 PID。这个 PID 一般是该组中的第一个进程的 PID。虽然进程组中的每个进程都有自己的 PID,但它们共享一个进程组 ID。

2)Session

在 Linux 中,前台进程后台进程的区分主要是通过进程的输入/输出交互方式来确定的,具体来说是基于终端控制和进程的执行方式。下面我会详细讲解如何区分前台进程和后台进程,以及相关的操作方法。

1. 前台进程(Foreground Process)

前台进程是指直接与用户交互的进程,通常在用户的终端窗口中执行,并且会占用该终端的输入输出。

  • 特征
    • 前台进程在启动后,会占用当前的终端,并与用户进行交互。
    • 用户可以直接向前台进程发送输入并获取其输出。
    • 一旦前台进程结束,终端控制权会归还给用户。
  • 如何识别
    • 在命令行中,运行一个进程时,默认情况下,它是前台进程。例如,运行 vim 编辑器,或者执行一个 python 脚本,这些都会是前台进程。
  • 管理前台进程
    • 如果你在执行一个命令时,你的终端会被该进程"占用",你只能看到该进程的输出,而无法执行其他命令。
    • 可以使用 Ctrl+C 来终止当前前台进程。
2. 后台进程(Background Process)

后台进程是指在系统后台运行的进程,它不会占用当前终端的输入输出流。用户可以在终端继续执行其他命令,而后台进程在不干扰用户操作的情况下继续执行。

  • 特征

    • 后台进程不会占用终端的输入输出,用户可以在终端继续执行其他命令。
    • 通常使用一个符号 & 将命令放到后台运行,或者使用一些专门的命令(如 nohupdisown)将进程移到后台。
    • 后台进程的输出通常会显示在终端中,除非使用输出重定向将其发送到文件。
  • 如何识别

    • 在命令行中,使用符号 & 将一个命令放到后台执行,例如:

      复制代码
      long_running_command &
    • 该命令将在后台运行,并且会立即返回到命令行,用户可以继续输入其他命令。

  • 管理后台进程

    • 使用 jobs

      命令查看当前的后台进程:

      复制代码
      jobs

      输出类似:

      复制代码
      [1]+  1234 running    long_running_command &
    • 使用

      fg 将后台进程带到前台:

      复制代码
      fg %1

      这将把进程 id1234,任务号1的任务带到前台运行。

    • 使用 bg将暂停的进程继续在后台运行:

      复制代码
      bg %1
    • 使用 kill命令终止后台进程:

      复制代码
      kill %1
3. 如何从前台切换到后台

如果你已经在前台运行了一个进程,但希望将其切换到后台,可以使用以下方法:

  • 暂停前台进程并将其移至后台

    • 在执行的前台进程中,按下 Ctrl+Z 将进程暂停。

    • 然后使用 bg命令将其发送到后台继续运行:

      复制代码
      bg
  • 将后台进程移动到后台执行

    • 使用 disown命令来彻底把后台进程从当前 shell 会话中"脱离"出来,防止它在 shell 会话关闭时被杀死:

      复制代码
      disown %1
4. 使用 nohup 命令启动后台进程

启动一个即使在关闭终端或退出会话后仍然继续运行的后台进程,可以使用 nohup 命令。

  • nohup :该命令使进程在你退出终端后仍然运行,并且它会将标准输出和标准错误重定向到文件 nohup.out

    复制代码
    nohup long_running_command &

    这样,无论你是否退出终端,long_running_command 都会继续运行。

3)守护进程

守护进程(Daemon) 是在后台运行的进程,它通常不与任何终端交互,并且在系统启动时或系统运行过程中自动启动。守护进程通常用于处理系统级任务,如日志记录、网络服务、定时任务等。常见的守护进程包括 web 服务器(如 Apache)、数据库服务(如 MySQL)等。

守护进程化(Daemonization)指的是将一个普通的进程转变为守护进程的过程。这个过程通常包括一系列的步骤,以确保进程能够在后台独立运行,并在必要时能够安全退出。守护进程话步骤如下:

  • 忽略控制终端的信号

    cpp 复制代码
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);
    • 守护进程通常不希望接收到来自控制终端(例如键盘输入或终端关闭)的信号。可以使用 signal()sigaction() 来忽略这些信号。

    • 特别是,守护进程可能需要忽略 SIGHUP 信号,这个信号通常在终端关闭时发送。

  • 创建一个新的会话

    cpp 复制代码
    if(fork() > 0){
        exit(0);
    }
    setsid();
    • 使用 fork() 创建一个子进程,确保父进程终止。这样,子进程就不再是终端的控制进程(父进程)。

    • 调用 setsid() 将子进程创建为一个新的会话,并成为该会话的首进程。这样可以避免进程被控制终端和控制进程干扰。

    • 通过调用 setsid(),子进程会成为新的会话领导进程,脱离控制终端,彻底实现后台运行。

  • 改变工作目录

    cpp 复制代码
    if(!cwd.empty()){
    	chdir(cwd.c_str());
    }
    • 守护进程应该将当前工作目录更改为根目录(/),以避免阻止文件系统的卸载。执行此操作可以确保守护进程不会锁定文件系统。

    • 使用 chdir("/") 进行目录切换。

  • 重定向文件描述符

    • 守护进程应该关闭标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)文件描述符。这是因为守护进程没有终端,通常不会与用户进行交互。

    • 可以使用 close() 关闭文件描述符

      cpp 复制代码
      fclose(stdin);
      fclose(stdout);
      fclose(stderr);
    • 或者将其重定向到 /dev/null

      cpp 复制代码
      freopen("/dev/null", "r", stdin);
      freopen("/dev/null", "w", stdout);
      freopen("/dev/null", "w", stderr);
  • 创建子进程

    • 使用 fork() 创建第二个子进程,确保守护进程不会偶然地重新获得控制终端。第一步的 fork() 创建的子进程还可能会被某些父进程或终端所关联,所以第二次 fork() 是确保完全与控制终端断开联系的措施。
  • 清理环境

    • 守护进程通常不需要与用户的会话状态或终端会话有关的环境变量。可以在守护进程化时清除这些变量,例如使用 unsetenv() 删除环境变量。

    • 通过调用 umask(0) 来设置文件创建权限掩码,这样守护进程创建的文件权限将不会受到父进程设置的限制。

  • deamon():函数通常位于 unistd.h 头文件中,使用它可以轻松地将一个进程转化为守护进程。

    int daemon(int nochdir, int noclose)

    cpp 复制代码
    // 将当前进程转化为守护进程
    daemon(0, 0)
    • nochdir:如果为 0,守护进程会将工作目录更改为根目录(/),以避免占用系统的任何文件系统。常见的做法是设置为 1 来保持当前工作目录不变,防止守护进程锁定特定目录。

    • noclose:如果为 0,守护进程会关闭标准输入、输出和错误输出,并将它们重定向到 /dev/null,通常会避免任何与终端的交互。设置为 1 会让守护进程保持标准文件描述符不关闭。

    • 返回值

      • 成功时,daemon() 返回 0。
      • 失败时,返回 -1,并设置 errno 以指示错误原因。
相关推荐
人猿泰飞2 分钟前
在Ubuntu-22.04.5中安装ONLYOFFICE DocSpace(协作空间)【注意:安装失败,谨慎参考!】
java·linux·运维·python·ubuntu·项目管理·onlyoffice
CAE虚拟与现实3 分钟前
修改wsl中发行版Ubuntu的主机名
linux·运维·ubuntu·wsl·wsl2·修改主机名
开发小能手-roy6 分钟前
Ubuntu服务器性能调优指南:从基础工具到系统稳定性提升
linux·运维·服务器·ubuntu
潘yi.14 分钟前
Shell编程之正则表达式与文本处理器
linux·运维·正则表达式
涛涛讲AI21 分钟前
wkhtmltopdf 实现批量对网页转为图片的好工具,快速实现大量卡片制作
linux·服务器·windows·windows效率工具
暴躁的小胡!!!34 分钟前
2025年最新Web安全(面试题)
网络·安全·web安全
破刺不会编程35 分钟前
什么是进程?
linux·运维·服务器
牛了爷爷39 分钟前
php伪协议
android·开发语言·php
GOTXX1 小时前
【Qt】QWidget 核⼼属性详解
开发语言·前端·c++·qt·机器学习·ai·widget
.普通人1 小时前
算法基础(以acwing讲述顺序为主,结合自己理解,持续更新中...)
c++·算法