IPV4流媒体项目(网络多播)-前置知识


仓库链接https://github.com/maxswordsman/IPV4_StreamingMedia

一、相关概念

1.流媒体

流媒体(Streaming Media)是一种通过网络传输的媒体形式,它允许用户在数据传输过程中即时播放音频、视频或其他媒体内容。这种技术让用户无需下载整个文件就能观看视频或听音乐



2.流量控制

流量控制是一种网络技术,用于管理和控制网络中数据的传输,以确保数据传输的稳定性和效率。流量控制主要目的是防止网络过载,优化或保证系统的性能和可靠性。这通常通过以下几种方式实现:

  • 速率控制:限制数据传输的速度,使其保持在网络能够处理的范围内。这有助于避免数据丢失和网络拥堵
  • 拥塞控制:在网络拥塞时,调整数据发送的速率,减少数据包的丢失和重新传输。TCP协议中的拥塞控制机制是一个典型例子
  • 优先级控制:根据数据包的重要性或优先级进行排序,优先传输更重要的数据。这常用于实时通信服务,如VoIP(语音通话)或在线游戏
  • 流量整形(Traffic Shaping):调整数据传输的时间和速率,使流量更平滑地通过网络。
(1) 流媒体项目中为什么需要流量控制

流媒体项目需要流量控制的原因主要是为了提高传输效率、保证服务质量和改善用户体验。具体来说,流量控制在流媒体项目中的作用包括以下几个方面:

  • 避免网络拥堵:流媒体内容,特别是高清视频,需要大量的带宽进行传输。如果没有适当的流量控制,数据的大量传输可能会导致网络拥堵,影响到其他用户的网络使用体验。通过流量控制,可以平衡网络负载,避免因数据过载导致的网络延迟和丢包
  • 提高传输稳定性:流量控制可以根据网络状况动态调整数据传输速率。这对于流媒体服务尤其重要,因为网络带宽的波动可能直接影响视频播放的连续性和质量。适应性流量控制有助于减少缓冲和中断,提供更平滑的观看体验
  • 保证服务质量(QoS):流媒体服务常常需要保证一定的服务质量,尤其是在直播或高清视频传输时。流量控制可以通过优先级控制和带宽分配来确保关键流媒体内容的优先传输,从而保持视频和音频的高质量标准
  • 合理分配资源:在用户众多的情况下,合理地分配有限的带宽资源对于流媒体服务商尤为重要。流量控制可以帮助服务商管理多用户的带宽需求,确保每个用户都能获得满意的服务

重点原因

  • Server端通过socket向多播组ip发送数据,考虑到丢包拥塞等问题,需要使用流量控制去解决
  • 在Client通过子进程调用解码器,播放mp3等音频文件,需要考虑到流控等问题
  • 在Client端父子进程通信使用管道,管道的读写是阻塞的,因此也需要考虑到流控等问题

(2) 令牌桶算法
  • 令牌桶(Token Bucket)是一种流量整形(Traffic Shaping)和流量限制(Rate Limiting)的机制,广泛用于网络带宽管理、服务器请求处理等场景。该算法通过控制数据传输的速率和突发性来确保网络服务的质量和公平性。令牌桶算法的核心思想是使用一个虚拟的“桶”,桶中存放着“令牌”(token),数据包的传输需要消耗令牌

    令牌桶算法提供了一种有效的方式来控制和管理数据流,以确保网络资源的合理分配和系统服务的高可用性

    令牌桶算法由一个存放令牌(tokens)的桶和一个固定的填充速率组成。数据包发送之前必须先从桶中取得足够的令牌,每个数据包可能需要一个或多个令牌。

    • 桶的大小:决定了在任何给定时间点,桶中可以累积的最大令牌数。这个参数可以影响突发(burst)流量的大小
    • 令牌填充速率:决定了令牌进入桶中的速率,通常以令牌/秒计量。这个速率限制了长期的平均发送速率

    工作原理

    • 令牌的生成:令牌以固定的速率被添加到桶中。桶有一个容量上限,超过这个上限的令牌将会被丢弃
    • 数据包的发送:每个要发送的数据包必须先获取一定数量的令牌才能被发送。如果桶中有足够的令牌,数据包就可以立即发送并且相应数量的令牌从桶中移除;如果桶中令牌不足,数据包需要等待直到有足够的令牌
    • 突发流量的控制:由于桶可以存储令牌,因此在令牌积累时允许一定程度的突发传输。这是通过在一段时间不使用令牌,然后使用积累的令牌来一次性发送多个数据包来实现的

    工作流程

    • 桶以一定的固定速率(r)获得令牌。
    • 桶中最多可以存储固定数量(b)的令牌,以允许一定程度的突发传输。
    • 当一个数据包到达时,如果桶中有足够的令牌,则从桶中移除相应数量的令牌,并允许数据包发送。
    • 如果桶中的令牌不足,则根据具体实现,数据包可能被丢弃或排队等待。

应用场景;

  • 网络带宽管理:通过限制网络流量的速率,确保网络资源被公平使用,防止某一应用或用户占用过多带宽。
  • 服务器请求处理:在服务器或API服务中,限制请求的速率来保护后端服务不被过载,提高系统的稳定性和可用性。
  • 多媒体传输:在视频流和音频流传输中控制数据的发送速率,以适应不同网络条件和缓冲需求

优点

  • 灵活性:令牌桶算法允许一定程度的突发流量,比较灵活地应对突增的数据传输需求。
  • 平滑网络流量:通过控制数据发送的平均速率,帮助平滑网络流量波动,减少拥塞。
  • 适应性:能够适应不同的网络环境和流量模式,通过调整令牌生成速率和桶的大小来满足不同的需求。

令牌桶实现可以浏览此前的博客:

UNIX环境编程-并发(11)中的

  • 一、信号 下的 7.令牌桶算法:令牌桶实现/实例程序alarm实现/实例程序setitimer实现
  • 二、线程 下的 7.线程令牌桶: 查询法/信号量通知法


3.多播/组播

(1) 多播

定义:多播是一种网络传输机制,它允许单个数据源同时向多个接收者发送数据(而广播是所有可能的目标是有区别的)。这与单播(一个源到一个目标)和广播(一个源到所有可能的目标)相对。

用途:多播主要用于节省带宽和提高效率,因为数据包只发送一次,但可以被网络中的多个接收者同时接收。这在流媒体、视频会议、实时应用程序等场景中非常有用。

如何工作:在多播中,数据包被发送到一个特定的多播组地址。网络设备(如路由器)识别这个地址,并将数据包路由到订阅了该多播组的所有接收者


(2) 组播

定义:组播通常是指在特定的网络协议(如 IP 组播)中实现的多播。

IP 组播:在 IP 网络中,组播使用特定的地址范围(例如,IPv4 中的 224.0.0.0 到 239.255.255.255)。组播地址代表一个组,数据发送到这个地址将被分发给加入该组的所有成员


(3) 组播的地址范围

IP 组播地址被定义在特定的范围内。对于 IPv4,这个范围是 224.0.0.0239.255.255.255

这个范围内的地址用于表示网络中的一个组播组,而不是特定的单个主机


(4) 224.224.224.224

在实际中,224.224.224.224 这样的地址通常用于组播通信,比如视频流、多点会议系统,或者其他需要同时发送数据到多个接收者的应用。不过,这个特定的地址并没有被分配给任何 “众所周知的” 组播服务(像其他一些较低的组播地址那样)。这意味着它可能被用于特定应用程序或网络的自定义用途



