一、printf 缓冲区与 write 函数差异
1. 核心区别
| 函数 |
缓冲区特性 |
输出效果 |
printf() |
自带缓冲区(行缓冲 / 全缓冲) |
数据先存入缓冲区,满足刷新条件(\n、fflush、缓冲区满、程序结束)才会输出到屏幕,不会立即显示 |
write() |
无缓冲区 |
直接将数据写入文件描述符(如 stdout 对应屏幕),数据能够立马显示,无需等待刷新 |
cpp
复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main() {
// printf有缓冲区,无\n/fflush时,sleep期间不会输出
printf("printf输出(无缓冲区刷新)");
// write无缓冲区,会立即输出
write(1, "write输出(立即显示)\n", strlen("write输出(立即显示)\n"));
sleep(3); // 阻塞3秒,观察输出顺序
printf("\nprintf输出(换行符触发刷新)\n");
return 0;
}
- 运行结果:先立即显示
write输出(立即显示),3 秒后才显示printf输出(无缓冲区刷新)和后续换行后的内容。
二、进程替换(exec 系列函数)
1. 核心分类与底层关系
| 类型 |
函数名 |
特性说明 |
| 系统调用 |
execve() |
所有 exec 系列函数的底层实现,必须传递环境变量,参数需指定完整路径 |
| 库函数 |
execl()、execle() |
列表型参数,参数以可变参数形式传递,以NULL结尾;execle()需手动传入环境变量 |
| 库函数 |
execlp()、execvp() |
带p后缀,无需指定完整路径,系统会自动在PATH环境变量中查找程序路径 |
| 库函数 |
execv() |
矢量型参数,参数以字符串数组形式传递,数组末尾需以NULL结尾 |
2. 关键特性
- 环境变量传递 :默认情况下,exec 库函数会隐式传递当前进程的环境变量;若需修改环境变量,需使用
execle()(手动传入自定义环境变量数组)。
- 参数命名规则 :
- 前 3 个替换函数(
execl()/execlp()/execle())的第二个参数是替换程序的名称,可自定义命名,但第一个参数(路径 / 程序名)必须准确。
execvp()的参数数组中,argv[0]通常与第一个参数(程序名)一致,属于合法且常规的传递习惯,argv[0]相当于给替换后的程序起新名字。
- 返回值特性 :exec 函数执行成功后,原进程内存空间被完全替换,后续代码不会执行;只有替换失败时,才会返回
-1并继续执行原进程后续代码。
- bash 底层原理 :bash 执行命令 / 程序时,会先
fork()创建子进程,再通过 exec 函数将子进程替换为目标程序,替换后的程序仍为 bash 的子进程(通过ppid可验证)。
3. 常用示例(含 fork+exec 组合)
(1)execlp(无需路径,执行 ls 命令)
cpp
复制代码
#include <unistd.h>
#include <stdio.h>
int main() {
printf("原进程PID=%d\n", getpid());
// 替换为ls -l命令,参数以NULL结尾
int ret = execlp("ls", "ls", "-l", NULL);
// 替换失败才会执行以下代码
if (ret == -1) {
perror("execlp替换失败");
return 1;
}
printf("原进程后续代码(不会执行)\n");
return 0;
}
(2)fork+exec 组合(父进程等待子进程替换执行)
cpp
复制代码
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
pid_t pid = fork();
if (pid > 0) {
// 父进程:等待子进程执行完毕
int status;
wait(&status);
printf("子进程替换执行完成,父进程PID=%d\n", getpid());
} else if (pid == 0) {
// 子进程:替换为ps -f命令
char* myargv[] = {"ps", "-f", NULL}; // execvp参数数组
printf("子进程PID=%d,即将替换为ps命令\n", getpid());
execvp("ps", myargv);
// 替换失败处理
perror("execvp替换失败");
exit(1);
} else {
perror("fork创建子进程失败");
return 1;
}
return 0;
}
(3)execle(手动传入环境变量)
cpp
复制代码
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
// 自定义环境变量
char* myenv[] = {"MY_ENV=test", "PATH=/usr/bin", NULL};
// 替换为echo命令,手动传入环境变量
execle("/usr/bin/echo", "echo", "$MY_ENV", NULL, myenv);
perror("execle替换失败");
return 1;
}
三、信号相关(signal、kill、SIGCHLD 等)
1. 核心概念
- 信号本质:用于通知进程发生了某个事件(软中断),程序正常运行时一般不产生信号,不当操作(如
ctrl+c)或系统事件(如子进程结束)会触发信号。
- 进程响应方式:默认处理(如终止进程)、忽略信号、自定义处理函数。
- 关键头文件:
#include <signal.h>
2. 常用信号宏定义
| 信号宏定义 |
信号编号 |
触发场景 |
默认处理方式 |
SIGINT |
2 |
键盘按下ctrl+c |
终止当前进程 |
SIGCHLD |
17 |
子进程结束后,系统自动向父进程发送该信号 |
忽略该信号 |
SIGPIPE |
- |
管道读端关闭后,写端继续写入数据时触发 |
终止当前进程 |
SIGKILL |
9 |
kill -9 pid发送的终止信号 |
强制终止进程(不可捕获 / 忽略) |
3. signal 函数(信号处理注册)
(1)函数特性
- 函数原型:
void (*signal(int sig, void (*func)(int)))(int);
- 第一个参数:要处理的信号(如
SIGINT),用于约定 "捕获到该信号时执行对应操作"。
- 第二个参数:
- 函数指针:返回值为
void,参数只有一个int类型(接收信号编号),函数名本身即为函数指针。
- 特殊指令:
SIG_IGN(忽略信号)、SIG_DFL(默认处理信号),用于命令内核执行对应操作。
- 核心逻辑:类似 "设置陷阱",仅约定信号与处理方式的关联,信号未触发时,不会执行处理函数;信号触发时,进程从用户态切换到内核态,执行处理函数后返回用户态继续运行(打破代码从上到下执行的常规逻辑)。
(2)使用示例
示例 1:自定义 SIGINT 信号处理函数
cpp
复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
// 自定义信号处理函数:返回值void,参数int(信号编号)
void fun_sig(int sig) {
printf("捕获到信号:sig=%d(SIGINT)\n", sig);
}
int main() {
// 约定:捕获SIGINT信号时,调用fun_sig函数
signal(SIGINT, fun_sig);
// 死循环,等待信号触发
while (1) {
printf("程序运行中,按ctrl+c触发信号\n");
sleep(1);
}
return 0;
}
- 运行结果:程序循环输出信息,按下
ctrl+c不会终止进程,而是触发fun_sig函数输出捕获信息,之后继续循环。
示例 2:动态修改信号处理方式(先忽略后默认)
cpp
复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void fun_sig(int sig) {
printf("第一次捕获信号:sig=%d,后续按ctrl+c将终止程序\n", sig);
// 将SIGINT信号恢复为默认处理方式(将来时,第二次触发才生效)
signal(SIGINT, SIG_DFL);
}
int main() {
// 第一步:约定捕获SIGINT,调用fun_sig
signal(SIGINT, fun_sig);
while (1) {
printf("程序运行中,第一次按ctrl+c忽略默认终止,第二次终止\n");
sleep(1);
}
return 0;
}
4. kill 函数 / 指令(发送信号)
(1)核心区别
| 类型 |
形式 |
说明 |
| 系统调用 |
kill(pid_t pid, int sig); |
头文件#include <signal.h>,向指定 PID 进程发送指定信号,成功返回 0,失败返回 - 1 |
| 终端指令 |
kill [信号] pid |
如kill -9 1234(发送 SIGKILL 信号强制终止 PID=1234 的进程),内置指令无需额外程序 |
(2)kill 系统调用示例
cpp
复制代码
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
if (argc != 3) {
printf("用法:%s <pid> <signal>\n", argv[0]);
return 1;
}
pid_t pid = atoi(argv[1]); // 转换为进程PID
int sig = atoi(argv[2]); // 转换为信号编号
// 向指定进程发送信号
int ret = kill(pid, sig);
if (ret == -1) {
perror("kill发送信号失败");
return 1;
}
printf("成功向PID=%d的进程发送信号%d\n", pid, sig);
return 0;
}
5. SIGCHLD 信号(子进程结束信号)
(1)核心作用
- 子进程结束后,系统自动向父进程发送SIGCHLD信号,父进程默认忽略该信号,导致子进程成为僵死进程。
- 通过注册SIGCHLD信号的处理函数,在函数中调用wait()/waitpid(),可自动回收子进程资源,避免僵死进程。
(2)示例:自动回收子进程
cpp
复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>
// SIGCHLD信号处理函数:回收子进程资源
void recycle_child(int sig) {
// 循环回收所有退出的子进程,避免遗漏
while (waitpid(-1, NULL, WNOHANG) > 0) {
printf("子进程已结束,资源已回收\n");
}
}
int main() {
// 注册SIGCHLD信号处理函数
signal(SIGCHLD, recycle_child);
pid_t pid = fork();
if (pid > 0) {
// 父进程:持续运行
while (1) {
printf("父进程运行中,PID=%d\n", getpid());
sleep(2);
}
} else if (pid == 0) {
// 子进程:运行5秒后退出
printf("子进程运行中,PID=%d,5秒后退出\n", getpid());
sleep(5);
exit(0);
} else {
perror("fork失败");
return 1;
}
return 0;
}
四、目录与用户信息操作
1. 目录操作(dirent.h)
(1)核心函数
| 函数 |
功能说明 |
opendir() |
打开目录流,返回DIR*结构体指针(失败返回 NULL),头文件#include <dirent.h> |
readdir() |
读取目录项,每次调用返回下一个目录项的struct dirent*指针,遍历完毕返回 NULL |
closedir() |
关闭目录流,释放资源 |
(2)示例:遍历目录内容
cpp
复制代码
#include <stdio.h>
#include <dirent.h>
#include <stdlib.h>
int main() {
// 打开当前目录
DIR* dir = opendir(".");
if (dir == NULL) {
perror("opendir打开目录失败");
return 1;
}
// 遍历目录项
struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
printf("目录项:%s\n", entry->d_name);
}
// 关闭目录流
closedir(dir);
return 0;
}
2. 用户信息操作(pwd.h)
(1)核心函数
| 函数 |
功能说明 |
getuid() |
获取启动当前进程的用户 ID(无参数,返回uid_t类型),头文件#include <pwd.h> |
getpwuid() |
根据用户 ID 查询用户详细信息,返回struct passwd*结构体指针(包含用户名、家目录等) |
(2)示例:获取用户信息
cpp
复制代码
#include <stdio.h>
#include <pwd.h>
#include <unistd.h>
int main() {
// 获取当前用户ID
uid_t uid = getuid();
printf("当前用户ID:%u\n", uid);
// 获取用户详细信息
struct passwd* pw = getpwuid(uid);
if (pw == NULL) {
perror("getpwuid查询用户信息失败");
return 1;
}
printf("用户名:%s\n", pw->pw_name);
printf("家目录:%s\n", pw->pw_dir);
printf("登录shell:%s\n", pw->pw_shell);
return 0;
}
3. 获取当前工作目录(unistd.h)
(1)函数说明
4. 文件信息获取(sys/stat.h)
(1)函数说明
-
函数:stat(const char* pathname, struct stat* statbuf);
-
功能:获取文件 / 目录的详细信息(大小、权限、类型等),存储到struct stat结构体中。
-
示例:
cpp
复制代码
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
struct stat st;
// 获取当前目录下a.txt的信息
if (stat("a.txt", &st) == -1) {
perror("stat获取文件信息失败");
return 1;
}
printf("文件大小:%ld 字节\n", st.st_size);
printf("文件权限:%o\n", st.st_mode & 0777); // 屏蔽高位,显示八进制权限
printf("文件最后访问时间:%ld\n", st.st_atime);
return 0;
}
五、进程间通信(IPC)****(都是重点)
1. 基础前置概念
(1)文件描述符(FD)
-
本质:操作系统为打开的文件 / 管道 /socket 等对象分配的非负整数编号,用于标识和操作这些对象,是程序操作底层资源的句柄。
(2)阻塞概念
- 程序执行到某行代码时暂停执行,直到特定操作完成或条件满足(如
wait()、read()阻塞、sleep()),阻塞期间无法执行后续代码,处于 "卡住" 状态。
(3)程序异常终止
- 若程序未运行到结尾却突然终止,大概率是触发了信号(如
SIGINT、SIGPIPE),信号默认处理方式为终止进程。
(4)通信方式分类
| 通信方式 |
特性说明 |
| 单工 |
仅单向通信(A→B,B 无法向 A 发送),如打印机(电脑→打印机) |
| 半双工 |
双向通信,但不能同时发送(如管道、对讲机) |
| 全双工 |
双向通信,可同时发送(如网络套接字、电话) |
2. IPC 分类
常见进程间通信方式:管道(有名 / 无名)、信号量、共享内存、消息队列、套接字。
3. 管道通信(核心:半双工通信,数据存储在内存中)
(1)管道核心特性
- 通信要求:双方进程需同时运行(时间意义上的同时),否则管道会阻塞。
- 阻塞规则:
- 管道为空时,
read()会阻塞,直到有数据写入或写端关闭。
- 管道为满时,
write()会阻塞,直到有数据读取或读端关闭。
- 通信方式:半双工(双方可互相通信,但不能同时发送数据,类似写信)。
- 管道关闭:一方关闭管道后,另一方会感知到(如读端收到
read()返回 0),并自动结束相关操作。
(2)有名管道(命名管道)
核心特性
- 存在形式:以文件形式存在于文件系统中(无后缀名),本质是内核中的数据结构。
- 适用范围:可用于任意两个进程间通信(无需父子关系)。
- 创建方式:终端指令
mkfifo 管道名(如mkfifo myfifo)。
- 头文件:
#include <fcntl.h>(管道本质是文件,需用文件操作函数打开 / 读写)。
示例:有名管道通信(写端 + 读端)
写端程序(fifo_write.c)
cpp
复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1. 创建有名管道(若已存在则无需重复创建)
if (mkfifo("myfifo", 0644) == -1) {
perror("mkfifo创建管道失败");
// 若管道已存在,忽略该错误
if (errno != EEXIST) return 1;
}
// 2. 以只写方式打开管道
int fd = open("myfifo", O_WRONLY);
if (fd == -1) {
perror("open打开管道失败");
return 1;
}
// 3. 向管道写入数据
const char* msg = "Hello 有名管道通信!";
write(fd, msg, strlen(msg));
printf("写端:已写入数据:%s\n", msg);
// 4. 关闭管道
close(fd);
return 0;
}
读端程序(fifo_read.c)
cpp
复制代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main() {
// 1. 以只读方式打开管道(需先创建管道或写端已创建)
int fd = open("myfifo", O_RDONLY);
if (fd == -1) {
perror("open打开管道失败");
return 1;
}
// 2. 从管道读取数据
char buf[1024] = {0};
ssize_t len = read(fd, buf, sizeof(buf) - 1);
if (len == -1) {
perror("read读取管道失败");
close(fd);
return 1;
} else if (len == 0) {
printf("读端:管道写端已关闭\n");
close(fd);
return 0;
}
printf("读端:已读取数据:%s\n", buf);
// 3. 关闭管道
close(fd);
return 0;
}
- 运行方式:打开两个终端,分别运行
./fifo_write和./fifo_read,实现跨进程通信。
(3)无名管道
核心特性
- 存在形式:仅存在于内核中,无文件形式,是内核内部的数据结构。
- 适用范围:仅能用于父子进程(或兄弟进程)间通信。
- 创建方式:调用
pipe()函数,无需手动创建文件。
- 函数原型:
int pipe(int fd[2]);(fd[0]为读端描述符,fd[1]为写端描述符,成功返回 0,失败返回 - 1)。
cpp
复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <stdlib.h>
int main() {
// 1. 创建无名管道
int fd[2];
if (pipe(fd) == -1) {
perror("pipe创建管道失败");
return 1;
}
// 2. fork创建父子进程
pid_t pid = fork();
if (pid > 0) {
// 父进程:关闭读端,向管道写数据
close(fd[0]);
const char* msg = "Hello 无名管道通信!";
write(fd[1], msg, strlen(msg));
printf("父进程:已写入数据:%s\n", msg);
close(fd[1]);
wait(NULL); // 等待子进程
} else if (pid == 0) {
// 子进程:关闭写端,从管道读数据
close(fd[1]);
char buf[1024] = {0};
ssize_t len = read(fd[0], buf, sizeof(buf) - 1);
if (len > 0) {
printf("子进程:已读取数据:%s\n", buf);
}
close(fd[0]);
exit(0);
} else {
perror("fork失败");
return 1;
}
return 0;
}
(4)管道异常处理
- 写端关闭,读端继续读取:
read()返回 0,标识管道已关闭。
- 读端关闭,写端继续写入:触发
SIGPIPE信号,默认终止进程。
4. 消息队列
(1)核心概念
- 本质:存储消息的队列(数据结构),消息以结构体形式存储,支持不同类型消息的分类读取。
- 适用范围:可用于任意两个进程间通信,数据存储在内核中。
- 头文件:
#include <sys/msg.h>
- 查看 / 删除指令:
ipcs:查看所有 IPC 资源(消息队列、信号量、共享内存)。
ipcs -q:仅查看消息队列。
ipcrm -q <msgid>:删除指定标识符的消息队列。
(2)核心函数
1. msgget(创建 / 获取消息队列)
- 函数原型:
int msgget(key_t key, int msgflg);
- 参数说明:
key:关键字(可自定义,如(key_t)1234),用于标识消息队列。
msgflg:标志位(IPC_CREAT | 0600:不存在则创建,存在则返回标识符,同时指定权限)。
- 返回值:成功返回消息队列标识符(
msgid),失败返回 - 1。
2. msgsnd(向消息队列添加消息)
- 函数原型:
int msgsnd(int msqid, const void* msgp, size_t msgsz, int msgflg);
- 参数说明:
msqid:消息队列标识符(msgget返回值)。
msgp:消息结构体指针(第一个成员必须是long type(消息类型),后续成员自定义数据)。
msgsz:消息数据大小(不包含long type的大小)。
msgflg:标志位(设为 0,阻塞等待发送)。
- 返回值:成功返回 0,失败返回 - 1。
3. msgrcv(从消息队列读取消息)
- 函数原型:
ssize_t msgrcv(int msqid, void* msgp, size_t msgsz, long msgtyp, int msgflg);
- 参数说明:
msqid:消息队列标识符。
msgp:接收消息的结构体指针。
msgsz:接收消息的数据大小(不包含long type)。
msgtyp:消息类型筛选:
- 0:接收队列中第一个消息(不区分类型)。
-
0:接收类型为msgtyp的第一个消息。
- <0:接收类型小于或等于该值绝对值的第一个消息。
msgflg:标志位(设为 0,阻塞等待接收)。
- 返回值:成功返回读取的字节数,失败返回 - 1。
4. msgctl(控制消息队列,如删除)
- 函数原型:
int msgctl(int msqid, int cmd, struct msqid_ds* buf);
- 参数说明:
msqid:消息队列标识符。
cmd:操作命令(IPC_RMID:立即删除消息队列)。
buf:队列属性结构体指针(删除时可设为 NULL)。
- 返回值:成功返回 0,失败返回 - 1。
(3)示例:消息队列通信(发送端 + 接收端)
消息结构体定义(共用)
cpp
复制代码
// 消息结构体:第一个成员必须是long type
struct msg_buf {
long msg_type; // 消息类型
char msg_data[32]; // 消息数据
};
发送端程序(msg_send.c)
cpp
复制代码
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
struct msg_buf {
long msg_type;
char msg_data[32];
};
int main() {
// 1. 创建/获取消息队列
int msgid = msgget((key_t)1234, IPC_CREAT | 0600);
if (msgid == -1) {
perror("msgget创建消息队列失败");
return 1;
}
// 2. 构造消息
struct msg_buf msg;
msg.msg_type = 1; // 消息类型设为1
strcpy(msg.msg_data, "Hello 消息队列!");
// 3. 发送消息
if (msgsnd(msgid, &msg, sizeof(msg.msg_data), 0) == -1) {
perror("msgsnd发送消息失败");
return 1;
}
printf("发送端:已发送消息,类型=%ld,内容=%s\n", msg.msg_type, msg.msg_data);
return 0;
}
接收端程序(msg_recv.c)
cpp
复制代码
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
struct msg_buf {
long msg_type;
char msg_data[32];
};
int main() {
// 1. 获取消息队列
int msgid = msgget((key_t)1234, IPC_CREAT | 0600);
if (msgid == -1) {
perror("msgget获取消息队列失败");
return 1;
}
// 2. 接收消息(接收类型为1的消息)
struct msg_buf msg;
ssize_t len = msgrcv(msgid, &msg, sizeof(msg.msg_data), 1, 0);
if (len == -1) {
perror("msgrcv接收消息失败");
return 1;
}
printf("接收端:已接收消息,类型=%ld,内容=%s,长度=%ld\n", msg.msg_type, msg.msg_data, len);
// 3. 删除消息队列
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl删除消息队列失败");
return 1;
}
printf("接收端:消息队列已删除\n");
return 0;
}
5. 信号量(同步互斥工具,解决临界资源竞争问题)
(1)核心概念
1. 基础定义
- 信号量本质 :特殊的非负整数变量,仅支持
+1(V 操作)和-1(P 操作)两种原子操作,用于控制进程对临界资源的访问。
- P 操作(获取资源):信号量值减 1,若减后值 < 0,进程阻塞等待;若≥0,可访问临界资源(类似公共厕所锁门)。
- V 操作(释放资源):信号量值加 1,若加后值≤0,唤醒阻塞的进程;若 > 0,无额外操作(类似公共厕所开门)。
- 临界资源:同一时刻仅允许一个进程访问的资源(如公共厕所、共享内存)。
- 临界区:访问临界资源的代码段(进程中操作临界资源的部分)。
- 默认初始值:通常为 1(实现互斥,保证同一时刻只有一个进程访问临界资源)。
2. 关键关联
- 信号量是进程间通信方式之一:进程通过访问和修改信号量的值,判断临界资源是否可用,实现进程间的 "交流"(类似红绿灯指示过马路)。
- 信号量集:多个信号量的集合(类似数组),每个信号量有索引(下标从 0 开始),
semid为信号量集的内核内部标识符。
- key 与 semid 的关系 :
key是用户指定的 "外部标识"(固定值,如(key_t)1234),semid是内核动态分配的 "内部标识"(同一 key 在不同环境下 semid 可能不同)。
3. 联合体(union)说明
- 本质:特殊数据类型,允许不同数据类型共享同一段内存空间,用于节省内存,主要为信号量控制操作传递参数。
- 用途 :单个信号量的初始值需通过联合体存储,传递给
semctl()函数完成初始化。
(2)核心函数(头文件:#include <sys/sem.h>)
| 函数名 |
功能说明 |
|
|
|
semget() |
创建 / 获取信号量集原型:int semget(key_t key, int nsems, int semflg);参数:- key:外部标识- nsems:信号量集中信号量的个数- semflg:标志位(`IPC_CREAT |
IPC_EXCL |
0600:创建全新信号量集,存在则失败;IPC_CREAT |
0600:不存在则创建,存在则返回现有标识)<br>返回值:成功返回semid`(信号量集标识符),失败返回 - 1 |
semop() |
操作信号量(执行 P/V 操作)原型:int semop(int semid, struct sembuf *sops, size_t nsops);参数:- semid:信号量集标识符- sops:sembuf结构体指针(描述单个信号量操作)- nsops:操作次数(sops若为数组,指定数组长度)返回值:成功返回 0,失败返回 - 1 |
|
|
|
semctl() |
控制信号量(初始化 / 删除)原型:int semctl(int semid, int semnum, int cmd, ...);参数:- semid:信号量集标识符- semnum:信号量在集中的索引(下标)- cmd:操作命令(SETVAL:设置信号量值;IPC_RMID:删除信号量集)- 可选参数:联合体(初始化时传递信号量初始值)返回值:成功返回非负整数,失败返回 - 1 |
|
|
|
关键结构体:sembuf(描述单个信号量操作)
cpp
复制代码
struct sembuf {
short sem_num; // 信号量在集中的索引(下标)
short sem_op; // 操作类型:-1(P操作)、1(V操作)
short sem_flg; // 标志位(通常设为0,阻塞等待;IPC_NOWAIT:非阻塞)
};
cpp
复制代码
#ifndef SEM_TOOL_H
#define SEM_TOOL_H
#include <sys/sem.h>
// 信号量集标识符(全局变量,方便封装函数访问)
extern int semid;
// 信号量外部标识(可自定义)
#define SEM_KEY (key_t)1234
// 联合体:用于设置信号量初始值
union semun {
int val; // 单个信号量的初始值
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
// 信号量初始化(创建信号量集并设置初始值)
void sem_init();
// P操作(获取资源,信号量减1)
void sem_p(int sem_index);
// V操作(释放资源,信号量加1)
void sem_v(int sem_index);
// 信号量集销毁(删除信号量集)
void sem_destroy();
#endif
cpp
复制代码
#include "sem_tool.h"
#include <stdio.h>
#include <stdlib.h>
int semid; // 全局信号量集标识符
// 信号量初始化
void sem_init() {
// 创建包含2个信号量的信号量集(可根据需求修改nsems)
semid = semget(SEM_KEY, 2, IPC_CREAT | IPC_EXCL | 0666);
if (semid == -1) {
// 若信号量集已存在,获取现有信号量集
semid = semget(SEM_KEY, 2, 0666);
if (semid == -1) {
perror("semget创建/获取信号量集失败");
exit(1);
}
printf("信号量集已存在,获取成功,semid=%d\n", semid);
} else {
printf("信号量集创建成功,semid=%d\n", semid);
// 初始化信号量0为1(允许第一个进程访问)
union semun sem_union0;
sem_union0.val = 1;
if (semctl(semid, 0, SETVAL, sem_union0) == -1) {
perror("semctl初始化信号量0失败");
exit(1);
}
// 初始化信号量1为0(阻塞进程直到有数据)
union semun sem_union1;
sem_union1.val = 0;
if (semctl(semid, 1, SETVAL, sem_union1) == -1) {
perror("semctl初始化信号量1失败");
exit(1);
}
printf("信号量0(初始值1)、信号量1(初始值0)初始化成功\n");
}
}
// P操作(根据信号量索引执行减1操作)
void sem_p(int sem_index) {
struct sembuf sem_buf;
sem_buf.sem_num = sem_index; // 信号量索引
sem_buf.sem_op = -1; // P操作:减1
sem_buf.sem_flg = 0; // 阻塞等待
if (semop(semid, &sem_buf, 1) == -1) { // 第三个参数:执行1次操作
perror("semop P操作失败");
exit(1);
}
printf("信号量%d P操作执行成功\n", sem_index);
}
// V操作(根据信号量索引执行加1操作)
void sem_v(int sem_index) {
struct sembuf sem_buf;
sem_buf.sem_num = sem_index; // 信号量索引
sem_buf.sem_op = 1; // V操作:加1
sem_buf.sem_flg = 0; // 默认标志
if (semop(semid, &sem_buf, 1) == -1) { // 第三个参数:执行1次操作
perror("semop V操作失败");
exit(1);
}
printf("信号量%d V操作执行成功\n", sem_index);
}
// 信号量集销毁(删除整个信号量集)
void sem_destroy() {
if (semctl(semid, 0, IPC_RMID) == -1) { // semnum参数在删除时可忽略,设为0即可
perror("semctl删除信号量集失败");
exit(1);
}
printf("信号量集(semid=%d)已成功删除\n", semid);
}
3. 测试程序(sem_test.c)
cpp
复制代码
#include "sem_tool.h"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <stdlib.h> // rand函数头文件
// rand函数说明:随机生成0到RAND_MAX之间的非负整数,用于模拟随机业务场景
// 示例:生成随机休眠时间,模拟不同进程访问临界资源的时机差异
int get_random_sleep_time() {
return rand() % 3 + 1; // 生成1-3秒的随机休眠时间
}
// 临界区函数(模拟访问临界资源)
void critical_area(int pid) {
printf("进程%d:进入临界区,开始访问临界资源\n", pid);
int sleep_time = get_random_sleep_time();
sleep(sleep_time); // 模拟临界资源操作耗时(随机时间)
printf("进程%d:退出临界区,释放临界资源(耗时%d秒)\n", pid, sleep_time);
}
int main() {
// 初始化信号量集
sem_init();
// 创建子进程
pid_t pid = fork();
if (pid > 0) {
// 父进程:P操作获取资源 -> 访问临界区 -> V操作释放资源
printf("父进程(PID=%d):准备获取临界资源\n", getpid());
sem_p(0); // 操作信号量0(初始值1)
critical_area(getpid());
sem_v(0); // 释放信号量0
wait(NULL); // 等待子进程结束
// 销毁信号量集
sem_destroy();
} else if (pid == 0) {
// 子进程:P操作获取资源 -> 访问临界区 -> V操作释放资源
printf("子进程(PID=%d):准备获取临界资源\n", getpid());
sem_p(0); // 操作信号量0(父进程释放后才能获取)
critical_area(getpid());
sem_v(0); // 释放信号量0
exit(0);
} else {
perror("fork创建子进程失败");
exit(1);
}
return 0;
}
(4)相关指令
| 指令 |
功能说明 |
ipcs |
查看所有 IPC 资源(信号量、共享内存、消息队列) |
ipcs -s |
仅查看信号量集的详细信息(包含 semid、key、信号量个数等) |
ipcrm -s <semid> |
删除指定 semid 的信号量集 |
6. 共享内存(最快的 IPC 方式,临界资源之一)
(1)核心概念
1. 本质与特性
- 本质:多个进程映射到同一块物理内存区域,该区域在各个进程的逻辑地址空间中可见,进程可直接读写该内存(无需数据拷贝,速度最快)。
- 存在问题:多个进程同时访问共享内存会导致数据不一致(如同时写入不同数据),需通过信号量或互斥锁实现同步互斥。
- 通信搭配:通常与两个信号量配合使用(一个控制写入、一个控制读取),实现 "写进程写入时读进程阻塞,读进程读取时写进程阻塞"。
2. 关键说明
- 共享内存是临界资源的一种,需通过同步机制保证访问安全。
- 映射:将物理共享内存转换为进程的逻辑地址,通过
shmat()实现;解除映射:将共享内存从进程逻辑地址空间分离,通过shmdt()实现。
3. 标志位说明
- 本质:二进制位或一组二进制位,用于表示特定状态或条件,控制程序流程、表示操作结果或系统状态。
- 或位运算符(|) :用于将多个标志位组合成一个新的标志位(如
IPC_CREAT|0600、IPC_CREAT|IPC_EXCL|0666),实现多个条件的同时生效。
- 常用标志位 :
IPC_CREAT:若共享内存 / 信号量不存在则创建,存在则返回现有标识符。
IPC_EXCL:与IPC_CREAT配合使用,创建全新资源,若资源已存在则函数调用失败。
0600/0666:文件权限位,指定资源的读写执行权限(与文件权限规则一致)。
4. rand 函数说明
- 功能 :随机生成整数,默认返回
0到RAND_MAX(系统定义的最大随机数,通常为2147483647)之间的非负整数。
- 头文件 :
#include <stdlib.h>。
- 用途:在共享内存 / 信号量测试中,模拟随机的业务耗时、访问时机差异,更贴近实际应用场景。
- 示例 :
int random_num = rand() % 5; // 生成0-4之间的随机整数
(2)核心函数(头文件:#include <sys/shm.h>)
| 函数名 |
功能说明 |
|
|
shmget() |
创建 / 获取共享内存段原型:int shmget(key_t key, size_t size, int shmflg);参数:- key:外部标识- size:共享内存大小(字节)- shmflg:标志位(`IPC_CREAT |
IPC_EXCL |
0600:创建全新共享内存,存在则失败)<br>返回值:成功返回shmid`(共享内存标识符),失败返回 - 1 |
shmat() |
映射共享内存(将共享内存连接到进程逻辑地址空间)原型:void *shmat(int shmid, const void *shmaddr, int shmflg);参数:- shmid:共享内存标识符- shmaddr:指定映射地址(通常设为 NULL,由系统自动分配)- shmflg:标志位(通常设为 0,可读可写)返回值:成功返回共享内存起始逻辑地址(指针),失败返回(void*)-1 |
|
|
shmdt() |
解除共享内存映射(将共享内存从进程逻辑地址空间分离)原型:int shmdt(const void *shmaddr);参数:shmaddr:shmat()返回的共享内存起始地址返回值:成功返回 0,失败返回 - 1 |
|
|
shmctl() |
控制共享内存(删除共享内存段等)原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数:- shmid:共享内存标识符- cmd:操作命令(IPC_RMID:删除共享内存段)- buf:共享内存属性结构体指针(删除时设为 NULL)返回值:成功返回 0,失败返回 - 1 |
|
|
(3)示例:共享内存 + 信号量实现进程间同步通信
cpp
复制代码
#ifndef SHM_TOOL_H
#define SHM_TOOL_H
#include <sys/shm.h>
#define SHM_KEY (key_t)4321 // 共享内存外部标识
#define SHM_SIZE 1024 // 共享内存大小(1KB)
// 创建/获取共享内存
int shm_create();
// 映射共享内存
void* shm_attach(int shmid);
// 解除共享内存映射
int shm_detach(void* shm_addr);
// 删除共享内存
int shm_remove(int shmid);
#endif
cpp
复制代码
#include "shm_tool.h"
#include <stdio.h>
#include <stdlib.h>
// 创建/获取共享内存
int shm_create() {
// 标志位组合:IPC_CREAT(不存在则创建)| 0666(读写权限)
int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget创建/获取共享内存失败");
exit(1);
}
printf("共享内存创建/获取成功,shmid=%d\n", shmid);
return shmid;
}
// 映射共享内存
void* shm_attach(int shmid) {
void* shm_addr = shmat(shmid, NULL, 0); // shmaddr=NULL由系统分配地址,shmflg=0可读可写
if (shm_addr == (void*)-1) {
perror("shmat映射共享内存失败");
exit(1);
}
printf("共享内存映射成功,起始地址:%p\n", shm_addr);
return shm_addr;
}
// 解除共享内存映射
int shm_detach(void* shm_addr) {
int ret = shmdt(shm_addr);
if (ret == -1) {
perror("shmdt解除共享内存映射失败");
return -1;
}
printf("共享内存解除映射成功\n");
return 0;
}
// 删除共享内存
int shm_remove(int shmid) {
int ret = shmctl(shmid, IPC_RMID, NULL); // IPC_RMID:删除共享内存段
if (ret == -1) {
perror("shmctl删除共享内存失败");
return -1;
}
printf("共享内存(shmid=%d)已删除\n", shmid);
return 0;
}
3. 写进程程序(shm_write.c)
cpp
复制代码
#include "sem_tool.h"
#include "shm_tool.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdlib.h> // rand函数头文件
// 生成随机字符串,模拟向共享内存写入的动态数据
void generate_random_data(char* buf, int max_len) {
const char* chars = "0123456789abcdefghijklmnopqrstuvwxyz";
int len = rand() % (max_len - 1) + 1; // 生成1到max_len-1的随机长度
for (int i = 0; i < len; i++) {
buf[i] = chars[rand() % strlen(chars)];
}
buf[len] = '\0'; // 字符串结束符
}
int main() {
// 1. 初始化信号量集(2个信号量:0控制写入,1控制读取)
sem_init();
// 2. 创建/获取共享内存并映射
int shmid = shm_create();
void* shm_addr = shm_attach(shmid);
// 3. P操作获取写入权限(信号量0)
printf("写进程:准备写入数据到共享内存\n");
sem_p(0);
// 4. 向共享内存写入数据(临界区操作,写入随机字符串)
char send_buf[SHM_SIZE] = {0};
generate_random_data(send_buf, SHM_SIZE);
strcpy((char*)shm_addr, send_buf);
printf("写进程:已写入随机数据:%s\n", send_buf);
// 5. V操作释放读取权限(信号量1,唤醒读进程)
sem_v(1);
// 6. 等待读进程读取完成(模拟业务耗时,随机休眠1-3秒)
int sleep_time = rand() % 3 + 1;
printf("写进程:等待读进程读取数据(休眠%d秒)\n", sleep_time);
sleep(sleep_time);
// 7. 解除共享内存映射
shm_detach(shm_addr);
// 8. 销毁信号量集(此处可由读进程删除共享内存,避免提前删除)
sem_destroy();
return 0;
}
4. 读进程程序(shm_read.c)
cpp
复制代码
#include "sem_tool.h"
#include "shm_tool.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdlib.h> // rand函数头文件
int main() {
// 1. 初始化信号量集(与写进程使用同一key,获取现有信号量集)
sem_init();
// 2. 创建/获取共享内存并映射
int shmid = shm_create();
void* shm_addr = shm_attach(shmid);
// 3. P操作获取读取权限(信号量1,写进程写入后唤醒)
printf("读进程:等待读取共享内存数据\n");
sem_p(1);
// 4. 从共享内存读取数据(临界区操作)
char buf[SHM_SIZE] = {0};
strcpy(buf, (char*)shm_addr);
printf("读进程:已读取数据:%s\n", buf);
// 5. V操作释放写入权限(信号量0,允许写进程再次写入)
sem_v(0);
// 6. 模拟业务处理耗时(随机休眠1-2秒)
int sleep_time = rand() % 2 + 1;
printf("读进程:处理读取到的数据(休眠%d秒)\n", sleep_time);
sleep(sleep_time);
// 7. 解除共享内存映射
shm_detach(shm_addr);
// 8. 删除共享内存
shm_remove(shmid);
return 0;
}
(4)编译与运行说明(VSCode 环境)
1. 环境准备
- 将所有相关文件放在同一目录下:
sem_tool.h、sem_tool.c、shm_tool.h、shm_tool.c、shm_write.c、shm_read.c。
- 确保 VSCode 已配置 C/C++ 编译环境(安装 GCC 编译器、C/C++ 插件)。
2. 编译命令
(5)相关指令
| 指令 |
功能说明 |
ipcs |
查看所有 IPC 资源(包含共享内存) |
ipcs -m |
仅查看共享内存的详细信息(shmid、key、大小等) |
ipcrm -m <shmid> |
删除指定 shmid 的共享内存段 |
六、补充基础知识点
1. binary 目录
binary通常指存放可执行二进制文件的目录,系统命令(ls、cp、mv等)存放在/bin或/usr/bin等bin目录下。