进程之间通讯
信号
信号是一种通过软件模拟硬件中断的实现方式,中断的优先级是最高的。
信号源
可以通过 kill -l
命令查看所有信号源。
ctrl + c
:SIGINT
=> 2ctrl + \
:SIGQUIT
=> 3abort
:SIGABRT
=> 6- 杀死信号 :
SIGKILL
=> 9 - 管道异常 :
SIGPIPE
=> 13 - 闹钟信号 :
SIGALRM
=> 14 - …
发送信号
使用 kill
函数发送信号:
int kill(pid_t pid, int sig);
- 第一个参数
pid
:表示进程号 - 第二个参数
sig
:表示信号源 - 返回值:
- 成功:0
- 失败:-1
捕捉信号
使用 signal
函数捕捉信号:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
- **第一个参数
signum
:**表示信号源 - **第二个参数
handler
:**捕捉到信号执行的操作SIG_IGN
:表示忽略(屏蔽信号)SIG_DFL
:表示默认(终止)sighandler_t
:表示自定义处理函数
- 返回值:
- 成功:返回上次执行函数地址
- 失败:
SIG_ERR
void handle_sigint(int sig) {
printf("Caught signal %d\n", sig);
}
int main() {
signal(SIGINT, handle_sigint);
while (1) {
printf("Hello, World!\n");
sleep(1);
}
return 0;
}
//程序将不断打印 “Hello, World!”,当用户按下 ctrl + c 发送 SIGINT 信号时,程序将捕捉到该信号并执行 handle_sigint 函数,打印出 “Caught signal 2”。
pause函数
int pause(void);
pause
函数使调用进程挂起,直到接收到一个信号。
- 功能:使调用进程挂起,直到捕获到一个信号。
- 返回值:只有当捕获到一个信号时,
pause
才会返回。返回值始终为-1
,并且errno
设置为EINTR
,表示操作被信号中断。
闹钟信号与定时器
闹钟信号
使用 alarm
函数可以设置一个闹钟,在指定的秒数后发送 SIGALRM
信号(编号 14)给调用进程。
unsigned int alarm(unsigned int seconds);
- 参数:
seconds
表示秒数 - 返回值:返回之前设置的闹钟剩余的秒数,如果没有设置闹钟则返回 0
alarm 函数例子
#include <stdio.h>
#include <unistd.h>
int main() {
unsigned int remaining;
// 设置一个5秒的闹钟
remaining = alarm(5);
printf("Previous alarm set for %u seconds.\n", remaining);
// 等待闹钟信号
pause();
// 闹钟信号到达后,程序继续执行
printf("Alarm expired.\n");
return 0;
}
//程序将设置一个5秒的闹钟,然后调用 pause 函数等待闹钟信号。当闹钟信号到达时,程序将打印消息并退出。
定时器
更精确的时间控制可以通过 getitimer
和 setitimer
函数来实现。
int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value,
struct itimerval *ovalue);
void handle_signal(int sig) {
if (sig == SIGVTALRM) {
printf("定时器信号 SIGVTALRM 被捕获\n");
}
}
int main() {
struct itimerval new_value;
// 设置信号处理函数
signal(SIGVTALRM, handle_signal);
// 设置间隔定时器
new_value.it_value.tv_sec = 2; // 间隔时间秒数
new_value.it_value.tv_usec = 0; // 间隔时间微秒数
new_value.it_interval.tv_sec = 2; // 重复间隔秒数
new_value.it_interval.tv_usec = 0; // 重复间隔微秒数
// 设置定时器
if (setitimer(ITIMER_VIRTUAL, &new_value, NULL) == -1) {
perror("setitimer");
return 1;
}
// 模拟一些工作
for (int i = 0; i < 5; ++i) {
sleep(1); // 每秒打印一次消息
printf("正在工作...\n");
}
return 0;
}
- **第一个参数
which
:**表示定时器类型ITIMER_REAL
:基于实际(墙上)时间的定时器,发送SIGALRM
信号ITIMER_VIRTUAL
:基于用户态时间的定时器,发送SIGVTALRM
信号ITIMER_PROF
:基于用户态和内核态时间的定时器,发送SIGPROF
信号
- **第二个参数
value
:**表示时间结构体struct itimerval
- 当使用
shmctl
时,根据cmd
的不同,buf
参数的用法也不同。以下是两种常见情况:如果不为 NULL,则返回上次设置的定时器值 - **返回值:**成功返回 0,失败返回 -1
定时器结构体
struct itimerval {
struct timeval it_interval; // 循环执行时间
struct timeval it_value; // 第一次执行时间
};
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒(1秒 = 1000000微秒)
};
注意事项:设定闹钟类型的不同处理方式
在使用 setitimer
设置不同类型的定时器时,需要注意它们各自的处理方式:
闹钟类型处理方式
ITIMER_REAL(机器时间类型)
- 处理方式:可以采用睡眠方式等待,也可以采用轮询方式。
- 说明:
ITIMER_REAL
定时器是基于实际(墙上)时间的,因此当进程睡眠时,定时器仍然在计数。当定时器到期时,会发送SIGALRM
信号来唤醒进程。
ITIMER_VIRTUAL(用户态时间类型)
- 处理方式:必须是轮询的方式等待。
- 说明:
ITIMER_VIRTUAL
定时器是基于进程在用户态下所消耗的时间。由于进程在睡眠时不会消耗用户态时间,因此定时器不会在进程睡眠时计数。进程必须保持运行状态(例如,通过轮询或其他非阻塞操作)以使定时器能够正常工作。
ITIMER_PROF(用户态加系统态时间类型)
- 处理方式:必须是轮询的方式等待。
- 说明:
ITIMER_PROF
定时器是基于进程在用户态和系统态下所消耗的总时间。与ITIMER_VIRTUAL
类似,进程在睡眠时不会消耗这两种时间,因此定时器不会在进程睡眠时计数。进程同样需要保持运行状态,以便定时器能够按预期工作。
示例代码说明
以下是一个简单的示例,演示如何使用轮询方式等待 ITIMER_VIRTUAL
定时器:
#include <stdio.h>
#include <sys/time.h>
#include <signal.h>
#include <unistd.h>
void handle_sigvtalrm(int sig) {
printf("Virtual timer expired.\n");
}
int main() {
struct itimerval timer;
// 设置信号处理函数
signal(SIGVTALRM, handle_sigvtalrm);
// 设置定时器
timer.it_value.tv_sec = 2; // 首次触发时间:2秒
timer.it_value.tv_usec = 0; // 微秒
timer.it_interval.tv_sec = 1; // 间隔时间:1秒
timer.it_interval.tv_usec = 0; // 微秒
// 设置 ITIMER_VIRTUAL 定时器
setitimer(ITIMER_VIRTUAL, &timer, NULL);
// 主循环,轮询等待信号
while (1) {
// 执行一些非阻塞操作...
// 由于定时器是基于用户态时间,因此进程不能睡眠
}
return 0;
}
管道通讯
管道通讯是一种在进程间传输数据的方式,分为匿名管道和有名管道两种。
匿名管道
匿名管道是一种没有名字的管道,只能用于父子进程或兄弟进程之间的通讯。
特点
- 半双工模式:管道一端只能用于写入数据,另一端用于读取数据。
- 异步通知:写入数据后,读取端可以立即或稍后读取数据。
分类
- 单工:管道只能发送或接收数据。
- 半双工:管道一端发送数据,另一端接收数据。
- 全双工:管道两端可以同时发送和接收数据。
创建匿名管道
int pipe(int filedes[2]);
int main()
{
int pipefd[2];
char buf[100];
pid_t pid;
// 创建匿名管道
if (pipe(pipefd) == -1)
{
perror("pipe");
exit(EXIT_FAILURE);
}
// 创建子进程
pid = fork();
if (pid == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0)
{ // 子进程
// 关闭父进程的读端
close(pipefd[0]);
// 写入数据到管道
write(pipefd[1], "Hello from child process!", 25);
// 关闭子进程的写端
close(pipefd[1]);
// 等待子进程结束
exit(EXIT_SUCCESS);
}
else
{ // 父进程
// 关闭父进程的写端
close(pipefd[1]);
// 从管道读取数据
read(pipefd[0], buf, sizeof(buf));
printf("Parent received: %s\n", buf);
// 关闭父进程的读端
close(pipefd[0]);
// 等待子进程结束
wait(NULL);
// 清理资源
exit(EXIT_SUCCESS);
}
}
- 参数
filedes
:一个有两个元素的数组,filedes[0]
用于读取,filedes[1]
用于写入。 - 返回值:成功返回 0,失败返回 -1。
发送和接收数据
- 发送数据:使用
write
函数。 - 接收数据:使用
read
函数。
关闭管道
- 使用
close
函数关闭文件描述符。
匿名管道大小
匿名管道的大小通常是 64K。
管道异常
如果写入端检测到读取端已经关闭,将引发管道异常,发送 SIGPIPE
信号。
管道读写规则
- 当没有数据可读时
- O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
- O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
- 当管道满的时候
- O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
- O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
- 如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
- 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
有名管道
有名管道通过管道文件进行通讯,可以实现任意进程之间的通讯。
创建有名管道文件
int mkfifo(const char *pathname, mode_t mode);
int main() {
const char *pathname = "./my_fifo";
mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
// 创建有名管道
if (mkfifo(pathname, mode) == -1) {
perror("mkfifo");
exit(EXIT_FAILURE);
}
// 继续执行其他操作...
//删除有名管道
unlink("./my_fifo");
return 0;
}
- **参数
pathname
:**管道文件的路径。 - **参数
mode
:**文件权限模式。 - **返回值:**成功返回 0,失败返回 -1。
删除有名管道文件
int unlink(const char *pathname);
创建临时文件
mktemp
函数用于创建一个唯一的临时文件或目录路径名,它使用一个模板字符串来生成这个路径名。这个模板字符串应该以 X
开头,后面跟着一个或多个 XXXXXX
,其中 X
将被替换为唯一的字符序列。
char *mktemp(char *template);
int main() {
char template[] = "temp.XXXXXX";
char *pathname;
// 创建一个唯一的临时文件路径名
pathname = mktemp(template);
if (pathname == NULL) {
perror("mktemp");
exit(EXIT_FAILURE);
}
// 打开临时文件
FILE *file = fopen(pathname, "w");
if (file == NULL) {
perror("fopen");
exit(EXIT_FAILURE);
}
// 写入数据到临时文件
fprintf(file, "This is a test file.\n");
fclose(file);
// 打印临时文件路径名
printf("Created temporary file: %s\n", pathname);
// 清理资源,如果需要,可以删除临时文件
// unlink(pathname);
return 0;
}
template
:指向包含模板字符串的指针。- 返回值
- 成功:返回指向临时文件路径名的指针。
- 失败:返回 NULL,并设置
errno
。
打开文件
使用 open
函数。
操作数据
- 发送数据:使用
write
函数。 - 接收数据:使用
read
函数。
检测文件是否存在
int access(const char *pathname, int mode);
if (access(argv[1], F_OK) == -1)
{
ret = mkfifo(argv[1], 0666);
ERRP(ret == -1, mkfifo, goto ERR1);
}
- 参数
mode
:可以是R_OK
、W_OK
、X_OK
或F_OK
。
消息队列
0. 获取创建消息队列的key
为了确保key的唯一性,使用以下函数:
key_t ftok(const char *pathname, int proj_id);
int main() {
key_t key;
const char *pathname = "/tmp/myfile"; // 需要一个存在的文件路径
int proj_id = 65; // 任意选取一个项目ID,通常是一个ASCII字符
// 使用ftok函数生成key
key = ftok(pathname, proj_id);
if (key == -1) {
perror("ftok");
return 1;
}
printf("The key is: %d\n", key);
return 0;
}
- **第一个参数:**表示文件名(路径) => inode
- **第二个参数:**表示用户输入序列号
- **返回值:**就是关键字
一般情况下,计算公式为:
key = (proj_id << 24) | (0x02 << 16) | (inode & 0xFFFF)
例如:
inode : 0x11223344;
proj_id : 0x778899
key : 99023344
1. 创建消息队列
int msgget(key_t key, int msgflg);
int main() {
key_t key;
int msgid;
// 使用 ftok 生成唯一的 key
key = ftok("/tmp/myfile", 65); // 假设 "/tmp/myfile" 存在且可访问
if (key == -1) {
perror("ftok");
exit(1);
}
// 创建消息队列
msgid = msgget(key, IPC_CREAT | 0666); // 0666 是权限位,表示所有用户可读写
if (msgid == -1) {
perror("msgget");
exit(1);
}
printf("消息队列创建成功,标识符为: %d\n", msgid);
// 接下来可以使用 msgid 进行消息队列的操作,如 msgsnd, msgrcv 等
// 销毁消息队列
// msgctl(msgid, IPC_RMID, NULL);
return 0;
}
- **第一个参数:**表示关键字
- **第二个参数:**表示创建消息队列标志位
IPC_CREAT
: 表示创建IPC_EXCL
: 表示判断消息队列是否存在(必须与IPC_CREAT
一起使用才有意义)
- 返回值:
- 成功:返回消息队列id
- 失败:-1
注意:识别消息队列的唯一标识就是消息队列id,消息队列id是由系统随机指定。
2. 发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 定义消息结构体
struct msg_buffer {
long msg_type; // 消息类型
char msg_text[100]; // 消息内容
};
int main() {
key_t key;
int msgid;
struct msg_buffer message;
// 使用 ftok 生成唯一的 key
key = ftok("/tmp/myfile", 65); // 确保 "/tmp/myfile" 存在且可访问
if (key == -1) {
perror("ftok");
exit(1);
}
// 创建消息队列
msgid = msgget(key, 0666 | IPC_CREAT);
if (msgid == -1) {
perror("msgget");
exit(1);
}
// 设置消息类型和内容
message.msg_type = 1; // 消息类型,通常为正整数
sprintf(message.msg_text, "Hello, message queue!");
// 发送消息到消息队列
if (msgsnd(msgid, &message, sizeof(message.msg_text), 0) == -1) {
perror("msgsnd");
exit(1);
}
printf("消息发送成功\n");
// 销毁消息队列
// msgctl(msgid, IPC_RMID, NULL);
return 0;
}
- **第一个参数:**表示消息队列描述符(
msgget
返回值) - **第二个参数:**表示数据
- **第三个参数:**表示数据大小
- **第四个参数:**表示标志位(通常为0)
IPC_NOWAIT
: 表示取消阻塞
- 返回值:
- 成功:0
- 失败:-1
3. 接受数据
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
struct msg_buffer {
long msg_type; // 消息类型
char msg_text[100]; // 消息内容
};
int main() {
key_t key;
int msgid;
struct msg_buffer message;
// 使用 ftok 生成唯一的 key
key = ftok("/tmp/myfile", 65); // 确保 "/tmp/myfile" 存在且可访问
if (key == -1) {
perror("ftok");
exit(1);
}
// 获取消息队列标识符
msgid = msgget(key, 0); // 不创建新队列,只获取已存在的队列
if (msgid == -1) {
perror("msgget");
exit(1);
}
// 接收消息
// msgtyp 设置为 1 表示我们只想接收类型为 1 的消息
// msgflg 设置为 0 表示默认行为,即阻塞直到接收到消息
if (msgrcv(msgid, &message, sizeof(message.msg_text), 1, 0) == -1) {
perror("msgrcv");
exit(1);
}
printf("接收到的消息: %s\n", message.msg_text);
// 销毁消息队列
// msgctl(msgid, IPC_RMID, NULL);
return 0;
}
- **第一个参数:**表示消息队列描述符
- **第二个参数:**表示存储数据空间地址
- **第三个参数:**表示空间大小
- **第四个参数:**表示数据类型
- 不考虑数据类型时为0
- 用于区分不同的数据类型
- **第五个参数:**表示标志位(通常为0)
- 返回值:
- 失败:-1
- 成功:表示接受数据个数
4. 消息队列控制
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
int main() {
key_t key;
int msgid;
struct msqid_ds msg_info;
// 使用 ftok 生成唯一的 key
key = ftok("/tmp/myfile", 65); // 确保 "/tmp/myfile" 存在且可访问
if (key == -1) {
perror("ftok");
exit(1);
}
// 获取消息队列标识符
msgid = msgget(key, 0); // 不创建新队列,只获取已存在的队列
if (msgid == -1) {
perror("msgget");
exit(1);
}
// 获取消息队列的属性
if (msgctl(msgid, IPC_STAT, &msg_info) == -1) {
perror("msgctl IPC_STAT");
exit(1);
}
// 打印消息队列的一些属性
printf("消息队列的当前消息数量: %lu\n", msg_info.msg_qnum);
printf("消息队列的最大消息数量: %lu\n", msg_info.msg_qbytes);
// 修改消息队列的权限
msg_info.msg_perm.mode = 0666; // 设置权限为所有用户可读写
if (msgctl(msgid, IPC_SET, &msg_info) == -1) {
perror("msgctl IPC_SET");
exit(1);
}
// 删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl IPC_RMID");
exit(1);
}
printf("消息队列已删除\n");
return 0;
}
struct msqid_ds {
struct ipc_perm msg_perm; // 消息队列的权限和拥有者信息
msgqnum_t msg_qnum; // 消息队列中的消息数量
msglen_t msg_qbytes; // 消息队列中的最大字节数
pid_t msg_lspid; // 最后发送消息的进程 ID
pid_t msg_lrpid; // 最后接收消息的进程 ID
time_t msg_stime; // 最后发送消息的时间
time_t msg_rtime; // 最后接收消息的时间
time_t msg_ctime; // 消息队列最后被修改的时间
};
- **第一个参数:**表示消息队列描述符
- **第二个参数:**表示给消息队列下达指令
IPC_STAT
: 表示获取消息队列属性IPC_SET
: 表示修改消息队列属性IPC_RMID
: 表示销毁消息队列
- **第三个参数:**对命令补充
- 返回值:
- 成功: 0
- 失败:-1
共享内存
1. 获取关键
ftok
2. 创建共享内存
int shmget(key_t key, size_t size, int shmflg);
int main() {
key_t key;
int shmid;
// 使用 ftok 生成一个唯一的 key
key = ftok("shmfile", 65); // "shmfile" 是一个存在的文件,65 是一个项目标识符
// 检查 ftok 是否成功
if (key == -1) {
perror("ftok");
return 1;
}
// 创建共享内存段
shmid = shmget(key, 1024, 0644 | IPC_CREAT);
// 检查 shmget 是否成功
if (shmid == -1) {
perror("shmget");
return 1;
}
printf("共享内存创建成功,标识符为: %d\n", shmid);
// 在这里可以继续使用 shmid 来映射共享内存、操作数据等
// 注意:在实际应用中,创建共享内存后应该进行映射、使用和销毁等操作,
// 但在这个例子中,我们只是创建了共享内存并打印了其标识符。
// 示例结束,不进行映射、数据操作和销毁操作
return 0;
}
- **第一个参数:**表示关键字
- **第二个参数:**表示创建共享内存大小
- **第三个参数:**表示标志位
IPC_CREAT
IPC_EXCL
: 判断共享内存是否存在,如果存在,则报错;如果不存在,则创建
- 返回值:
- 成功:返回共享内存id
- 失败:-1
3. 映射
- 映射共享内存到进程中 (因为创建好之后还在内核态)
void *shmat(int shmid, const void *shmaddr, int shmflg);
int main() {
key_t key;
int shmid;
void *shared_memory;
// 使用 ftok 生成一个唯一的 key
key = ftok("shmfile", 65); // "shmfile" 是一个存在的文件,65 是一个项目标识符
// 检查 ftok 是否成功
if (key == -1) {
perror("ftok");
return 1;
}
// 创建共享内存段
shmid = shmget(key, 1024, 0644 | IPC_CREAT);
// 检查 shmget 是否成功
if (shmid == -1) {
perror("shmget");
return 1;
}
// 将共享内存映射到进程的地址空间
shared_memory = shmat(shmid, (void *)0, 0);
//shared_memory = shmat(shmid, NULL, 0); (void *)0 == NULL
// 检查 shmat 是否成功
if (shared_memory == (void *)-1) {
perror("shmat");
return 1;
}
printf("共享内存映射成功,映射地址为: %p\n", shared_memory);
// 在这里可以继续使用 shared_memory 指针来操作共享内存中的数据
// 示例结束,不进行数据操作和解除映射操作
// 注意:在实际应用中,使用完共享内存后应该进行解除映射和销毁操作,
// 但在这个例子中,我们只是映射了共享内存并打印了其地址。
return 0;
}
- **第一个参数:**表示描述符
shmget
返回值 - **第二个参数:**表示映射空间地址
NULL
: 表示由系统指定空间地址
- **第三个参数:**表示标志位 0
- 返回值:
- 成功:系统返回指定用户操作空间地址
- 失败:NULL
4. 操作数据
strcpy
memcpy
memmove
5. 解映射
int shmdt(const void *shmaddr);
int main() {
key_t key;
int shmid;
void *shared_memory;
// 使用 ftok 生成一个唯一的 key
key = ftok("shmfile", 65); // "shmfile" 是一个存在的文件,65 是一个项目标识符
// 检查 ftok 是否成功
if (key == -1) {
perror("ftok");
return 1;
}
// 创建共享内存段
shmid = shmget(key, 1024, 0644 | IPC_CREAT);
// 检查 shmget 是否成功
if (shmid == -1) {
perror("shmget");
return 1;
}
// 将共享内存映射到进程的地址空间
shared_memory = shmat(shmid, (void *)0, 0);
// 检查 shmat 是否成功
if (shared_memory == (void *)-1) {
perror("shmat");
return 1;
}
printf("共享内存映射成功,映射地址为: %p\n", shared_memory);
// ... 在这里进行共享内存的数据操作 ...
// 解除共享内存映射
if (shmdt(shared_memory) == -1) {
perror("shmdt");
return 1;
}
printf("共享内存解除映射成功。\n");
// 注意:在实际应用中,你可能还需要销毁共享内存段,
// 但在这个例子中,我们只是展示了如何解除映射。
return 0;
}
- 参数: 表示映射到进程中空间地址
- 返回值:
- 失败:-1
- 成功:0
6. 销毁共享内存
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
int main() {
key_t key;
int shmid;
void *shared_memory;
// 使用 ftok 生成一个唯一的 key
key = ftok("shmfile", 65); // "shmfile" 是一个存在的文件,65 是一个项目标识符
// 检查 ftok 是否成功
if (key == -1) {
perror("ftok");
return 1;
}
// 创建共享内存段
shmid = shmget(key, 1024, 0644 | IPC_CREAT);
// 检查 shmget 是否成功
if (shmid == -1) {
perror("shmget");
return 1;
}
// 将共享内存映射到进程的地址空间
shared_memory = shmat(shmid, (void *)0, 0);
// 检查 shmat 是否成功
if (shared_memory == (void *)-1) {
perror("shmat");
return 1;
}
printf("共享内存映射成功,映射地址为: %p\n", shared_memory);
// ... 在这里进行共享内存的数据操作 ...
// 解除共享内存映射
if (shmdt(shared_memory) == -1) {
perror("shmdt");
return 1;
}
printf("共享内存解除映射成功。\n");
// 销毁共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
return 1;
}
printf("共享内存段销毁成功。\n");
return 0;
}
-
shmid
:- 类型:
int
- 描述:共享内存标识符。这是通过
shmget
函数成功创建共享内存段后返回的标识符。它唯一地标识了系统中的共享内存段。
- 类型:
-
cmd
:- 类型:
int
- 描述:要执行的命令。以下是一些常用的命令值:
IPC_STAT
: 获取共享内存段的shmid_ds
结构,并将其存储在buf
指向的位置。IPC_SET
: 设置共享内存段的shmid_ds
结构中的某些成员。要修改的成员必须通过buf
指针传递。IPC_RMID
: 销毁共享内存段。当最后一个进程解除与共享内存段的连接时,该段将被删除。IPC_INFO
: 返回有关系统范围内的共享内存限制和参数的信息。SHM_INFO
: 返回有关系统范围内的共享内存使用情况的信息。
- 类型:
-
buf
:-
类型:
struct shmid_ds *
-
描述:指向
shmid_ds
结构的指针,该结构用于获取或设置共享内存的属性。这个参数的使用取决于cmd
参数的值。以下是shmid_ds
结构的一些重要成员:shm_perm
: 包含共享内存的访问权限和所有权信息。shm_segsz
: 共享内存段的大小(以字节为单位)。shm_atime
: 最后一次调用shmat
的时间。shm_dtime
: 最后一次从共享内存段分离的时间。shm_ctime
: 最后一次更改shmid_ds
结构的时间。shm_cpid
: 创建共享内存段的进程 ID。shm_lpid
: 最后一次执行shmat
或shmdt
操作的进程 ID。shm_nattch
: 当前连接到共享内存段的进程数。
-
当使用 shmctl
时,根据 cmd
的不同,buf
参数的用法也不同。以下是两种常见情况:
-
如果
cmd
是IPC_RMID
:buf
应该是NULL
,因为不需要获取或设置任何属性。这个命令用于销毁共享内存段。
-
如果
cmd
是IPC_STAT
:buf
应该指向一个有效的shmid_ds
结构,以便shmctl
可以填充它。这个命令用于获取共享内存段的当前状态和属性。
信号量
信号量的作用
- 信号量是一把锁,用于进程资源,实现锁状态。
- 加锁是为了防止进程之间竞争CPU资源而导致数据出现混乱或丢失。
信号量操作流程
1. 获取创建信号量的关键字
- 使用
ftok
函数生成一个唯一的键(key),用于标识信号量。
2. 创建信号量锁
-
使用
semget
函数创建信号量集。int semget(key_t key, int nsems, int semflg); int main() { key_t key; int semid; // 使用 ftok 生成一个唯一的 key key = ftok("shmfile", 65); // "shmfile" 是一个存在的文件,65 是一个项目标识符 // 检查 ftok 是否成功 if (key == -1) { perror("ftok"); return 1; } // 创建信号量集 semid = semget(key, 3, 0666 | IPC_CREAT); // 检查 semget 是否成功 if (semid == -1) { perror("semget"); return 1; } printf("信号量集创建成功,标识符为: %d\n", semid); // 在这里可以继续使用 semid 来初始化信号量、操作信号量等 // 示例结束,不进行初始化、操作信号量等操作 return 0; }
key_t key
: 信号量的键。int nsems
: 创建的信号量数量。int semflg
: 标志位,可能包含IPC_CREAT
等。- 返回值:
- 成功返回信号量锁id。
- 失败返回 -1。
3. 信号量锁初始化
-
使用
semctl
函数初始化信号量。int semctl(int semid, int semnum, int cmd, ...); int main() { key_t key; int semid; int arg = 1; // 使用 ftok 生成一个唯一的 key key = ftok("shmfile", 65); // "shmfile" 是一个存在的文件,65 是一个项目标识符 // 检查 ftok 是否成功 if (key == -1) { perror("ftok"); return 1; } // 创建信号量集 semid = semget(key, 1, 0666 | IPC_CREAT); // 检查 semget 是否成功 if (semid == -1) { perror("semget"); return 1; } // 初始化信号量 arg.val = 1; // 设置 val 成员为 1,表示信号量的初始值 if (semctl(semid, 0, SETVAL, arg) == -1) { perror("semctl"); return 1; } printf("信号量初始化成功,初始值为: %d\n", arg.val); // 在这里可以继续使用 semid 来操作信号量等 // 示例结束,不进行其他操作 return 0; }
int semid
: 信号量锁id。int semnum
: 信号量编号(从0开始)。int cmd
: 命令类型,如IPC_STAT
、IPC_SET
、IPC_RMID
等。...
: 根据cmd
类型,可能需要传递其他参数。- 第一次锁必须处于打开状态
- 打开:正数
- 关闭:负数
- 返回值:
- 成功返回0。
- 失败返回 -1。
4. 设置加锁和解锁
-
使用
semop
函数对信号量进行操作。int semop(int semid, struct sembuf *sops, unsigned nsops); //加锁 int sem_lock(int id, int num) { struct sembuf sem; sem.sem_num = num; sem.sem_op = -1; // P操作,减一 sem.sem_flg = 0; return semop(id, &sem, 1); } //解锁 int sem_unlock(int id, int num) { struct sembuf sem; sem.sem_num = num; sem.sem_op = 1; // V操作,加一 sem.sem_flg = 0; return semop(id, &sem, 1); }
-
int semid
: 信号量锁id。 -
struct sembuf *sops
: 锁操作结构体指针,包含锁编号、操作值和标志位。struct sembuf { short sem_num; // 信号量的编号 short sem_op; // 信号量的操作 short sem_flg; // 操作的标志位 };
sem_num
: 信号量的编号。信号量集中的每个信号量都有一个唯一的编号,这个编号从 0 开始,对应于信号量集中的第一个信号量。sem_op
: 信号量的操作。这个字段包含了对信号量执行的操作类型,可以是减操作(P 操作)或加操作(V 操作)。- 对于 P 操作,
sem_op
应为负数,其绝对值表示需要减去的信号量值。例如,sem_op = -1
表示需要从信号量中减去 1。 - 对于 V 操作,
sem_op
应为正数,其值表示需要加上的信号量值。例如,sem_op = 1
表示需要向信号量中加上 1。
- 对于 P 操作,
sem_flg
: 操作的标志位。0:表示快速互斥锁。
-
unsigned nsops
: 操作的数量。 -
返回值:
- 成功返回0。
- 失败返回 -1。
-
5. 信号量锁的销毁
-
使用
semctl
函数销毁信号量。int semctl(int semid, int semnum, int cmd, ...); if (semctl(semid, 0, IPC_RMID, 0) == -1) { perror("semctl"); return 1; }
int semid
: 信号量锁id。int semnum
: 信号量编号。int cmd
: 命令类型,使用IPC_RMID
销毁信号量。...
: 根据cmd
类型,可能需要传递其他参数。- 返回值:
- 成功返回0。
- 失败返回 -1。