4.UDP通信数据包推荐大小

UDP时必须考虑数据包的大小,特别是要考虑网络的最大传输单元(MTU)和避免IP分片

(1) 网络的最大传输单元(MTU)

MTU 是网络可以承载的最大数据包大小,不同的网络其MTU可能不同,但大多数以太网的标准MTU大小是1500字节。考虑到IP头和UDP头的大小(IP头通常20字节,UDP头8字节),理想的UDP数据包大小应该小于或等于1472字节(1500 - 20 - 8 = 1472)

注意:数据包通常包括多层协议头,其中IP头和UDP头是最为基础和重要的部分,它们位于网络层和传输层,分别用于正确路由数据包到目的地址和确保数据包的端到端传输

(2) 避免IP分片

当UDP数据包大于网络的MTU时,数据包会在网络层被分片,这会增加重组数据包的负担,并可能因为某个片段的丢失导致整个数据包的丢失。因此,为了最小化这些风险,推荐的做法是使UDP数据包的大小不超过1472字节,以适应大多数网络环境



5.可变长数组(柔性数组成员)

示例:

1
2
3
4
struct msg_channel_st {
chnid_t chnid;
uint8_t data[1];
}

在C语言中,上述 struct msg_channel_st 结构体中的 data[1] 数组通常被用作一种编程技巧,有时被称为“柔性数组成员”(flexible array member)或“结构体尾数组”(struct hack)。尽管在这个具体示例中使用的是一个元素大小的数组,这实际上是一种较老的技术手段,用于实现类似于C99标准中引入的柔性数组成员的功能

开发者经常使用大小为1的数组作为结构体的最后一个成员,以模拟可变大小的数据。这种做法允许结构体存储和访问一个不确定长度的数组,而不需要另外进行内存分配调用

如何工作:

在这种模式下,通常分配给结构体的内存要比结构体本身声明的更大,以便包括足够的空间来存储实际需要的数组数据

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdlib.h>
#include <stdio.h>

typedef int chnid_t; // 假设 chnid_t 是 int 类型

struct msg_channel_st {
chnid_t chnid;
uint8_t data[1];
};

int main() {
int desired_size = 100; // 假设我们需要100个字节的数据
struct msg_channel_st *msg = malloc(sizeof(struct msg_channel_st) + desired_size - 1);
if (!msg) {
perror("Failed to allocate memory");
return -1;
}

msg->chnid = 123;
for (int i = 0; i < desired_size; ++i) {
msg->data[i] = i % 256;
}

// 使用 msg->data[0] 到 msg->data[desired_size - 1]
free(msg);
return 0;
}

在这个例子中,data[1] 仅占用一个字节的空间,但实际分配给 msg 的内存足以扩展 data 数组到100字节。这样,就可以通过 data 数组索引超过1,直接使用后面的内存空间

(1) C99 标准的柔性数组成员

从C99开始,语言标准提供了正式的支持通过将数组的大小声明为零来创建柔性数组成员:

1
2
3
4
struct msg_channel_st {
chnid_t chnid;
uint8_t data[]; // 柔性数组成员
};

这种方式更清晰,并且由标准支持,减少了可能的未定义行为。使用零长度数组的方式比使用 data[1] 更为标准和安全,因此在支持C99的编译器上推荐使用柔性数组成员。



6.C语言中结构体数据对齐方式

在 C 语言中,可以使用特定的编译器指令或属性来控制数据对齐:

  • GNU C 使用 __attribute__((packed)) 来取消对齐,使结构体尽可能紧凑。
  • GNU C 也可以使用 __attribute__((aligned(x))) 来指定一个特定的对齐界限。
  • MSVC 使用 #pragma pack(push, n)#pragma pack(pop) 来控制结构体的对齐。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
struct __attribute__((packed)) PackedExample {
char a;
int b;
char c;
};

// 或者
struct PackedExample {
char a;
int b;
char c;
}__attribute__((packed));

在这个例子中,PackedExample 结构体将不会有任何填充,成员 b 的对齐可能不是其自然界限,这可能影响性能,或在某些硬件上引发问题



7.相关函数

(1) getopt()

getopt() 函数是一个用于解析命令行选项的 C 语言标准库函数,它帮助程序处理命令行参数,使得用户可以输入格式化的选项和参数

1
2
3
#include <unistd.h>

int getopt(int argc, char * const argv[], const char *optstring);

参数:

  • argc: 命令行参数的数量。
  • argv: 命令行参数的数组。
  • optstring: 一个字符串,包含程序可识别的选项字符(程序只接受短选项名,如-h),如果选项后跟冒号:,则表示该选项后必须跟一个参数值

返回:

  • 返回识别到的选项字符。
  • 如果所有命令行选项都已解析完毕,则返回 -1。
  • 如果遇到一个选项字符不在optstring中,则返回字符?
  • 如果一个需要参数的选项未给出参数,则返回字符:(如果optstring以冒号开始)

全局变量

  • optarg: 指向当前选项参数(如果有)的指针。
  • optind: 下一个将被处理的元素的索引。
  • opterr: 如果不为零,getopt()会在遇到错误时打印错误信息
  • optopt: 它在某些特定情况下存储关于命令行选项的额外信息。当 getopt() 调用返回 ‘?’ 来表示一个错误——通常是因为遇到了一个未知选项或者一个需要参数的选项没有提供参数,optopt 变量会被设置为引起错误的选项字符

实例:

使用getopt()函数,程序接受两个选项:-a-b,以及一个需要参数的选项 -c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <unistd.h>

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

// optstring中的冒号表示-c选项后必须有一个关联的参数
while ((opt = getopt(argc, argv, "abc:")) != -1) {
switch (opt) {
case 'a':
printf("Option a is specified\n");
break;
case 'b':
printf("Option b is specified\n");
break;
case 'c':
printf("Option c is specified with value %s\n", optarg);
break;
case '?': // 未知选项
printf("Unknown option: %c\n", optopt);
break;
default:
printf("Other error\n");
}
}

// 处理额外的参数
for (; optind < argc; optind++) {
printf("Additional argument: %s\n", argv[optind]);
}

return 0;
}

(2) getopt_long()

