"解锁进程间高效沟通,Linux IPC是你的关键钥匙!"#Linux系统编程之进程间通信【下】
前言
欢迎踏入Linux系统编程之进程间通信【下篇】的广阔领域!本篇博文将带您深入探索共享内存的奥秘,从概述其基本概念到实战编程应用,让您全面掌握。随后,我们将转向信号的世界,全面阐述信号的基本概念,并通过编程实例展示如何利用信号进行进程间通信,更将深入探讨信号如何携带消息,并以直观简图辅助理解,实战演示信号携带消息的编程技巧。此外,信号量作为重要的同步机制也将被详细介绍,通过分两部分进行的实战编程教学,确保您能够熟练掌握其使用。最后,我们还将展示信号量与共享内存的结合应用,让您领略它们在复杂进程间通信场景中的强大协作能力。希望本篇博文能够成为您学习Linux系统编程中进程间通信的宝贵资源,激发您深入探索的兴趣。看到这篇博文的朋友们,请您先点赞鼓励,再一同踏上这段精彩的探索之旅吧!
预备知识
一、C变量
二、基本输入输出
三、流程控制
四、函数
五、指针
六、字符串
七、结构体
八、联合体
九、Linux系统基本操作命令如mkdir,ls -l等。
十、Linux系统编程之进程的知识
如果以上知识不清楚,请自行学习后再来浏览。如果我有没例出的,请在评论区写一下。谢谢啦!
一、 共享内存概述
1.1 共享内存概述简图
如下图
二、 共享内存编程实战
2.1 共享内存介绍
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
2.1.1 共享内存的特点
-
共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
-
因为多个进程可以同时操作,所以需要进行同步。
-
信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
2.2 共享内存几个重要API介绍
2.2.1 shmget函数介绍
在Linux系统中,shmget
函数是用于创建或访问一个共享内存段(shared memory segment)的。这个函数是POSIX共享内存接口的一部分,但它实际上来源于System V IPC(进程间通信)机制。尽管POSIX共享内存(如shm_open
、shm_unlink
等)在现代Linux系统中更为常用,但了解shmget
的使用仍然有其价值。
函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明
- key:一个键(key),用于唯一标识共享内存段。多个进程可以通过相同的键来访问同一个共享内存段。通常,这个键是通过
ftok
函数生成的。 - size:共享内存段的大小(以字节为单位)。
- shmflg:权限标志和模式标志的组合。权限标志(如
0666
)指定了共享内存段的权限,类似于文件权限。模式标志可以是IPC_CREAT
(如果共享内存段不存在,则创建它)和IPC_EXCL
(与IPC_CREAT
一起使用时,如果共享内存段已存在,则调用失败)。
返回值
- 成功时,
shmget
返回一个非负整数,即共享内存段的标识符(shmid)。 - 失败时,返回-1,并设置errno以指示错误。
2.2.2 shmat函数介绍
在Linux系统中,shmat
函数用于将先前由shmget
创建的共享内存段附加(attach)到进程的地址空间中。一旦共享内存段被附加,进程就可以通过返回的指针来访问共享内存中的数据。
函数原型
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明
- shmid:
shmget
返回的共享内存标识符(shmid)。 - shmaddr:指定附加的地址。如果
shmaddr
是NULL,内核会选择一个合适的地址。否则,如果shmflg
中包含了SHM_RND
,shmaddr
会向下舍入到最接近的SHMLBA(共享内存低边界地址)的倍数。如果shmflg
中包含了SHM_REMAP
且指定的地址区域已经被其他映射所占用,那么该映射会被替换。 - shmflg:通常设置为0,但也可以包含
SHM_RDONLY
来以只读方式附加共享内存段。
返回值
- 成功时,
shmat
返回一个指向共享内存段的指针。 - 失败时,返回
(void *)-1
,并设置errno以指示错误。
2.2.3 shmdt函数介绍
在Linux系统中,shmdt
函数用于将先前通过shmat
函数附加(attach)到进程地址空间的共享内存段分离(detach)出去。这意味着进程将不再能够通过返回的指针来访问该共享内存段中的数据,但共享内存段本身并不会被删除,它仍然存在于系统中,直到被显式删除或系统重启。
函数原型
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数说明
- shmaddr:一个指针,指向当前进程地址空间中共享内存段的起始地址。这个地址是之前通过
shmat
函数调用获得的。
返回值
- 成功时,
shmdt
返回0。 - 失败时,返回-1,并设置相应的errno以指示错误。
使用注意事项
- 调用
shmdt
函数并不会删除共享内存段,只是解除了当前进程与共享内存段之间的关联。 - 如果在调用
shmdt
之后还有进程附加了该共享内存段,那么该共享内存段将继续存在。 - 当最后一个附加了该共享内存段的进程也调用了
shmdt
之后,如果没有进程再对该共享内存段进行shmctl
的IPC_RMID
操作来显式删除它,那么该共享内存段将在系统重启时被清理。
2.2.4 shmctl函数介绍
在Linux系统中,shmctl
函数是用于控制共享内存段(由shmget
创建的)的一个系统调用。它允许进程执行各种操作,如获取共享内存的状态、修改其权限或删除共享内存段。以下是shmctl
函数的基本使用方法:
函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明
- shmid:共享内存段的标识符(ID),由
shmget
函数返回。 - cmd:控制命令,指定要执行的操作。常用的命令包括
IPC_STAT
、IPC_SET
和IPC_RMID
(删除共享内存)。 - buf:指向
shmid_ds
结构体的指针,用于传递或接收与共享内存段相关的信息。根据cmd
参数的不同,buf
可能用于输入(如设置权限)或输出(如获取状态)。
结构体shmid_ds
shmid_ds
结构体包含了共享内存段的状态信息,如权限、关联的进程数、创建时间等。其定义可能因系统而异,但通常包含以下字段:
struct shmid_ds {
struct ipc_perm shm_perm; /* 权限结构 */
size_t shm_segsz; /* 段的大小 */
time_t shm_atime; /* 最后访问时间 */
time_t shm_dtime; /* 最后分离时间 */
time_t shm_ctime; /* 最后改变时间 */
pid_t shm_cpid; /* 创建进程ID */
pid_t shm_lpid; /* 最后操作进程ID */
shmatt_t shm_nattch; /* 当前附加的进程数 */
/* ... 可能还有其他字段 ... */
};
2.3 共享内存写入程序代码
#include <stdio.h>
使用共享内存的相关API必须包含以下三个头文件
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
// int shmget(key_t key, size_t size, int shmflg);
int main()
{
key_t key = 0;
int smid = 0;
int shmdtmark = 0;
int shmctlmark = 0;
char *smaddr = NULL;
key = ftok(".",10); 获取key值
putchar('\n');
if(key == -1)
{
puts("Get the key fail");
}
else
{
puts("Get the key successful");
}
smid = shmget(key,1024,IPC_CREAT|0666); 获取共享内存ID
if(smid == -1)
{
puts("Get the smid fail");
exit(-1);
}
else
{
puts("Get the smid successful");
}
smaddr = shmat(smid,0,0); 映射共享内存
if(smaddr == NULL)
{
puts("Map shared memory fail");
}
else
{
puts("Map shared memory successful");
}
strcpy(smaddr,"This is shared memory write data"); 向共享内存中写入数据
sleep(5); 睡眠5秒等待读程序读取共享内存内容
shmdtmark = shmdt(smaddr); 取消共享内存映射
if(shmdtmark == -1)
{
puts("Release mapped shared memory fail");
}
else
{
puts("Release mapped shared memory successful");
}
shmctlmark = shmctl(smid,IPC_RMID,0); 移除共享内存
if(shmctlmark == -1)
{
puts("Remove the shared memory fail");
}
else
{
puts("Remove the shared memory successful");
}
putchar('\n');
return 0;
}
2.4 共享内存读取程序代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
// int shmget(key_t key, size_t size, int shmflg);
int main()
{
key_t key = 0;
int smid = 0;
int shmdtmark = 0;
char *smaddr = NULL;
key = ftok(".",10);
putchar('\n');
if(key == -1)
{
puts("Get the key fail");
}
else
{
puts("Get the key successful");
}
smid = shmget(key,1024,IPC_CREAT|0666);
if(smid == -1)
{
puts("Get the smid fail");
exit(-1);
}
else
{
puts("Get the smid successful");
}
smaddr = shmat(smid,0,0); 映射共享内存
if(smaddr == NULL)
{
puts("Map shared memory fail");
}
else
{
puts("Map shared memory successful");
}
printf("Read data : %s\n",smaddr); 输出共享内存内容
shmdtmark = shmdt(smaddr);
if(shmdtmark == -1)
{
puts("Release mapped shared memory fail");
}
else
{
puts("Release mapped shared memory successful");
}
putchar('\n');
return 0;
}
注意:在共享内存写入程序中已经移除了共享内存,所以子读程序中不必删除共享内存
2.5 共享内存编程实战读写程序运行结果
如下图
三、 信号概述
3.1 信号概述模拟图
如下图
3.2 信号的名字和编号
每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。信号定义在signal.h
头文件中,信号名都定义为正整数。
具体的信号名称可以使用kill -l
来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。下图为使用kill -l
查看信号编号和名字。
3.3 信号的处理
信号的处理有三种方法,分别是:忽略、捕捉和默认动作
- 忽略信号,大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是
SIGKILL
和SIGSTOP
)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景 - 捕捉信号,需要告诉内核,用户希望如何处理某一种信号,说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
- 系统默认动作,对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。具体的信号默认动作可以使用man 7 signal来查看系统的具体定义。在此,我就不详细展开了,需要查看的,可以自行查看。
3.4 信号的使用
实际上,kill 命令是一个用于向进程发送信号的工具,其中 kill -9 PID
的用法是强制终止指定进程(PID)。例如,当你在后台启动了一个 top
工具,并通过 ps 命令获取到它的进程ID(PID)
后,使用 kill -9 PID
命令就是向该 top 进程发送了一个 SIGKILL
信号(信号编号为9
),以此强制结束
该进程。查看信号编号与名称的对应关系,我们可以确认9
号信号正是SIGKILL,其默认动作就是无条件地终止进程。因此,上述执行过程实质上就是触发了 SIGKILL 信号的默认行为——即杀死进程
。
对于信号而言,其核心价值并非在于简单地终止或“杀死”信号本身,而是在于作为一种实现异步通信的有效手段。
注:以上部分内容来自大佬:“故事狗”
点击这里查看原文
3.4.1 示例程序代码
#include <stdio.h>
int main()
{
while(1); 让程序一直运行
return 0
}
3.4.1 使用 kill -9 PID 杀死这个进程
如下图
四、信号编程
4.1 信号处理函数signal介绍
signal
函数用于为指定的信号设置一个信号处理函数(也称为信号处理器)。
4.1.1 signal函数参数介绍
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
sig
:指定要处理的信号的编号。sighandler_t
:指向信号处理函数的指针,该函数在接收到信号时被调用。
4.1.2 signal函数返回值
如果成功,signal 函数返回之前的信号处理函数
的指针;如果发生错误,则返回SIG_ERR
。
4.2 信号发送函数kill介绍
在Linux系统中,kill
函数是用于向进程发送信号的。这个函数定义在<signal.h>
(或<sys/signal.h>
,但在大多数现代系统中推荐使用<signal.h>
)头文件中,并允许程序向另一个进程发送指定的信号。如果接收信号的进程注册了针对该信号的处理函数(也称为信号处理器),那么该函数将被调用;如果没有注册处理函数,并且信号不是由程序自动忽略的,那么进程的默认行为(如终止进程)将被触发。
4.2.1 kill函数原型
#include <signal.h>
#include <sys/types.h>
int kill(pid_t pid, int sig);
pid
:要发送信号的进程的进程ID(PID)。如果pid
为0,则信号将被发送给与调用进程同组的所有进程,忽略系统进程和设置了用户ID的进程;如果pid
为-1,则信号将被发送给除系统进程、设置了用户ID的进程以及调用进程自身外的所有进程;如果pid
为< -1,则信号将被发送给组ID等于pid
的绝对值的所有进程。sig
:要发送的信号编号。在Linux中,信号的编号从1开始,其中SIGKILL
(9)用于立即终止进程,而SIGTERM
(15)则请求进程终止。
4.2.2 kill函数返回值
- 成功时,
kill
函数返回0。 - 失败时,返回-1,并设置
errno
以指示错误。
4.3 atoi函数介绍
在Linux系统中(实际上,在所有支持标准C库的系统中),atoi
函数用于将字符串转换成整数。这个函数定义在 <stdlib.h>
头文件中,它接受一个指向以 null 结尾的字符串的指针作为参数,该字符串表示一个整数,并返回该整数的值。
4.3.1 stoi函数原型
#include <stdlib.h>
int atoi(const char *nptr);
nptr
:指向要转换的字符串的指针。
4.3.2 atoi返回值
atoi
函数返回转换后的整数。如果字符串不包含任何数字或者第一个非空白字符不是有效数字,则返回0。如果字符串表示的整数超出了int
类型能够表示的范围,则会发生溢出,并且函数返回的是通过模2^n(n是int
类型的位数)后的结果,这通常不是原始数字。
4.3.3 atoi注意事项
atoi
函数不检查字符串中的错误,除了确定字符串是否包含有效的整数表示之外。因此,它不适合需要错误处理的复杂情况。- 字符串中的前导空白符(如空格、制表符等)会被忽略。
- 如果字符串以
+
或-
开头,则该函数会相应地返回正数或负数。 - 如果字符串中的数字超出了
int
的表示范围,则会发生溢出,但atoi
不会提供任何指示这一点的机制。
4.4 kill命令介绍
在Linux系统中,kill
命令用于向进程发送信号,默认情况下发送的是SIGTERM
(信号15),该信号请求程序终止运行并清理。但是,kill
命令也可以用来发送其他信号,以实现不同的功能,比如暂停(SIGSTOP
)、继续执行(SIGCONT
)、立即终止(SIGKILL
)等。
4.4.1 kill命令基本用法
kill [options] <pid> [...]
<pid>
:要发送信号的进程的进程ID(PID)。[options]
:可选参数,比如-s
来指定要发送的信号,或者使用信号的数字或名称。
4.4.2 kill命令常用选项
-s <signal>
或--signal <signal>
:指定要发送的信号。信号可以是数字(如9
代表SIGKILL
),也可以是名称(如KILL
)。-l
或--list
:列出所有可用的信号及其编号和名称。-p
:显示进程的PID,而不实际发送信号。
4.4.3 kill命令示例
-
发送
SIGTERM
信号给指定PID的进程:kill 1234
这里,
1234
是进程的PID,默认情况下发送的是SIGTERM
信号。 -
发送
SIGKILL
信号给指定PID的进程:kill -s SIGKILL 1234
或者,使用信号的编号:
kill -9 1234
SIGKILL
信号不能被捕获、忽略或阻塞,因此它会导致进程立即终止。 -
列出所有可用的信号:
kill -l
这将列出所有可用的信号及其编号和名称。
-
发送信号给多个进程:
你可以一次向多个进程发送信号,只需在命令中列出它们的PID即可:
kill 1234 5678
这将向PID为1234和5678的进程发送
SIGTERM
信号。 -
使用
-p
选项(注意:这实际上并不发送信号):kill -p 1234
这将仅显示PID为1234的进程的PID,并不发送任何信号。这个选项在脚本中可能很有用,用于检查进程是否存在。
4.4.4 kill命令注意事项
- 只有拥有相应权限的用户(通常是进程的拥有者或超级用户)才能向进程发送信号。
- 某些信号(如
SIGKILL
和SIGSTOP
)不能被捕获、忽略或阻塞,因此它们会导致进程无条件地执行特定操作。 - 在使用
kill
命令时,请确保目标PID是正确的,以避免意外终止错误的进程。 - 对于某些长时间运行或处于阻塞状态的进程,发送
SIGTERM
可能不足以使其终止。在这种情况下,可能需要发送SIGKILL
信号来强制终止进程。但是,请注意,强制终止进程可能会导致数据丢失或其他副作用。
4.5 信号处理程序
4.5.1 信号处理程序代码
#include <stdio.h>
#include <signal.h>
// typedef void (*sighandler_t)(int);
// sighandler_t signal(int signum, sighandler_t handler);
void signalHandler(int signum) 判断是什么信号,输出信号的编号和名称
{
switch(signum)
{
case 2:
printf("(%d) SIGINT\n",signum);
break;
case 9:
printf("(%d) SIGKILL\n",signum);
break;
case 10:
printf("(%d) SIGUSR1\n",signum);
break;
default:
printf("This signal is not included\n");
break;
}
printf("never quit\n");
}
int main()
{
signal(SIGINT,signalHandler); 检测信号SIGINT
signal(SIGKILL,signalHandler); 检测信号SIGKILL
signal(SIGUSR1,signalHandler); 检测信号SIGUSR1
signal(SIGSEGV,signalHandler); 检测信号SIGSEGV
while(1); 仿真程序退出
return 0;
}
4.5.2 信号处理程序配合kill命令运行结果
如下图
4.6 信号发送程序
4.6.1 信号发送程序代码
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
//int kill(pid_t pid, int sig);
int main(int argc,char **argv)
{
int signum = 0;
int pid = 0;
int killmark = 0;
signum = atoi(argv[1]); 将接收字符的signum转换为数字的signum
pid = atoi(argv[2]); 将接收字符的pid转换为数字的pid
killmark = kill(pid,signum); 发送信号
if(killmark == -1) 对信号发送是否成功进行判断
{
puts("send signal fail");
}
else
{
puts("send signal successful");
}
return 0;
}
4.6.2 信号发送程序配合信号处理程序运行结果
如下图
4.6.3 采用system函数发送信号程序代码
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>
//int kill(pid_t pid, int sig);
int main(int argc,char **argv)
{
int signum = 0;
int pid = 0;
int sysmark = 0;
char sigbuf[128];
signum = atoi(argv[1]);
pid = atoi(argv[2]);
sprintf(sigbuf,"kill -%d %d",signum,pid); 拼接kill命令
sysmark = system(sigbuf); 发送kill命令
if(sysmark == -1) 判断kill命令发送是否成功
{
puts("Send signal fail");
}
else
{
puts("Send signal successful");
}
return 0;
}
注意:使用system函数发送信号运行结果和kill函数发送信号程序一样。
五、信号如何携带消息简图
如下图
六、 信号携带消息编程实战
6.1 sigaction函数介绍
在Linux系统中,sigaction
函数是用于检查和更改与特定信号相关联的处理动作的一个强大的接口。它允许你精确控制信号处理的行为,包括信号处理函数、信号处理时的行为(比如是否阻塞其他信号)、以及信号处理前的动作(比如自动重置信号处理函数到默认行为)。
6.1.1 sigaction函数原型
#include <signal.h>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
- sig:要处理的信号编号。
- act:指向一个
sigaction
结构的指针,该结构包含了对信号的新处理方式和选项。如果此参数为NULL,则不会改变信号的处理方式,但可以用来查询当前的处理方式(此时oact
不能为NULL)。 - oact:如果非NULL,指向一个
sigaction
结构的指针,该结构在调用前会被填充为调用前的信号处理方式。这允许程序保存旧的信号处理函数,以便之后恢复。
6.1.2 sigaction
结构体
struct sigaction {
void (*sa_handler)(int); // 信号处理函数,或SIG_IGN或SIG_DFL
void (*sa_sigaction)(int, siginfo_t *, void *); // 替代信号处理函数,用于实时信号
sigset_t sa_mask; // 在处理此信号时阻塞的信号集
int sa_flags; // 标志位,用于修改信号的行为
void (*sa_restorer)(void); // 已废弃,不应使用
};
sa_handler
- 类型:
void (*)(int)
- 描述:这是一个指向函数的指针,该函数是当指定信号发生时被调用的信号处理函数。如果你希望使用默认的信号处理方式(即忽略信号或执行默认操作),可以将此字段设置为
SIG_IGN
或SIG_DFL
。如果设置为其他值(即自定义的信号处理函数的地址),则当信号发生时,会调用该函数。
sa_sigaction
- 类型:
void (*)(int, siginfo_t *, void *)
- 描述:这是一个替代的信号处理函数指针,用于实时信号(也称为排队信号)。与
sa_handler
不同,这个函数能够接收更多关于信号的信息,包括信号的附加数据和发送信号的进程ID。要使用这个函数,你需要将sa_flags
中的SA_SIGINFO
标志设置为1。如果未设置此标志,则忽略sa_sigaction
字段。
sa_mask
- 类型:
sigset_t
- 描述:这是一个信号集,指定了在执行信号处理函数期间应该被阻塞的信号。这有助于防止在信号处理函数执行期间发生信号嵌套(即在同一信号处理函数执行期间再次接收到相同或不同信号的情况)。通过阻塞某些信号,你可以确保信号处理函数的原子性。
sa_flags
- 类型:
int
- 描述:这是一个标志位字段,用于修改信号的行为。一些重要的标志包括:
SA_RESTART
:如果设置了此标志,并且信号处理函数返回后,某个被信号中断的系统调用将会自动重启。这通常用于I/O操作,以避免因信号中断而丢失数据。SA_NODEFER
(或SA_NOMASK
):如果设置了此标志,则在信号处理函数执行期间,不会自动将正在处理的信号添加到sa_mask
信号集中。这允许信号处理函数自己处理它正在处理的信号。但是,请注意,这可能导致信号嵌套。SA_RESETHAND
:当信号处理函数返回时,将信号处理函数重置为SIG_DFL
。这有助于避免在信号处理函数中不小心重新启用信号。SA_SIGINFO
:指示使用sa_sigaction
而不是sa_handler
作为信号处理函数。这允许接收关于信号的更多信息。
sa_restorer
- 类型:
void (*)(void)
- 描述:这是一个已废弃的字段,不应在新代码中使用。在早期的UNIX系统中,这个字段用于指定一个函数,该函数将恢复信号处理的默认行为,但现代系统不再支持这种用法。
6.1.3 sa_sigaction函数指针
在sigaction
结构体中,sa_sigaction
函数指针是一个特殊的信号处理函数,它用于处理那些支持“信号信息”(siginfo)的信号,这些信号通常被称为实时信号。与普通的信号处理函数(即sa_handler
指向的函数)不同,sa_sigaction
函数能够接收更多关于信号的信息,这些信息通过siginfo_t
结构体提供。
sa_sigaction
函数指针的形参包括三个参数:
-
int signum:这是接收到的信号编号。
-
*siginfo_t info:这是一个指向
siginfo_t
结构体的指针,该结构体包含了关于信号的详细信息。siginfo_t
结构体的具体字段可能因系统而异,但通常包括信号的来源(如发送信号的进程ID)、信号码(对于某些信号,这提供了额外的信息)以及信号发生的地址(对于某些与硬件相关的信号)。 -
*void ucontext:这是一个指向
ucontext_t
结构体的指针,该结构体包含了信号发生时进程的上下文信息。这允许信号处理函数在必要时恢复或检查进程的寄存器状态、栈信息等。然而,需要注意的是,并非所有系统都支持ucontext
参数,或者它可能以不同的形式出现(例如,在某些系统上,它可能是一个指向ucontext_t
的指针的指针)。
siginfo_t
结构体的原型可能因系统而异,但通常包含以下字段(这些字段可能不是所有系统都支持):
struct siginfo_t {
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since kernel 2.6.32) */
}
};
以下是对该结构体的内容介绍
-
si_signo:信号的编号。这是触发信号处理函数的信号的标识符。
-
si_errno:如果信号是由于某个系统调用失败而产生的,则此字段包含对应的
errno
值。否则,此字段的值未定义。 -
si_code:信号代码,提供了关于信号来源的额外信息。不同的信号可能有一组预定义的代码,用于指示信号的具体原因。
-
si_trapno:触发硬件生成信号的陷阱号。这主要用于与硬件相关的信号,如机器检查异常等,在大多数体系结构上未使用。
-
si_pid:发送信号的进程的 PID(进程标识符)。对于由其他进程发送的信号,此字段包含发送进程的 PID。如果信号是由内核自身生成的(例如,由于进程尝试访问无效的内存地址),则此字段的值可能为 0 或具有特殊含义。
-
si_uid:发送信号的进程的真实用户 ID。这有助于确定哪个用户导致了信号的产生。
-
si_status:对于由
SIGCHLD
信号报告的已停止或已终止的子进程,此字段包含子进程的退出状态或停止信号。 -
si_utime 和 si_stime:这些字段在 POSIX.1-2001 中已废弃,并且在新版本的 UNIX 和类 UNIX 系统中通常不被使用。它们原本用于记录处理信号时消耗的用户时间和系统时间。
-
si_value:信号值。这是一个
sigval_t
类型的联合,可以包含一个整数或指针,具体取决于信号的来源和上下文。 -
si_int 和 si_ptr:这些是
si_value
联合中的成员,分别用于整数和指针类型的数据。根据信号的具体情况和上下文,这两个字段之一将被使用。 -
si_overrun:对于 POSIX.1b 定时器信号,此字段包含定时器溢出的次数。定时器溢出发生在定时器到期时,但由于某种原因(如系统负载高)未能及时传递信号。
-
si_timerid:对于 POSIX.1b 定时器信号,此字段包含触发信号的定时器的标识符。
-
si_addr:导致信号的内存地址。对于某些类型的信号(如
SIGSEGV
,即段错误),此字段包含导致信号发生的内存地址。 -
si_band:与实时信号相关的带事件(band event)的编号。在较新的 glibc 版本中,此字段的类型已从
int
更改为long
。 -
si_fd:与信号相关的文件描述符。这主要用于与文件描述符相关的信号,如
SIGIO
(I/O 就绪信号)。 -
si_addr_lsb:触发信号的内存地址的最低有效位(LSB)。这有助于确定访问违规是对齐错误还是其他类型的错误。
6.2 sigqueue函数介绍
sigqueue
函数是 POSIX 实时信号扩展的一部分,它允许发送信号时附带额外的数据(通过 union sigval
类型)给接收信号的进程。这对于需要传递更多上下文信息的场景非常有用。然而,需要注意的是,并非所有的信号都支持通过 sigqueue
发送数据,通常只有那些被定义为实时信号的信号(如 SIGRTMIN 到 SIGRTMAX)才支持这一功能。
6.2.1 sigqueue函数原型
在 Linux 系统中,sigqueue
函数的原型定义在 <signal.h>
头文件中,如下所示:
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
pid
:目标进程的 PID。如果pid
为 0,则信号将被发送给与调用进程同组的所有进程,但发送者除外。如果pid
小于 -1,则信号将被发送给进程组 ID 等于-pid
的所有进程。sig
:要发送的信号编号,必须是实时信号(SIGRTMIN 到 SIGRTMAX)。value
:一个union sigval
类型的值,用于向接收者传递额外的数据。union sigval
可以是一个整数(sival_int
)或一个指针(sival_ptr
)。
6.2.2 sigqueue函数返回值
- 成功时,
sigqueue
返回 0。 - 出错时,返回 -1,并设置
errno
以指示错误原因。
6.3 使用sigaction函数处理信号程序代码
#include <stdio.h>
#include <signal.h>
// int sigaction(int signum, const struct sigaction *act struct sigaction *oldact);
void handler(int signum, siginfo_t *data, void *mark) 信号处理函数
{
printf("signum = %d\n",signum); 输出信号编号
if(mark != NULL) 判断是否接收到数据
{
puts("Data has been received");
printf("Sender's pid = %d\n",data->si_pid); 输出发送方的进程标识符
printf("Sender's send data = %d\n",data->si_int); 输出接收到的数据
printf("Sender's send value = %d\n",data->si_value.sival_int); 输出接收到的数据
}
else
{
puts("No data was received");
}
}
int main()
{
int sigac = 0; 定义用于判断sigaction函数是否成功绑定信号处理函数变量
struct sigaction act; 定义结构体sigaction变量act
act.sa_sigaction = handler; 让结构体sigaction中的函数指针变量sa_sigaction指向信号处理函数handler
act.sa_flags = SA_SIGINFO; 让结构体sigaction中的是否接收数据变量sa_flags使能接收
sigac = sigaction(SIGUSR1,&act,NULL); 使用sigaction函数绑定信号处理函数,并传递捕获的信号SIGUSR1
if(sigac == -1) 判断sigaction函数是否绑定成功
{
puts("Failed to bind the signal handler");
}
else
{
puts("Binding signal processing function successful");
}
printf("Oneself pid = %d\n",getpid());
while(1); 等待信号来临,防止程序退出
return 0;
}
6.4 使用sigqueue函数发送信号程序代码
#include <stdio.h>
#include <signal.h>
//int sigqueue(pid_t pid, int sig, const union sigval value);
int main(int argc,char **argv)
{
int sigqu = 0; 定义用于判断sigqueue函数是否成功发送信号变量
int signum = 0;
int pid = 0;
union sigval value; 定义发送数据变量
value.sival_int = 100; 发送数为100
signum = atoi(argv[1]); 将字符的数字转为整型的数据
pid = atoi(argv[2]);
sigqu = sigqueue(pid,signum,value); 使用sigqueue函数发送数据
if(sigqu == -1) 判读sigqueue函数是否发送数据成功
{
puts("Send to data fail");
}
else
{
puts("Send to data successful");
}
printf("Oneself pid = %d\n",getpid());
return 0;
}
6.5 sigaction函数处理信号程序配合sigqueue函数发送信号程序运行结果
如下图
七、 信号量概述
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
7.1 信号量概述简图
如下图
7.2 信号量的特点
- 信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
- 信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
- 每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
- 支持信号量组。
7.3 信号量的原型
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。
Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。
1 #include <sys/sem.h>
2 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
3 int semget(key_t key, int num_sems, int sem_flags);
4 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
5 int semop(int semid, struct sembuf semoparray[], size_t numops);
6 // 控制信号量的相关信息
7 int semctl(int semid, int sem_num, int cmd, ...);
当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。
八、 信号量编程实战(一)
8.1 semget函数介绍
在Linux系统中,semget
函数是用于创建或访问一组信号量的系统调用。信号量是一种用于进程间同步的机制,它允许一个或多个进程以某种顺序来访问共享资源或执行代码段。semget
函数定义在 <sys/sem.h>
头文件中,并需要链接实时库(通常是 -lrt
)。
8.1.2 semget函数原型
#include <sys/sem.h>
#include <sys/ipc.h>
int semget(key_t key, int nsems, int semflg);
8.1.3 semget函数参数说明
- key:一个键(key),用于唯一标识一组信号量。如果
key
为IPC_PRIVATE,则会创建一个新的、唯一的信号量集。 - nsems:信号量集中的信号量数量。
- semflg:标志位,用于控制信号量集的创建和访问权限。通常,这个参数是权限位(如0666)和IPC_CREAT(如果信号量集不存在则创建)或IPC_EXCL(与IPC_CREAT一起使用,如果信号量集已存在则调用失败)的按位或。
8.1.4 semget函数返回值
成功时,semget
返回信号量集的标识符(一个非负整数)。失败时,返回-1,并设置errno
以指示错误。
8.2 semctl函数介绍
semctl
函数是 POSIX 线程(也称为 pthreads)同步机制中用于操作信号量(semaphores)的一个系统调用。在 Linux 系统中,信号量通常用于进程间或线程间的同步。semctl
函数提供了对信号量集(semaphore set)的多种操作,包括初始化信号量集、获取信号量的值、设置信号量的值等。
8.2.1 semctl函数原型
#include <sys/sem.h>
#include <sys/ipc.h>
int semctl(int semid, int semnum, int cmd, ...);
8.2.2 semctl函数参数说明
- semid:信号量集的标识符,由
semget
函数返回。 - semnum:要操作的信号量在信号量集中的索引(从 0 开始)。
- cmd:指定要执行的操作,常用的有
GETVAL
(获取信号量的值)、SETVAL
(设置信号量的值,仅用于初始化时)、IPC_RMID
(删除信号量集)等。 - …:根据
cmd
的不同,可能需要额外的参数。例如,SETVAL
需要一个union semun
类型的参数来指定新的信号量值。
8.2.3 union semun联合体介绍
在union semun
联合体中,各个变量的具体含义取决于你打算如何使用semctl
函数以及你想要执行的操作。然而,由于union semun
本身不是POSIX标准的一部分,而是许多UNIX和Linux系统为了提供信号量操作而引入的一个非标准扩展,因此其确切的成员可能会根据不同的系统或库实现而有所不同。不过,以下是一个常见的union semun
定义及其成员含义的概述:
union semun {
int val; /* 用于SETVAL,设置信号量的值 */
struct semid_ds *buf; /* 用于IPC_STAT和IPC_SET,获取或设置信号量集的状态 */
unsigned short *array; /* 用于GETALL和SETALL,获取或设置信号量集中所有信号量的值 */
// 注意:在某些系统中,可能还包括一个用于IPC_INFO的特定成员,但这不是通用的
};
- 成员含义
-
val
:- 当使用
semctl
的SETVAL
命令时,val
成员被用来指定要设置的信号量的值。这是初始化信号量时常用的方法。 - 注意:
SETVAL
命令只能在信号量集刚被创建且尚未被任何进程打开时使用,或者用于将信号量重置为其初始值(在某些系统上可能不支持后者)。
- 当使用
-
buf
:- 当使用
semctl
的IPC_STAT
命令时,buf
成员应该指向一个struct semid_ds
类型的缓冲区,该命令将信号量集的状态复制到该缓冲区中。 - 当使用
IPC_SET
命令时,buf
成员也应该指向一个struct semid_ds
类型的缓冲区,但这次是将缓冲区中的新状态设置到信号量集中。然而,需要注意的是,并不是struct semid_ds
中的所有字段都可以被IPC_SET
命令修改。
- 当使用
-
array
:- 当使用
semctl
的GETALL
命令时,array
成员应该指向一个足够大的无符号短整型数组(unsigned short
),该命令将信号量集中所有信号量的当前值复制到该数组中。 - 当使用
SETALL
命令时,array
成员也应该指向一个无符号短整型数组,但这次是将数组中的新值设置到信号量集中的相应信号量上。
- 当使用
8.3 信号量创建并初始化程序代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
//int semget(key_t key, int nsems, int semflg);
//int semctl(int semid, int semnum, int cmd, ...);
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};
int main()
{
key_t key; 定义键值变量
int semid; 定义信号量ID变量
int semmark; 定义信号量判断初始化是否成功变量
union semun data; 定义信号量初识值变量
pid_t pid; 定义进程ID变量
data.val = 1; 初始化信号量初值为1
key = ftok(".",10); 获取键值
if(key == -1) 判断键值是否或取成功
{
puts("Get key fail");
}
else
{
puts("Get key successful");
} 获取一个信号量
semid = semget(key,1,IPC_CREAT|0666); 获取或创建信号量ID
if(semid == -1) 判断信号量ID是否获取成功
{
puts("Get semaphore is fail");
}
else
{
puts("Get semaphore is successful");
}
semmark = semctl(semid,0,SETVAL,data); 给第一个信号量赋值
if(semmark == -1) 判断是否成功给信号量赋值
{
puts("Init semaphore is fail");
}
else
{
puts("Init semaphore is successful");
}
pid = fork(); 创建进程
if(pid > 0) 判断父进程
{
puts("This is father process");
}
else if(pid == 0) 判断子进程
{
puts("This is child process");
}
else 判断创建进程失败
{
puts("fork error");
}
return 0;
}
8.4 信号量创建并初始化程序运行结果
如下图
九、信号量编程实战(二)
9.1 semop函数介绍
在Linux系统中,semop
函数是用于对一组信号量执行一系列操作的系统调用。这些操作可以是增加(释放)信号量的值、减少(等待)信号量的值或者同时进行多种操作。semop
是信号量操作的基础函数,它允许进程以原子方式修改多个信号量的值,从而避免竞态条件。
9.1.1 semop函数原型
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
- semid:信号量集的标识符,由
semget
函数返回。 - sops:指向
sembuf
结构数组的指针,该数组描述了要执行的操作序列。 - nsops:
sops
数组中的元素数量,即要执行的操作数量。
9.1.2 sembuf结构
sembuf
结构用于描述单个信号量操作,其定义如下:
struct sembuf {
unsigned short sem_num; /* 信号量在信号量集中的索引 */
short sem_op; /* 要执行的操作:负数表示等待(减少),正数表示释放(增加),0表示测试 */
short sem_flg; /* 操作标志,通常设置为0或IPC_NOWAIT */
};
- sem_num:指定了信号量集中哪个信号量将被操作。
- sem_op:指定了要对信号量执行的操作。正数表示增加信号量的值(释放资源),负数表示减少信号量的值(等待资源),0通常用于测试信号量的值。
- sem_flg:操作标志。通常,这个字段被设置为0,但也可以设置为
IPC_NOWAIT
,表示如果操作不能立即执行(例如,在sem_op
为负数时信号量的值不足以满足操作),则semop
立即返回一个错误,而不是使调用进程睡眠。
9.2 P操作和V操作编程实战程序代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <unistd.h>
//int semget(key_t key, int nsems, int semflg);
//int semctl(int semid, int semnum, int cmd, ...);
//int semop(int semid, struct sembuf *sops, unsigned nsops);
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};
void pGetKey(int id) P操作函数:获得钥匙
{
struct sembuf set; 定义描述单个信号量操作变量set
int semopback; 定义信号量操作函数返回值
set.sem_num = 0; 定义信号量的编号
set.sem_op = -1; 负数表示等待(减少),目的是拿到钥匙
set.sem_flg = SEM_UNDO; 设置SEM_UNDO表示没有获取到钥匙阻塞
semopback = semop(id,&set,1); 调用信号操作函数
if(semopback == -1) 判断是否拿到了钥匙
{
puts("Get lock fail");
}
else
{
puts("Get lock successful");
}
}
void vPutBackKey(int id)
{
struct sembuf set; 定义描述单个信号量操作变量set
int semopback; 定义信号量操作函数返回值
set.sem_num = 0; 定义信号量的编号
set.sem_op = 1; 正数表示释放(增加),目的是归还钥匙
set.sem_flg = SEM_UNDO; 设置SEM_UNDO表示没有归还钥匙阻塞
semopback = semop(id,&set,1); 调用信号操作函数
if(semopback == -1) 判断是否归还钥匙
{
puts("Put back lock fail");
}
else
{
puts("Put back lock successful");
}
}
int main()
{
key_t key;
int semid;
int semmark;
union semun data;
pid_t pid;
data.val = 0;
key = ftok(".",10);
if(key == -1)
{
puts("Get key fail");
}
else
{
puts("Get key successful");
}
semid = semget(key,1,IPC_CREAT|0666);
if(semid == -1)
{
puts("Get semaphore is fail");
}
else
{
puts("Get semaphore is successful");
}
semmark = semctl(semid,0,SETVAL,data);
if(semmark == -1)
{
puts("Init semaphore is fail");
}
else
{
puts("Init semaphore is successful");
}
pid = fork();
if(pid > 0)
{
pGetKey(semid); 在父进程拿钥匙
puts("This is father process");
vPutBackKey(semid); 用完钥匙归还
semctl(semid,0,IPC_RMID); 删除信号量
}
else if(pid == 0)
{
puts("This is child process");
vPutBackKey(semid); 在子进程归还钥匙,让子进程先执行
}
else
{
puts("fork error");
}
return 0;
}
9.3 P操作和V操作编程实战程序运行结果
如下图
十、 信号量结合共享内存编程
10.1 信号量结合共享内存编程写程序代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sem.h>
#include <unistd.h>
// int shmget(key_t key, size_t size, int shmflg);
key_t key = 0; 定义键值群聚变量
int semid; 定义信号量ID全局变量
union semun { 定义信号量初始化联合体
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO (Linux-specific) */
};
void initSemaphore() 构造信号量初始化函数
{
int semmark;
union semun data;
data.val = 0;
semid = semget(key,1,IPC_CREAT|0666);
if(semid == -1)
{
puts("Get semaphore is fail");
}
else
{
puts("Get semaphore is successful");
printf("semid = %d\n",semid);
}
semmark = semctl(semid,0,SETVAL,data); 只在写程序中初始化信号量的值
if(semmark == -1)
{
puts("Init semaphore is fail");
}
else
{
puts("Init semaphore is successful");
}
}
void vPutBackKey(int id) 构造归还钥匙函数
{
struct sembuf set;
int semopback;
set.sem_num = 0;
set.sem_op = 1;
set.sem_flg = SEM_UNDO;
semopback = semop(id,&set,1);
if(semopback == -1)
{
puts("Put back lock fail");
}
else
{
puts("Put back lock successful");
}
}
int main()
{
int smid = 0;
int shmdtmark = 0;
int shmctlmark = 0;
char *smaddr = NULL;
key = ftok(".",10);
putchar('\n');
if(key == -1)
{
puts("Get the key fail");
}
else
{
puts("Get the key successful");
printf("key = %d\n",key);
}
initSemaphore();
smid = shmget(key,1024,IPC_CREAT|0666);
if(smid == -1)
{
puts("Get the smid fail");
exit(-1);
}
else
{
puts("Get the smid successful");
}
smaddr = shmat(smid,0,0);
if(smaddr == NULL)
{
puts("Map shared memory fail");
}
else
{
puts("Map shared memory successful");
}
strcpy(smaddr,"This is shared memory write data");
vPutBackKey(semid); 归还钥匙,以便让读程序运行
sleep(1); 等待1秒,方便子程序读取数据
semctl(semid,0,IPC_RMID); 删除信号量
shmdtmark = shmdt(smaddr);
if(shmdtmark == -1)
{
puts("Release mapped shared memory fail");
}
else
{
puts("Release mapped shared memory successful");
}
shmctlmark = shmctl(smid,IPC_RMID,0);
if(shmctlmark == -1)
{
puts("Remove the shared memory fail");
}
else
{
puts("Remove the shared memory successful");
}
putchar('\n');
return 0;
}
10.2 信号量结合共享内存编程读程序代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sem.h>
#include <unistd.h>
// int shmget(key_t key, size_t size, int shmflg);
key_t key = 0; 定义键值群聚变量
int semid; 定义信号量ID全局变量
void initSemaphore() 构造信号量初始化函数
{
int semmark;
semid = semget(key,1,IPC_CREAT|0666);
if(semid == -1)
{
puts("Get semaphore is fail");
}
else
{
puts("Get semaphore is successful");
printf("semid = %d\n",semid);
}
}
void pGetKey(int id) 构造拿钥匙函数
{
struct sembuf set;
int semopback;
set.sem_num = 0;
set.sem_op = -1;
set.sem_flg = SEM_UNDO;
semopback = semop(id,&set,1);
if(semopback == -1)
{
puts("Get lock fail");
}
else
{
puts("Get lock successful");
}
}
void vPutBackKey(int id) 构造归还钥匙函数
{
struct sembuf set;
int semopback;
set.sem_num = 0;
set.sem_op = 1;
set.sem_flg = SEM_UNDO;
semopback = semop(id,&set,1);
if(semopback == -1)
{
puts("Put back lock fail");
}
else
{
puts("Put back lock successful");
}
}
int main()
{
int smid = 0;
int shmdtmark = 0;
char *smaddr = NULL;
key = ftok(".",10);
putchar('\n');
if(key == -1)
{
puts("Get the key fail");
}
else
{
puts("Get the key successful");
printf("key = %d\n",key);
}
initSemaphore();
smid = shmget(key,1024,IPC_CREAT|0666);
if(smid == -1)
{
puts("Get the smid fail");
exit(-1);
}
else
{
puts("Get the smid successful");
}
smaddr = shmat(smid,0,0);
if(smaddr == NULL)
{
puts("Map shared memory fail");
}
else
{
puts("Map shared memory successful");
}
pGetKey(semid); 拿钥匙
printf("Read data : %s\n",smaddr);
vPutBackKey(semid); 归还钥匙
shmdtmark = shmdt(smaddr);
if(shmdtmark == -1)
{
puts("Release mapped shared memory fail");
}
else
{
puts("Release mapped shared memory successful");
}
putchar('\n');
return 0;
}
10.3 信号量结合共享内存编程写程序配合信号量结合共享内存编程读程序运行结果
如下图
结束语
非常感谢您的耐心阅读!在您即将离开之际,如果您觉得内容有所收获或启发,不妨动动手指,点个赞再走,这将是对我莫大的鼓励和支持。衷心感谢您的点赞与关注,期待未来能继续为您带来更多有价值的内容!谢谢您!