函数 getopt_long 是 C 语言中用于解析命令行选项的一个函数,它扩展了 getopt 函数的功能,允许程序接受长选项名(例如 --help)和短选项名(例如 -h

函数原型

1
2
3
4
5
6
#include <getopt.h>

int getopt_long(int argc, char * const argv[],
const char *optstring,
const struct option *longopts,
int *longindex);

参数:

  • argcargv:这是从 main 函数传递的参数数量和参数值数组,分别表示命令行参数的个数和参数数组。
  • optstring:这是一个包含合法选项字符的字符串。如果一个字符后面跟随一个冒号 (:),那么它需要一个参数(例如 "a:b:" 表示选项 -a-b 都需要参数)。
  • longopts:一个指向 option 结构数组的指针,用于定义长选项。
  • longindex:一个指向整数的指针,用于存储当前解析的长选项在 longopts 数组中的索引。

option结构体定义

1
2
3
4
5
6
struct option {
const char *name; // 长选项名
int has_arg; // 选项是否需要参数
int *flag; // 如何返回值:NULL表示返回val,非NULL表示返回0并设置flag指向的变量为val
int val; // 选项短名称或返回值
};
  • name:长选项的名称(例如 "help")。
  • has_arg:这个选项是否需要参数,可能的值为 no_argumentrequired_argumentoptional_argument
  • flag:如果不为 NULL,则 getopt_long 返回 0,并且将 val 的值存入 flag 指向的变量。如果为 NULL,getopt_long 直接返回 val
  • val:用于指定函数找到该选项时返回的值,或者是相对应的短选项字符。

(3) if_nametoindex()

if_nametoindex() 是一个用于将网络接口名称转换为相应的索引号的标准库函数

1
2
3
#include <net/if.h>

unsigned int if_nametoindex(const char *ifname);

参数:

  • ifname:指向要查找索引的网络接口名称的字符串

返回值:

  • 返回值是接口的索引号,这是一个正整数,用于唯一标识系统中的网络接口。
  • 如果指定的接口不存在或者发生其他错误,if_nametoindex() 返回 0,并且 errno 被设置为相应的错误代码。

(4) dup2()

dup2() 函数是用来复制文件描述符的系统调用,它允许将一个已存在的文件描述符复制到另一个指定的文件描述符上。如果目标文件描述符已经打开,dup2() 会先将其关闭,然后进行复制。这个功能经常在重定向文件描述符,特别是在实现标准输入输出重定向时使用。

newfd关闭,将oldfd作为newfd的位置

1
2
3
#include <unistd.h>

int dup2(int oldfd, int newfd);

参数:

  • oldfd:要被复制的文件描述符
  • newfd:复制的目标文件描述符。如果 newfd 已经打开,dup2() 会先将其关闭(如果关闭失败,dup2() 也会失败并返回)

返回值:

  • 成功时返回新的文件描述符(即 newfd);
  • 失败时返回 -1 并设置 errno 以指示错误原因

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
int filefd;

filefd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (filefd < 0) {
perror("open");
exit(EXIT_FAILURE);
}

// 将标准输出重定向到文件
if (dup2(filefd, STDOUT_FILENO) < 0) {
perror("dup2");
close(filefd);
exit(EXIT_FAILURE);
}

// 现在,所有标准输出将写入到文件
printf("This will go to the file 'output.txt'\n");

// 关闭文件描述符
close(filefd);
return 0;
}

(5) execl()

execl() 函数是一种执行程序的系统调用,属于exec函数族中的一个。这些函数用于在当前进程的上下文中加载并运行一个新的程序,即,它们用于替换当前进程的内存空间与运行的代码,而不是创建一个新的进程。这意味着一旦 execl() 成功执行,原始程序的代码和数据将被新程序的代码和数据完全替代,并且原程序中 execl() 调用之后的代码不会被执行

1
2
3
#include <unistd.h>

int execl(const char *path, const char *arg, ... /*, (char *) NULL */);
  • path:要执行的程序的路径。
  • arg:传递给新程序的参数列表,以NULL终止。第一个参数通常是程序的名称

execl() 需要指定程序的完整路径,随后是要传递给该程序的参数列表,这些参数必须以 NULL 结尾以标示参数列表的结束。注意,这些参数都是以字符串的形式传递的,包括那些本质上是数字的参数也需要先转换为字符串

返回值:

  • 如果 execl() 调用成功,则不会返回值,因为当前进程的映像已被新的程序替换
  • 如果有错误发生,execl() 会返回 -1,并设置全局变量 errno 来指示错误的类型

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <unistd.h>

int main() {
printf("Executing 'ls' program to list directory contents\n");

// 执行ls命令,列出当前目录下的所有文件和文件夹
execl("/bin/ls", "ls", "-l", (char *) NULL);

// 如果execl()调用失败,将执行以下代码
perror("execl failed to run ls");
return 1;
}

如果 execl() 调用成功,它将执行 ls 命令来列出当前目录的内容,并且之后的 perror()return 语句将不会执行

在实例中,"/bin/ls"execl 函数中指定的程序的路径。它指向要执行的程序文件在文件系统中的确切位置。这里的 /bin/ls 是Unix和类Unix操作系统中 ls 命令的标准位置。ls 命令用于列出目录内容

/bin/:这是许多Unix和类Unix系统中用于存放基本用户二进制文件的标准目录。这些二进制文件通常是系统操作所必需的,包括如 lscatmv 等常用命令

ls:这是一个程序,用于列出计算机文件系统中目录的内容。在此实例中,ls 被用来显示当前工作目录下的所有文件和子目录

使用注意

由于 execl() 替换了当前进程的内容,它通常在创建了一个子进程(通过 fork())之后使用,这样父进程可以继续执行,而子进程则加载并运行新的程序。如果直接在父进程中使用 execl() 而没有先进行 fork(),那么一旦 execl() 调用成功,原始的父进程将不存在,被新程序取代


(6) setsid()

setsid() 函数在 Unix-like 系统中用于创建一个新的会话,并设置调用进程为该会话的会话领导,并且调用的进程还会成为进程组的组长,此外调用进程会脱离控制终端。这个函数通常在守护进程的创建过程中使用,以确保该进程与任何终端脱离关联,避免意外的用户交互或信号影响进程的运行

1
2
#include <unistd.h>
pid_t setsid(void);

返回值:

  • 成功时:返回新会话的会话ID,该ID也是新会话中的新进程组的进程组ID,并且是调用进程的进程ID。
  • 失败时:返回 -1,并设置 errno 以指示错

错误情况:

  • EPERM:调用进程已经是一个进程组的领导。在这种情况下,不能创建新会话,因为新的会话领导不能是现有进程组的领导

使用场景:

setsid() 用于创建一个新会话并使调用进程成为会话领导,脱离所有控制终端。这是通过以下方式实现的:

  • 会话领导:调用进程成为新会话的领导,这意味着它不再属于其原来的会话和进程组,也不会有控制终端
  • 进程组领导:调用进程同时成为新进程组的领导
  • 终端脱离:由于会话脱离了控制终端,任何尝试打开终端设备的操作都将失败,除非重新分配控制终端

实例:

使用 setsid() 在创建守护进程时创建新会话:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
pid_t pid = fork();

if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}

// 子进程继续
pid_t sid = setsid();
if (sid < 0) {
perror("setsid failed");
exit(EXIT_FAILURE);
}

printf("New session ID: %d\n", sid);

// 接下来的代码是守护进程的工作
while(1) {
// 守护进程任务
sleep(10);
}

return 0;
}
setsid在创建守护进程时创建新会话,为什么父进程需要退出
  • 资源占用:父进程继续存在意味着系统资源(如内存和进程表项)被占用。如果父进程仅用于产生守护进程而无其他用途,其继续存在可能被视为资源浪费
  • 进程管理:在某些情况下,父进程可能用于监控或管理子进程(守护进程)。这可以是有意的设计选择,用于控制或重启子进程等。然而,如果父进程意外终止,它可能会留下子进程成为孤儿进程,尽管这通常不会影响守护进程的运行,因为它已经通过 setsid() 与父进程的会话脱离
  • 虽然守护进程的创建通常建议父进程退出以避免任何潜在的干扰或资源浪费,但如果父进程继续存在,这并不一定会直接影响守护进程的操作,前提是守护进程已经通过 setsid() 成功独立于任何会话。然而,通常最好遵循典型的模式,即父进程在创建守护进程的子进程后退出,除非有特别的设计需求使得父进程需要持续存在

(7) sigaction()

sigaction() 函数用于在进程中检查或修改与指定信号相关联的处理动作

1
2
3
#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:

  • signum: 需要操作的信号编号,例如 SIGINT, SIGTERM 等。
  • act: 指向 sigaction 结构的指针,该结构指定了新的信号处理设置。如果此参数为 NULL,系统将不修改当前的信号处理动作。
  • oldact: 指向 sigaction 结构的指针,用于保存信号的当前处理设置(在 sigaction 调用之前的设置)。如果此参数为 NULL,以前的信号处理动作将不被保存

sigaction结构定义

1
2
3
4
5
6
7
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
  • sa_handler: 指定信号处理函数的指针,该函数接受一个整型信号编号作为参数。
  • sa_sigaction: 一个更高级的信号处理函数,它可以接收额外的信号信息,用于 SA_SIGINFO 标志被设置时。
  • sa_mask: 在信号处理函数执行时,额外需要阻塞的信号集合。
  • sa_flags: 用于指定信号处理的各种选项和行为,比如 SA_RESTART(使被信号中断的系统调用自动重启)和 SA_SIGINFO(使用 sa_sigaction 函数而非 sa_handler)。
  • sa_restorer: 用于指定一个恢复函数,这是历史遗留选项,在现代系统中通常不使用。

(8) nanosleep()

naaosleep()提供比传统的 sleep 函数更高的时间分辨率,允许使用纳秒级别的精度来指定睡眠时间

1
2
3
#include <time.h>

int nanosleep(const struct timespec *req, struct timespec *rem);

参数:

  • req:指向 timespec 结构的指针,该结构指定了要暂停的时间长度。timespec 结构如下定义:
1
2
3
4
struct timespec {
time_t tv_sec; // 秒
long tv_nsec; // 纳秒
};
  • rem:如果非 NULL,则在函数返回时,此结构会被设置为未睡完的剩余时间。这通常在 nanosleep 被信号打断时发生,允许程序决定是否重新开始剩余的睡眠时间

返回值:

  • 成功:返回 0
  • 错误:返回 -1 并设置 errno 来指示错误类型

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <time.h>

int main() {
struct timespec ts;
ts.tv_sec = 2; // 秒
ts.tv_nsec = 500000000L; // 纳秒

// 调用 nanosleep
if (nanosleep(&ts, NULL) < 0) {
perror("nanosleep");
} else {
printf("Slept for 2.5 seconds\n");
}

return 0;
}

(9) pthread_once()

该函数确保某个指定的初始化函数在一个程序中只被执行一次,即使多个线程尝试执行它。这个函数特别适用于多线程环境中的一次性初始化任务,例如初始化全局变量、设置环境或配置单例模式

1
2
3
#include <pthread.h>

int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));

参数:

  • once_control:指向 pthread_once_t 类型的变量的指针,该变量必须使用 PTHREAD_ONCE_INIT 初始化。这个变量跟踪初始化函数是否已被调用
  • init_routine:指向要执行的初始化函数的指针。这个函数没有参数和返回值。pthread_once 会确保这个函数只被执行一次

返回:

  • 返回 0 表示成功。
  • 返回非零值表示发生错误

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <pthread.h>
#include <stdio.h>

// 定义全局 once 控制变量
pthread_once_t once = PTHREAD_ONCE_INIT;

// 初始化函数
void initialize() {
printf("Initialization done.\n");
}

// 线程函数
void* thread_function(void* arg) {
// 使用 pthread_once 确保初始化函数只被执行一次
pthread_once(&once, initialize);
printf("Thread %ld running.\n", (long) arg);
return NULL;
}

int main() {
pthread_t threads[4];

// 创建多个线程
for (long i = 0; i < 4; i++) {
pthread_create(&threads[i], NULL, thread_function, (void*)i);
}

// 等待所有线程完成
for (int i = 0; i < 4; i++) {
pthread_join(threads[i], NULL);
}

return 0;
}

(10) glob()

glob() 函数是在 POSIX 系统中用于文件名模式匹配的函数。它按照指定的模式搜索匹配的文件名,这个模式可以包含通配符,如 * (匹配任何字符串) 和 ? (匹配任何单个字符),以及字符范围符号 [...]

1
2
3
#include <glob.h>

int glob(const char *pattern, int flags, int (*errfunc)(const char *epath, int eerrno), glob_t *pglob);

参数:

  • pattern:用于文件匹配的模式,可以包含通配符。
  • flags:控制函数行为的标志,可以通过位或运算符 | 组合多个标志。
  • errfunc:一个回调函数,当 glob() 遇到不能读取的路径时调用。这个函数接受两个参数:导致错误的路径和对应的 errno 值。如果此函数返回非零值,则 glob() 将停止处理并返回。
  • pglob:一个指向 glob_t 结构的指针,该结构用于存储匹配到的文件名和其他相关信息。

常用的flags:

  • GLOB_NOCHECK:如果没有找到匹配项,返回原始模式。
  • GLOB_MARK:在每个返回的目录名后添加一个斜杠(/)。
  • GLOB_NOSORT:不对结果进行排序。
  • GLOB_NOESCAPE:使得反斜杠 \ 不能作为转义字符,所有字符都按字面意义解释

返回值:

  • 成功时,glob() 返回 0
  • 如果发生错误,可能返回 GLOB_NOSPACE(内存不足),GLOB_ABORTED(读取错误,例如由 errfunc 返回非零值导致),或 GLOB_NOMATCH(没有找到匹配的文件)。

glob_t结构体:

1
2
3
4
5
6
typedef struct {
size_t gl_pathc; /* 路径计数,匹配到的文件数量 */
char **gl_pathv; /* 指向找到的文件名数组的指针 */
size_t gl_offs; /* 在数组开始处保留的空槽位 */
int gl_flags; /* 用于传递给 glob() 的标志,可能不在所有实现中都存在 */
} glob_t;

字段说明:

  • gl_pathc:表示匹配到的路径数量,即 gl_pathv 数组中存储的文件名个数。
  • gl_pathv:是一个指向字符串数组的指针,每个字符串都是一个与指定模式匹配的文件路径。
  • gl_offs:在 gl_pathv 数组的开头保留的未使用槽的数量,这可以用于将一些特定的数据存放在路径数组的前面。
  • gl_flags:用于存储标志位,这些标志在调用 glob() 时指定,以控制函数的行为

实例:搜索当前目录下所有的.txt文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <glob.h>

int main() {
glob_t results;
int ret = glob("*.txt", 0, NULL, &results); // 查找所有 .txt 文件

if (ret == 0) {
for (size_t i = 0; i < results.gl_pathc; i++) {
printf("%s\n", results.gl_pathv[i]);
}
// globfree() 来释放为 glob_t 结构分配的内存
globfree(&results);
} else if (ret == GLOB_NOMATCH) {
printf("No .txt files found.\n");
} else {
printf("An error occurred.\n");
}

return 0;
}

(11) memcpy()

函数用于将一块内存的内容复制到另一块内存

1
2
3
#include <string.h>

void *memcpy(void *dest, const void *src, size_t n);

参数:

  • dest:指向目标内存区域的指针,这块内存将接收数据。
  • src:指向源内存区域的指针,将从这里复制数据。
  • n:要复制的字节数。

返回值:

  • memcpy 函数返回指向目标内存区域的指针,即 dest 参数的值

(12) pread()

pread 函数是一个用于从文件中读取数据的 UNIX 系统调用。这个函数提供了一种在不改变文件指针位置的情况下从指定位置读取数据的能力,它是对传统 read 函数的补充。这特别有用在多线程环境中,因为它避免了使用单独的 lseekread 调用,从而避免了潜在的竞争条件

1
2
3
#include <unistd.h>

ssize_t pread(int fd, void *buf, size_t count, off_t offset);

参数:

  • fd:文件描述符,引用一个已打开的文件。
  • buf:指向一个缓冲区的指针,用来存储从文件中读取的数据。
  • count:指定要读取的字节数。
  • offset:文件中的偏移量,从文件开始处的字节数,指定从哪里开始读取数据。

返回值:

  • 成功时pread 返回实际读取的字节数,如果达到文件末尾则返回 0。
  • 失败时,返回 -1 并设置 errno 以指示错误原因

功能描述:

preadread 的不同之处在于,pread 允许你指定一个偏移量,从该偏移量处开始读取数据,而不会影响文件的当前偏移量(即文件描述符所指向的位置)。这使得多个线程或进程可以安全地同时读取同一个文件的不同部分,而不会互相干扰


(13) pause()

pause()函数用于使调用它的进程挂起直到捕获到一个信号。由于 pause() 只在捕获到信号后被中断,它主要用于在进程中等待外部事件的发生。这种用途在某些类型的程序中特别有用,如那些需要等待某些系统事件或用户操作的守护进程或服务器

1
2
3
#include <unistd.h>

int pause(void);

功能行为:

  • 功能pause() 使调用它的进程挂起,直到该进程捕获到一个信号。一旦接收到信号,如果该信号没有结束进程,pause() 将返回。
  • 返回值pause() 函数总是返回 -1,并且 errno 被设置为 EINTR,表明错误是由信号中断引起的

使用场景:

  • 等待信号:在一些应用程序中,可能需要在执行某些操作之前等待特定的信号。例如,程序可能挂起等待系统管理员发送的配置重载信号。
  • 简单的事件驱动程序:在一些基于事件的程序中,pause() 可以用来在低成本地等待事件发生,因为它不消耗 CPU 资源。
  • 与信号处理结合pause() 通常与信号处理器结合使用,以响应特定的信号。程序可以设置信号处理函数来处理如终止信号或用户自定义信号等

(14) pipe()

管道(pipe)是通过系统调用 pipe() 创建的。这个调用允许两个进程通过一个无名管道进行单向的数据传输。pipe() 函数非常适用于在父子进程间实现简单的进程间通信

1
2
3
#include <unistd.h>

int pipe(int pipefd[2]);

参数:

  • pipefd: 一个整型数组,长度为2。成功调用 pipe() 后,pipefd[0] 将被设置为管道的读端,而 pipefd[1] 将被设置为管道的写端

返回值:

  • 成功时返回0。
  • 失败时返回-1,并设置相应的errno值

管道熟悉:

  • 单向数据流:管道只支持单向数据流。如果需要双向通信,需要创建两个管道。
  • 数据的先进先出(FIFO)特性:管道中的数据遵循FIFO原则,即首先写入管道的数据也将首先被读出
  • 阻塞性和非阻塞性行为:默认情况下,如果管道的读端没有数据,读操作会阻塞,直到有数据可读;如果管道的写端已满,写操作会阻塞,直到管道中有足够的空间;管道可以通过修改文件描述符的属性设置为非阻塞模式

子进程读管道时候,关闭写端;父进程写管道,关闭读端,原因

  • 避免死锁:管道有容量限制,如果管道满了,写操作将阻塞,直到有足够空间。如果子进程也能写(写端没关闭),父进程在写入数据时可能会等待子进程从管道中读取数据来释放空间,但如果子进程在等待父进程写入完毕后才开始读取,这就可能导致死锁

  • 确保数据完整性:关闭不使用的端口可以避免意外写入或读取,保护数据传输过程的完整性。如果子进程不关闭管道的写端,那么理论上子进程也能向管道中写入数据,这可能会与父进程的数据交织在一起,导致接收方难以区分数据来源,引起数据处理错误

  • 资源管理:在任何操作系统中,文件描述符都是有限的资源。及时关闭不需要的文件描述符有助于避免资源泄漏,确保系统资源的有效利用


(15) strdup()

strdup() 用于复制一个字符串,分配一个新的内存空间,并将原字符串的内容复制到这个新的内存空间中

strdup() 接收一个 const char* 类型的字符串作为参数,为它分配与原字符串相同长度加上结束字符 \0 的内存空间,然后将原字符串的内容复制到新分配的内存中,并返回这个新字符串的指针。

1
2
3
#include <string.h>

char *strdup(const char *s);

返回:

  • 返回一个指针,指向新分配的字符串,该字符串是传入的字符串 s 的一个副本。
  • 如果内存分配失败,则返回 NULL

(16) gfets()

fgets()用于从指定的文件流中读取字符串。它在 <stdio.h> 头文件中定义,并常用于从文件或标准输入(如键盘)读取一行文本

1
char *fgets(char *str, int n, FILE *stream);

参数:

  • str:指向一个字符数组的指针,这个数组将用来存储从文件流中读取的字符串
  • n:指定最多读取的字符数,包括空终止字符(\0)。通常,为了安全读取一行,n 应该设置为目标字符数组的大小
  • stream:指向 FILE 对象的指针,该对象标识了要读取的输入流。常见的输入流包括标准输入(stdin),或者是通过 fopen() 打开的文件

返回值:

  • 如果成功,fgets() 返回 str 的指针。
  • 如果遇到文件结束(EOF)或读取错误,且没有读取到任何字符,返回 NULL
  • 如果在读取到 n-1 个字符之前遇到了一个换行符,fgets() 会将换行符也读入字符串中,并在最后添加一个空字符(\0


8.设置套接字属性

(1) 加入多播组

多播是指数据传输的一种方式,允许将数据包发送给多个目的地址。创建和加入一个多播组涉及到在套接字上设置一些特定的选项。在 Linux 系统中,这通常涉及到设置 IP_ADD_MEMBERSHIP 套接字选项,以便加入一个多播组

第一步

首先,多播通常是基于UDP实现的,因此需要创建一个报式UDP套接字

1
2
3
4
5
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
第二步

设置结构体struct ip_mreqn,指定多播组请求的信息

结构体struct ip_mreq主要用于套接字编程中设置 IP 层面的选项,如加入或离开一个多播组。它是在使用 setsockopt() 函数时,指定多播相关操作(如加入多播组)所必须的数据结构,其定义如下。

而结构体struct ip_mreqn 是在 Linux 中定义的一个结构体,用于在使用 IP 多播时设置和获取套接字选项。这个结构体类似于 struct ip_mreq,但提供了更多控制,尤其是可以直接指定网络接口的索引,而不仅仅是通过 IP 地址指定网络接口。这对于多网络接口的环境特别有用。

1
2
3
4
5
6
7
8
9
10
struct ip_mreq {
struct in_addr imr_multiaddr; // IP 多播组的地址
struct in_addr imr_interface; // 本地接口的 IP 地址
};

struct ip_mreqn {
struct in_addr imr_multiaddr; // IP 多播组地址
struct in_addr imr_address; // 本地接口的 IP 地址
int imr_ifindex; // 网络接口索引
};
  • imr_multiaddr: 用于指定多播组的 IP 地址。加入或离开的多播组必须是有效的多播地址,通常位于 224.0.0.0 到 239.255.255.255 范围内。
  • imr_address: 指定本地网络接口的 IP 地址。使用该字段可以选择多播数据应该通过哪个具体的本地网络接口进行接收或发送。如果设置为 INADDR_ANY,系统会选择默认接口。
  • imr_ifindex: 网络接口的索引。这个索引是系统级别的网络接口的唯一标识,可以通过如 ifconfigip link 命令看到的接口编号。设置此字段可以直接指定哪个网络接口用于多播,而无需通过 IP 地址来确定。

使用场景

  • 加入多播组:使用 setsockopt()IP_ADD_MEMBERSHIP 选项时,可以通过此结构体指定加入的多播组和使用的网络接口。
  • 离开多播组:使用 setsockopt()IP_DROP_MEMBERSHIP 选项时,同样通过此结构体指定离开的多播组和网络接口。
1
2
3
4
5
6
7
8
9
10
11
12
struct ip_mreqn mreqn;
memset(&mreqn, 0, sizeof(mreqn));
mreqn.imr_multiaddr.s_addr = inet_addr("224.0.0.1"); // 多播地址
mreqn.imr_address.s_addr = htonl(INADDR_ANY); // 选择默认接口
mreqn.imr_ifindex = 0; // 0 表示让系统选择接口


struct ip_mreqn mreqn;
memset(&mreqn, 0, sizeof(mreqn));
mreqn.imr_multiaddr = inet_addr("224.0.0.1"); // 多播地址
mreqn.imr_address = htonl(INADDR_ANY); // 选择默认接口
mreqn.imr_ifindex = 0; // 0 表示让系统选择接口
第三步

使用setsockopt()函数设置套接字选项

1
2
3
4
if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
perror("setsockopt(IP_ADD_MEMBERSHIP) failed");
exit(EXIT_FAILURE);
}
  • IPPROTO_IP:这是设置 IP 层级的选项
  • IP_ADD_MEMBERSHIP:这个选项用于加入一个多播组
完整流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
/*1. 创建UDP套接字*/
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}

/*2. 设置struct ip_mreqn结构体属性*/
struct ip_mreqn mreqn;
memset(&mreqn, 0, sizeof(mreqn));
inet_pton(AF_INET, "224.2.2.2", &mreq.imr_multiaddr);; // 多播地址
inet_pton(AF_INET, "0.0.0.0", &mreq.imr_address); // 选择默认接口
mreq.imr_ifindex = if_nametoindex("etho"); // 0 表示让系统选择接口

/*3. 设置套接字属性,在IP层进行设置,打开添加多播组选项IP_ADD_MEMBERSHIP*/
if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreqn, sizeof(mreqn)) < 0) {
perror("setsockopt(IP_ADD_MEMBERSHIP) failed");
close(sockfd);
exit(EXIT_FAILURE);
}

// 现在可以接收发向224.2.2.2多播组的数据

close(sockfd);
return 0;
}


(2) 创建多播组

设置多播套接字时候,通常需要考虑的套接字选项:

IP_MULTICAST_IF

  • 当系统拥有多个网络接口(如以太网、Wi-Fi等)时,需要明确指定哪一个接口用于多播数据的发送
  • 用途:指定用于发送多播数据包的出口接口

IP_MULTICAST_TTL

  • 用途:设置多播数据包的生存时间(Time To Live),即数据包在网络中可以跨越的最大路由器数量。
  • 设置方法:通常提供一个整数值
  • 指定了多播数据包可以在网络中传递的最大跳数(router hops)。TTL 的值决定了多播消息能传播的范围大小:
  • TTL=1:数据包只在同一子网中传递,不会被路由器转发到其他子网
  • TTL>1:数据包可以跨越多个子网,TTL 值越大,可到达的网络范围越广
  • 通常默认的TTL值为1

IP_ADD_MEMBERSHIP

  • 用途:用于加入一个多播组,允许接收发往该组的多播数据包。
  • 设置方法:提供一个 ip_mreq 结构,其中包含多播组的IP地址和用于接收该组数据包的本地接口的IP地址
IP_MULTICAST_IF属性设置

第一步:创建UDP套接字

1
2
3
4
5
6
7
8
int serverSd;
serverSd = socket(AF_INET,SOCK_DGRAM,0);
if(serverSd < 0)
{
// 套接字创建失败,向系统日志发送信息
perror("socket()");
exit(1);
}

第二步:设置结构体struct ip_mreqn,指定多播组请求的信息

指定创建的多播组ip,本机ip,以及指定用于发送多播数据包的出口接口,即通过什么网卡设备(网络接口)去发送多播数据

1
2
3
4
struct ip_mreqn mreq;
inet_pton(AF_INET, "224.2.2.2", &mreq.imr_multiaddr); // 多播组ip
inet_pton(AF_INET, "0.0.0.0", &mreq.imr_address); // 服务端本机ip
mreq.imr_ifindex = if_nametoindex("enp3s0"); // 网卡设备索引,enp3s0为机器的网卡设备名称

第三步:在IP层,设置属性名 IP_MULTICAST_IF 建立多播组,其中IPPROTO_IP表示IP层

1
2
3
4
5
if (setsockopt(serverSd, IPPROTO_IP, IP_MULTICAST_IF, &mreq, sizeof(mreq)) < 0)
{
perror("setsockopt()");
exit(1);
}

(3) 允许发送的多播数据被本地回环接口接收

IP_MULTICAST_LOOP 套接字选项用于控制是否允许发送的多播数据被本地回环接口接收,即控制发送的多播消息是否能够被同一主机上的其他多播套接字接收

注意事项

  • 在IP层IPPROTO_IP设置
  • 设置属性名IP_MULTICAST_LOOP
  • 布尔变量loop,属性开关

实例:设置 IP_MULTICAST_LOOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int sockfd; // 套接字描述符
u_char loop = 1; // 启用回送

sockfd = socket(AF_INET, SOCK_DGRAM, 0);

// 启用多播数据包的回送
if (setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) < 0) {
perror("setsockopt(IP_MULTICAST_LOOP) failed");
close(sockfd);
exit(EXIT_FAILURE);
}


9.守护进程

守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。一般采用以d结尾的名字

(1) 进程组

多个进程的集合就是进程组, 这个组中必须有一个组长, 组长就是进程组中的第一个进程,组长以外的都是普通的成员,每个进程组都有一个唯一的组ID,进程组的ID和组长的PID是一样的。

进程组中的成员是可以转移的,如果当前进程组中的成员被转移到了其他的组,或者进制中的所有进程都退出了,那么这个进程组也就不存在了。如果进程组中组长死了, 但是当前进程组中有其他进程,这个进程组还是继续存在的。下面介绍几个常用的进程组函数:

  • 得到当前进程所在的进程组的组ID
1
pid_t getpgrp(void);
  • 获取指定的进程所在的进程组的组ID,参数 pid 就是指定的进程
1
pid_t getpgid(pid_t pid);
  • 将某个进程移动到其他进程组中或者创建新的进程组
1
int setpgid(pid_t pid, pid_t pgid);

参数:

  • pid: 某个进程的进程ID
  • pgid: 某个进程组的组ID,如果pgid对应的进程组存在,pid对应的进程会移动到这个组中, pid != pgid;如果pgid对应的进程组不存在,会创建一个新的进程组, 因此要求 pid == pgid, 当前进程就是组长了

返回值:

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

(2) 会话

会话(Session)是一种处理进程组和终端交互的机制,会话使得系统能够组织和管理相关的进程群体

会话(session)是由一个或多个进程组组成的,一个会话可以对应一个控制终端, 也可以没有。一个普通的进程可以调用 setsid() 函数使自己成为新 session 的领头进程(会长),并且这个 session 领头进程还会被放入到一个新的进程组中。先来看一下setsid()函数的原型:

1
2
3
4
5
6
7
8
#include <unistd.h>

// 获取某个进程所属的会话ID
pid_t getsid(pid_t pid);

// 将某个进程变成会话 =>> 得到一个守护进程
// 使用哪个进程调用这个函数, 这个进程就会变成一个会话
pid_t setsid(void);

setsid函数使用的条件

  • 进程不能是进程组的领导:为了成功调用 setsid() 并创建一个新的会话,调用进程必须不是一个进程组的领导(即它的进程ID不能等于其进程组ID)。这是为了防止已有进程组或会话的领导直接创建新会话,可能导致系统中会话和进程组的结构混乱。

setsid函数使用的注意事项

  • 调用这个函数的进程不能是组长进程, 如果是该函数调用失败,如果保证这个函数能调用成功呢
    • 先fork()创建子进程, 终止父进程, 让子进程调用这个函数
  • 如果调用这个函数的进程不是进程组长, 会话创建成功
    • 这个进程会变成当前会话中的第一个进程,同时也会变成新的进程组的组长
    • 该函数调用成功之后, 当前进程就脱离了控制终端,因此不会阻塞终端

调用进程使用setsid函数的效果

  • 创建新的会话:调用进程将创建一个新的会话,并成为该会话的领导
  • 创建新的进程组:调用进程同时也会创建一个新的进程组,并成为该进程组的领导(进程组ID即为调用进程的pid
  • 断开与控制终端的联系:(一个会话可以有控制终端也可以没有)如果调用进程在调用 setsid() 时有一个控制终端,它将失去对该终端的控制。这样做是为了确保会话的独立性和安全性,避免会话领导意外接收到终端相关的信号或输入

控制终端的重新分配:

  • 一个没有控制终端的会话在需要的情况下可以重新打开和分配一个控制终端,但这通常不适用于守护进程。守护进程的设计理念是完全独立于用户交互操作,长时间在后台默默运行

(3) 创建守护进程步骤

如果要创建一个守护进程,标准步骤如下,部分操作可以根据实际需求进行取舍:

第一步: 创建子进程, 让父进程退出

  • 因为父进程有可能是组长进程,不符合条件,也没有什么利用价值,退出即可
  • 子进程没有任何职务, 目的是让子进程最终变成一个会话, 最终就会得到守护进程

第二步: 通过子进程创建新的会话,调用函数 setsid(),使该进程成为会话领头进程,脱离控制终端, 变成守护进程

第三步: 改变当前进程的工作目录 (可选项, 不是必须要做的)

  • 某些文件系统可以被卸载, 比如: U盘, 移动硬盘,进程如果在这些目录中运行,运行期间这些设备被卸载了,运行的进程也就不能正常工作了

  • 修改当前进程的工作目录需要调用函数 chdir()

  • 例如:将当前工作目录改变到根目录(/),以避免守护进程持续占用任何挂载点

  • int chdir(const char *path);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    **第四步**: 重新设置文件的掩码 (可选项, 不是必须要做的)

    * 掩码: umask, 在创建新文件的时候需要和这个掩码进行运算, 去掉文件的某些权限

    * 设置 umask 为 0 以**确保守护进程可以读写其创建的任何文件**,且不受继承的文件模式掩码的限制

    * 设置掩码需要使用函数 umask()

    * ```c
    mode_t umask(mode_t mask);

第五步: 关闭/重定向文件描述符 (不做也可以, 但是建议做一下)

  • 启动一个进程, 文件描述符表中默认有三个被打开了, 对应的都是当前的终端文件

  • 因为进程通过调用 setsid() 已经脱离了当前终端, 因此关联的文件描述符也就没用了, 可以关闭

  • close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    * 重定向文件描述符(和关闭二选一): 改变文件描述符关联的默认文件, 让他们指向一个特殊的文件/dev/null,只要把数据扔到这个特殊的设备文件中, 数据被被销毁了

    * ```c
    int fd = open("/dev/null", O_RDWR);
    // 重定向之后, 这三个文件描述符就和当前终端没有任何关系了
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);

第六步:根据实际需求在守护进程中执行某些特定的操作


(4) 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <syslog.h>

/*信号行为函数*/
void signal_handler(int sig) {
syslog(LOG_INFO, "Received signal to terminate.");
closelog(); // 关闭系统日志
exit(0); // 退出守护进程
}

void daemonize() {
pid_t pid;

pid = fork();

if (pid < 0) {
exit(1); // 子进程创建失败
}


if (pid > 0) {
printf("PPID EXIT.\n");
printf("daemon PID : %d\n",pid);
exit(0); // 父进程退出
}

// 因为父进程已经退出,如下都是子进程的工作

// 更改文件模式掩码
umask(0);

// 创建一个新的会话,子进程脱离控制终端
if (setsid() < 0) {
exit(0);
}

// 改变当前工作目录
if ((chdir("/")) < 0) {
exit(EXIT_FAILURE);
}

// 关闭标准的文件流
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
}

int main() {
// 创建守护进程
daemonize();

// 打开系统日志
openlog("mydaemon", LOG_PID, LOG_DAEMON);
syslog(LOG_NOTICE, "Daemon started.");

// 设置信号处理器
// SIGINT 与 SIGTERM 的信号行为都定义为 signal_handler 函数 退出守护进程
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);

while (1) {
pause(); // 挂起守护进程
}

syslog(LOG_NOTICE, "Daemon exiting");

return 0;
}

编译运行守护进程就可以执行起来了…

退出守护进程:

在代码中定义了SIGINTSIGTERM信号的行为为退出守护进程,因此可以使用在ubuntu终端使用kill指令向守护进程发送SIGTERM信号退出守护进程

1
$ kill [daemon_pid]


10.系统日志

使用 POSIX 标准提供的 syslog 接口。这组函数允许程序将消息发送到系统日志服务,这通常由系统的日志守护进程(如 syslogdrsyslogd)处理,这些日志可以配置为记录到不同的文件中,发送到远程日志服务器,或者以其他方式处理。

系统日志头文件

1
#include <syslog.h>

使用syslog函数,发送信息到日志系统

1
void syslog(int priority, const char *format, ...);
  • priority: 这个参数是一个由日志设施和日志级别组合的整数。设施用于指示消息的类型,级别用于指示消息的严重性
  • format: 类似于 printf,这是一个格式化字符串,后面可以跟随变量参数列表,用于生成最终的日志消息

设置日志选项和设施

在开始记录消息前,调用 openlog 函数来打开一个连接到系统日志器的通道:

1
void openlog(const char *ident, int option, int facility);
  • ident: 这个字符串将被添加到所有日志消息前,通常是程序的名称。
  • option: 用于修改日志记录的行为。常见选项包括 LOG_PID(在日志消息中包含程序 PID),LOG_CONS(如果日志消息不能记录到日志系统,则输出到控制台),等等。
  • facility: 指定日志消息的类型,常用的如 LOG_USER(用户级消息)、LOG_DAEMON(系统守护进程),等等

关闭日志

调用 closelog 来关闭与系统日志的连接:

1
void closelog(void);

实例:使用系统日志记录一条消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <syslog.h>

int main() {
// 打开日志
openlog("myapp", LOG_PID | LOG_CONS, LOG_USER);

// 写入一条日志消息
syslog(LOG_INFO, "Starting my application.");

// 在此处执行应用程序逻辑

// 写入另一条日志消息
syslog(LOG_INFO, "Exiting my application.");

// 关闭日志
closelog();

return 0;
}

(1) openlog()

openlog() 函数是用来打开与系统日志的连接,并设置后续 syslog() 调用的默认设施和行为的。这个函数定义在 <syslog.h> 头文件中,是用于向系统日志发送消息的一部分

1
2
3
#include <syslog.h>

void openlog(const char *ident, int option, int facility);

参数:

  • ident: 指向一个字符串的指针,该字符串将被加到所有由 syslog() 发送的消息前面。这通常是程序的名称。它可以帮助在日志文件中区分来自不同应用程序的日志消息。
  • option: 这个参数是一个整数,用于指定日志操作的选项。它可以是以下常量的按位或(bitwise OR)组合:
    • LOG_PID: 在每条日志消息中包括程序的进程ID。
    • LOG_CONS: 如果消息不能记录到日志,则输出到系统控制台。
    • LOG_ODELAY: 延迟打开日志设备直到第一次调用 syslog()(这是默认行为)。
    • LOG_NDELAY: 立即打开日志设备,而不是延迟打开。
    • LOG_NOWAIT: 不等待子进程(这在某些实现中可能会有所不同)。
    • LOG_PERROR: 除了发送到系统日志外,还将消息复制到标准错误输出。
  • facility: 指定日志消息的默认设施。设施参数用于指定日志消息的种类。每种设施代表了不同类型的程序。常用的设施有:
    • LOG_AUTH: 安全/授权消息(DEPRECATED,使用 LOG_AUTHPRIV)。
    • LOG_AUTHPRIV: 安全/授权消息(私有)。
    • LOG_CRON: 计时器守护进程。
    • LOG_DAEMON: 其他系统守护进程。
    • LOG_USER: 一般用户级消息。
    • LOG_LOCAL0LOG_LOCAL7: 为本地使用保留的设施。

(2) syslog()

syslog() 函数用于将日志消息发送到系统日志服务,这是一个用于中央日志管理的标准机制。消息可以由系统日志守护程序,如 syslogdrsyslogd,处理并根据配置决定如何处理这些消息(例如,将它们写入文件、发送至远程服务器或其他处理)

1
2
#include <syslog.h>
void syslog(int priority, const char *format, ...);

参数:

  • priority: 这个参数指定了消息的重要性,它是由日志级别和设施级别组成的。日志级别(如 LOG_INFOLOG_ERR)表明消息的严重性。设施级别(如 LOG_USERLOG_DAEMON)表明消息的种类。这两者通常使用按位或操作符(|)组合在一起。
  • format: 这是一个格式字符串,与 printf 函数的格式字符串相似,它定义了后续参数的格式。format 后面跟随的是可变数量的参数,这些参数用于替换格式字符串中的格式占位符。

日志级别:

  • LOG_EMERG: 紧急情况,通常会被立即广播到所有用户。
  • LOG_ALERT: 必须立即采取行动的条件。
  • LOG_CRIT: 关键条件,比如硬件故障等。
  • LOG_ERR: 错误条件。
  • LOG_WARNING: 警告条件。
  • LOG_NOTICE: 正常但重要的条件。
  • LOG_INFO: 信息性消息。
  • LOG_DEBUG: 调试级消息。


11.while(1) pause();的使用

pause() 放在一个无限循环中的主要作用是使程序保持活动状态,同时不消耗 CPU 资源,直到它接收到一个信号。这种模式常见于以下场景:

  • 信号驱动的程序:程序的执行依赖于外部信号的触发。例如,守护进程或服务器进程可能会使用这种模式,在等待系统事件或外部输入时保持低功耗状态。
  • 事件驱动的应用:在事件驱动的应用中,程序的主要任务是响应外部事件(如用户输入、系统消息等),而在事件之间,程序不需要做任何事情。

示例应用:

考虑一个守护进程,它需要在后台运行并响应系统信号进行特定的任务(如重新加载配置文件、关闭服务等)。使用 while(1) pause(); 可以让这个守护进程在不忙碌时不占用 CPU 资源,同时随时准备响应信号

注意:

  • 信号处理:使用这种结构时,通常需要提前设定信号处理函数。如果没有正确设置信号处理,程序可能无法响应任何事件,或者表现出不预期的行为。
  • 终止条件:虽然循环是无限的,但程序应能在接收到特定信号(如 SIGTERMSIGINT)时优雅地终止。通常,这需要在信号处理函数中实现适当的逻辑


12.在终端中使用ps -aux如何判断一个进程是否是守护进程

在使用 ps aux 命令查看进程信息时,并不能直接显示一个进程是否为守护进程(daemon)。守护进程通常是在后台运行的服务进程,它们没有控制终端。但是,你可以通过观察几个关键特征来间接判断一个进程是否可能是守护进程:

  • TTY: 守护进程通常没有终端(TTY)。在 ps aux 输出的结果中,如果某个进程的 TTY 列显示为 ?,这通常意味着该进程没有关联的终端,可能是一个守护进程
  • 父进程(PPID): 守护进程通常由 init 进程(PID 为 1)或者其他守护进程管理。可以查看 PPID 列,如果该列的值为 1,这表明它是由系统的初始进程启动的,很可能是守护进程。如果一个守护进程的父进程不为1,表示程可能是由另一个用户级进程(例如,一个脚本或服务启动器)启动的。这意味着它可能是一个守护进程,但不是系统级的守护进程,而是由特定的应用程序或服务管理
  • 进程名和目的: 某些进程名字可能会给出一些提示,比如以 d 结尾的进程往往是守护进程,如 sshd、httpd 等

通过使用ps aux并且通过管道输出所有条目带?的进程信息

1
ps aux | grep ' ? '


13.为什么端口号需要与服务端的相同

在网络通信中,端口号是用来识别发送到某个特定IP地址上的特定应用程序或服务的标识。当你在客户端和服务端设置相同的端口号时,你确保了两者能够在同一个通信频道上进行数据交换。

对于多播通信来说,这个概念尤其重要:

  • 多播地址:多播地址指定了数据包应该被送达的一组接收者。这是通过IP层的多播机制来实现的,其中数据包被送到所有订阅了该多播组的主机。
  • 端口号:端口号进一步细分了接收数据的具体应用程序。即使多个应用可能监听同一个多播地址,通过不同的端口号,它们可以区分哪些数据是它们需要处理的。

简而言之,将服务端和客户端设置为使用相同的端口号意味着:

  • 数据路由:服务端发送到特定多播地址和端口号的数据只会被监听相同地址和端口号的客户端接收。这就像是服务端在广播一个频道,而客户端调到这个频道上来接收信号。
  • 协议一致性:在开发和部署网络应用程序时,确保服务端和客户端在通信端口上达成一致是常见的实践,这有助于简化配置和减少潜在的错误。
  • 避免冲突:如果客户端尝试使用与服务端不同的端口号,它们将无法接收到服务端的数据,因为它们实际上监听的是一个不同的”频道”。

所以,在设计和实施基于多播的通信系统时,保持服务端和客户端在相同的端口上监听,是确保它们可以互相通信的关键步骤。这样设置有助于减少网络配置的复杂性,并确保数据的正确传递