• 首页
  • 搜索
  • 工具
  • 分类
  • 日志
  • 友链
  • 图片

It's Geek KingYoungy

KEEP CHALLENGE

分类 C/C++Unix/Linux应用层程序设计 下的文章

C/C++Unix/Linux应用层程序设计

深入理解pthread互斥量与条件变量的使用

2025-03-12 浏览量 325 暂无评论

深入理解pthread互斥量与条件变量的使用

在多线程编程中,线程同步是一个关键问题。为了避免多个线程同时访问共享资源导致的数据竞争和不一致,POSIX线程库(pthread)提供了互斥量(Mutex)和条件变量(Condition Variable)两种重要的同步机制。本文将详细介绍它们的使用方法,并通过代码示例帮助读者更好地理解。

1. 互斥量(Mutex)

互斥量是一种简单的同步机制,用于保护共享资源,确保同一时间只有一个线程可以访问该资源。互斥量的基本操作包括初始化、加锁、解锁和销毁。

1.1 互斥量的初始化

互斥量可以通过静态或动态方式初始化。静态初始化使用宏PTHREAD_MUTEX_INITIALIZER,而动态初始化使用pthread_mutex_init函数。

// 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

// 动态初始化
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
1.2 互斥量的加锁与解锁

线程在访问共享资源前需要加锁,访问完成后解锁。如果互斥量已被其他线程锁定,当前线程会被阻塞,直到互斥量被解锁。

pthread_mutex_lock(&mutex);
// 访问共享资源
pthread_mutex_unlock(&mutex);
1.3 互斥量的销毁

当互斥量不再需要时,应调用pthread_mutex_destroy函数销毁它,以释放系统资源。

pthread_mutex_destroy(&mutex);

2. 条件变量(Condition Variable)

条件变量用于生产者-消费者模型中,防止消费者拿到互斥量后的忙等待。它允许线程在某个条件未满足时进入等待状态并释放互斥锁,并在条件满足时被唤醒并请求互斥锁。条件变量通常与互斥量一起使用。

2.1 条件变量的初始化

条件变量也可以通过静态或动态方式初始化。

// 静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

// 动态初始化
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
2.2 条件变量的等待与通知

线程在等待条件变量时,会释放持有的互斥量,并在条件满足时重新获得互斥量。其他线程可以通过pthread_cond_signal或pthread_cond_broadcast唤醒等待的线程。

// 等待条件变量
pthread_mutex_lock(&mutex);
while (condition == false) { //一定要将循环检查共享资源,防止被唤醒后共享资源被没有被生产者生产
    pthread_cond_wait(&cond, &mutex);
}
pthread_mutex_unlock(&mutex);

// 通知条件变量
pthread_mutex_lock(&mutex);
condition = true;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
2.3 条件变量的销毁

当条件变量不再需要时,应调用pthread_cond_destroy函数销毁它。

pthread_cond_destroy(&cond);

3. 示例代码

以下是一个使用互斥量和条件变量的简单示例,展示了如何实现线程间的同步。

#include <pthread.h>
#include <stdio.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

void *thread_function(void *arg) {
    pthread_mutex_lock(&mutex);
    while (ready == 0) {
        pthread_cond_wait(&cond, &mutex);
    }
    printf("Thread condition met, proceeding\n");
    pthread_mutex_unlock(&mutex);
    return NULL;
}

int main() {
    pthread_t thread_id;
    pthread_create(&thread_id, NULL, thread_function, NULL);

    pthread_mutex_lock(&mutex);
    printf("Main thread signaling condition variable\n");
    ready = 1;
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mutex);

    pthread_join(thread_id, NULL);
    return 0;
}

在这个示例中,主线程通过条件变量唤醒等待的线程,确保线程在条件满足时继续执行。

4. 总结

互斥量和条件变量是多线程编程中常用的同步机制。互斥量用于保护共享资源,而条件变量用于防止线程的忙等待,协调生产者与消费者线程。通过合理使用这两种机制,可以有效地避免数据竞争和死锁问题,提高多线程程序的稳定性和性能。

希望本文能帮助你更好地理解和使用pthread互斥量与条件变量。如果你有任何问题或需要进一步的帮助,请随时留言!

- 阅读全文 -
C/C++Unix/Linux应用层程序设计

System V 消息队列总结

2025-03-05 浏览量 304 暂无评论

System V 消息队列总结:

前言

消息队列是一种面向消息的ipc机制,即内核会保证进程对消息队列IO时数据块(消息)的整体性。且其是双工的,任何进程都可以往里放消息,同时任何进程都可以往里读消息(甚至读到其自己的)。


1. msgget - 创建消息队列/获取已有消息队列的标识符

#include <sys/msg.h>

int msgget(key_t key, int msgflg);

• 参数:
• key:消息队列的唯一键值(通常由 ftok 生成),或使用 IPC_PRIVATE 创建私有队列。
• msgflg:标志位(如 IPC_CREAT 创建队列,IPC_EXCL 配合 IPC_CREAT 确保队列不存在时新建)。
• 返回值:成功返回消息队列标识符(msqid),失败返回 -1。


2. msgsnd - 发送消息

#include <sys/msg.h>

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

• 参数:
• msqid:消息队列标识符。
• msgp:指向消息结构体的指针(需自定义,见下文)。
• msgsz:消息内容(mtext)的字节数(不包括 mtype 字段)。
• msgflg:标志位(如 IPC_NOWAIT 非阻塞发送,队列满时立即返回错误)。
• 返回值:成功返回 0,失败返回 -1。

注意:就算多进程/线程同时非阻塞地调用该函数,内核依旧会保证消息之间地独立性。因为内核并不是依靠其阻塞来保证独立性的。


3. msgrcv - 接收消息

#include <sys/msg.h>

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

• 参数:
• msqid:消息队列标识符。
• msgp:接收消息的缓冲区指针(需自定义消息结构体)。
• msgsz:缓冲区中 mtext 部分的最大容量。
• msgtyp:指定接收消息的类型(见下文规则)。
• msgflg:标志位(如 IPC_NOWAIT 非阻塞接收,MSG_NOERROR 允许截断过长消息)。
• 返回值:成功返回实际接收的 mtext 字节数,失败返回 -1。


4. msgctl - 控制消息队列

#include <sys/msg.h>

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

• 参数:
• msqid:消息队列标识符。
• cmd:控制命令(常用值):

◦ **`IPC_STAT`**:获取队列元数据到 `buf`。
◦ **`IPC_SET`**:通过 `buf` 修改队列元数据(如权限、容量限制)。
◦ **`IPC_RMID`**:立即删除队列(`buf` 可设为 `NULL`)。

• buf:指向 struct msqid_ds 的指针(用于读写元数据)。
• 返回值:成功返回 0,失败返回 -1。


关键数据结构

消息结构体(需自定义)

struct msgbuf {
    long mtype;     // 消息类型(必须 > 0)
    char mtext[];  // 消息内容(实际长度由 `msgsz` 指定,这里的字符数组表示就是一块内存,可以是各种数据的组合)
};

队列元数据结构体(struct msqid_ds)

提取自内核维护的msg_ids数据结构的对应entries指针指向的结构。

struct msqid_ds {
    struct ipc_perm msg_perm;  // 权限信息(所有者、读写权限等)
    time_t          msg_stime; // 最后发送时间
    time_t          msg_rtime; // 最后接收时间
    time_t          msg_ctime; // 最后修改时间
    unsigned long   msg_cbytes; // 当前队列中的字节数
    msgqnum_t       msg_qnum;   // 当前队列中的消息数
    msglen_t        msg_qbytes; // 队列最大容量(字节数)
    pid_t           msg_lspid;  // 最后发送消息的进程PID
    pid_t           msg_lrpid;  // 最后接收消息的进程PID
};

核心原理

  1. 消息类型(mtype):
    • 用于分类消息,接收时可指定类型过滤(如 msgtyp > 0 匹配特定类型,msgtyp = 0 接收任意类型,msgtyp < 0 接受mtype小于msgtyp绝对值的mtype最小的消息)。
    • 类型为 long 类型,必须为正整数。
  2. 内核管理:
    • 消息队列以内核对象形式存在,独立于进程生命周期(需显式调用 msgctl(IPC_RMID) 删除)。
    • 消息以链表形式存储,保证先进先出(FIFO),但可通过 msgtyp 实现优先级读取。
  3. 同步与原子性:
    • msgsnd 和 msgrcv 是原子操作:单次调用发送或接收一条完整消息。
    • 若队列满或空,默认阻塞进程(除非指定 IPC_NOWAIT)。

示例代码片段

// 定义消息结构体(实际使用需动态分配 mtext 大小)
struct my_msg {
    long mtype;
    char mtext[100];
};

// 发送消息
struct my_msg msg_send = {1, "Hello"};
msgsnd(msqid, &msg_send, strlen(msg_send.mtext), 0);

// 接收消息
struct my_msg msg_recv;
msgrcv(msqid, &msg_recv, sizeof(msg_recv.mtext), 1, 0);

总结

• 适用场景:需要类型过滤、原子性操作或异步通信的进程间交互(如任务分发、事件通知)。
• 缺点:依赖内核资源,需手动管理队列生命周期;跨平台支持较弱(推荐优先考虑 POSIX 消息队列或套接字)。

- 阅读全文 -
C/C++Unix/Linux应用层程序设计

C语言可变参数与命令行参数解析:stdarg与getopt详解

2025-03-04 浏览量 318 暂无评论

C语言可变参数与命令行参数解析:stdarg与getopt详解


一、可变参数处理:stdarg.h

1.1 核心功能

stdarg.h 头文件提供在函数中处理不定数量参数的能力,常用于实现类似printf()的格式化输出函数。通过宏定义实现,与平台特性强关联。

1.2 核心类型与函数原型

#include <stdarg.h>

// 参数列表指针类型
va_list;

// 初始化va_list指针,last_param是最后一个固定参数名
void va_start(va_list ap, last_param);

// 获取下一个参数,type为参数类型(如int、char*)
// 一定要确定可变参数的个数,不要在最后一个参数被读取了依然调用该函数
type va_arg(va_list ap, type);

// 清理va_list指针
void va_end(va_list ap);

1.3 使用步骤

  1. 函数声明使用省略号(如int func(int n, ...))
  2. 定义va_list变量
  3. va_start初始化指针
  4. va_arg逐个读取参数
  5. va_end释放资源

1.4 示例:简化版printf函数实现

#include <stdarg.h>
#include <stdio.h>

void my_printf(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);  // 初始化参数列表

    for (const char *p = fmt; *p != '\0'; p++) {
        if (*p != '%') {
            putchar(*p);  // 普通字符直接输出
            continue;
        }

        // 处理格式符 %
        switch (*++p) {    // 跳过%并读取下一个字符
            case 'd': {    // 处理整数
                int num = va_arg(ap, int);
                printf("%d", num);  // 借用标准库输出
                break;
            }
            case 's': {    // 处理字符串
                char *str = va_arg(ap, char*);
                printf("%s", str);
                break;
            }
            case 'c': {    // 处理字符
                char ch = va_arg(ap, int);  // char提升为int传递
                putchar(ch);
                break;
            }
            case '%': {    // 转义%符号
                putchar('%');
                break;
            }
            default: {     // 未知格式符
                putchar('?');
                break;
            }
        }
    }

    va_end(ap);  // 清理参数列表
}

// 调用示例:
// my_printf("ID:%d, Name:%s, Level:%c%%", 101, "Alice", 'A');
// 输出: ID:101, Name:Alice, Level:A%

1.5 注意事项

• 必须至少包含一个固定参数
• va_arg必须按实际类型调用(类型错误导致未定义行为)
• C99后支持va_copy复制参数列表


二、命令行参数解析:getopt()

2.1 核心功能

unistd.h中的getopt()函数提供命令行选项解析能力,支持带参数选项(如-f filename)、组合短选项(如-abc)等常见格式。

2.2 函数原型与全局变量

#include <unistd.h>

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

// 全局变量:
extern char *optarg;  // 当前选项的参数值
extern int optind;    // 下一个要处理的argv索引,在getopt返回-1时其值为第一个非选项或选项参数的索引
extern int optopt;    // 无效选项字符

选项字符串格式:

• 单个字符:表示无参数选项(如"a"对应-a)
• 字符后接冒号:必须带参数(如"f:"对应-f file)
• 字符后接双冒号:可选参数(非标准扩展,谨慎使用)

2.3 使用流程

int main(int argc, char **argv) {
    int opt;
    while ((opt = getopt(argc, argv, "hf:v")) != -1) {
        switch (opt) {
            case 'h':
                printf("Help info\n");
                break;
            case 'f':
                printf("File: %s\n", optarg); // 获取参数
                break;
            case 'v':
                printf("Version 1.0\n");
                break;
            case '?': // 未知选项
                printf("Unknown option: %c\n", optopt);
                break;
        }
    }
    // 处理剩余参数(非选项参数)
    for (int i = optind; i < argc; i++) {
        printf("Extra argument: %s\n", argv[i]);
    }
    return 0;
}

2.4 运行示例

$ ./demo -f config.txt -v input.txt
File: config.txt
Version 1.0
Extra argument: input.txt

2.5 注意事项

• 选项参数通过optarg获取
• 遇到非选项参数时停止解析(可用--强制结束选项解析)
• 重复调用getopt()继续解析时需重置optind = 1


三、总结对比

特性stdarggetopt
主要用途函数内部处理可变参数解析命令行选项
核心操作va_start/va_arg/va_end宏getopt()函数循环调用
典型场景自定义格式化输出、数学计算开发命令行工具
参数类型安全无(依赖开发者保证)选项字符预先定义
跨平台性标准C库支持POSIX系统支持(Windows需额外实现)

掌握这两个工具,能够显著提升C语言函数设计的灵活性和命令行工具的开发效率。实际使用时需注意参数类型匹配与错误处理,避免因类型错误导致程序崩溃。

- 阅读全文 -
C/C++Unix/Linux应用层程序设计

对底层IO的深度总结

2025-03-03 浏览量 320 评论数 1

对底层IO的深度总结

前言

本文介绍 stdio 库函数与系统调用 read/write 的核心行为,重点解析缓冲机制对 I/O 操作的影响。


一、read 系统调用

功能:原子地从内核缓冲区(内存)读取数据块到用户缓冲区(内存)。原子——多进程同时调用read也会保证每个进程读取到的数据块是完整的。

具体表格:

I/O类型是否启用O_NONBLOCK数据/状态条件read行为
标准输入否输入未就绪(如终端无输入)阻塞,直到输入数据或EOF
标准输入是输入未就绪立即返回EAGAIN错误
标准输入无所谓输入已就绪(用户键入回车读取实际输入字节数(≤请求字节数)
标准输入无所谓输入结束(如Ctrl+D)返回0(EOF)
普通文件无所谓文件未结束读取请求的字节数(除非文件剩余字节不足,返回实际剩余字节数)
普通文件无所谓已到文件结尾返回0(EOF)
管道/FIFO否管道中无数据,但写入端未关闭阻塞,直到有数据写入
管道/FIFO否管道中无数据,且写入端已关闭返回0(EOF)
管道/FIFO否管道中有数据(≤请求字节数)读取实际可用字节数
管道/FIFO是管道中无数据,但写入端未关闭立即返回EAGAIN错误
管道/FIFO是管道中有数据(≤请求字节数)读取实际可用字节数
套接字否无数据到达(面向连接,如TCP)阻塞,直到数据到达或连接关闭
套接字否连接关闭(如TCP收到FIN)返回0(EOF)
套接字否数据到达(≤请求字节数)读取实际到达字节数
套接字是无数据到达立即返回EAGAIN错误
套接字是数据到达(≤请求字节数)读取实际到达字节数

总结
read 何时阻塞:

  • 1.未设置O_NONBLOCK,
  • 2.对象是支持阻塞的设备(终端设备、套接字、管道等)而非普通文件,
  • 3.此时内核缓冲区恰好没有数据。

阻塞需同时满足上述三种情况。可以理解为read会等待可能提供数据的设备。其余情况,内核缓冲区一旦有数据块被write写入就一次性搬走并返回数据量,没读到就返回0(EOF/设备关闭)或-1(发生错误)。


二、write 系统调用

功能:原子地将用户缓冲区(内存)的数据块写入内核缓冲区(内存)。原子——多进程同时调用write也能保证每个进程写的数据块是独立的。

对象类型是否启用 O_NONBLOCK行为描述
标准输出未启用如果标准输出是终端设备,write 会阻塞,直到数据被读取或终端缓冲区满。
启用如果标准输出是终端设备,write 会立即返回。如果终端缓冲区满,返回 -1 并设置 errno 为 EAGAIN。
普通文件未启用write 会立即将数据写入文件,不会阻塞。
启用write 会立即将数据写入文件,不会阻塞。
管道/FIFO未启用如果写入字节数 n ≤ PIPE_BUF,write 会原子地写入n字节;如果 n > PIPE_BUF,write不能保证原子性。
启用如果写入字节数 n ≤ PIPE_BUF,write 会原子地写入n字节;如果 n > PIPE_BUF,返回 -1 并设置 errno 为 EAGAIN。
套接字未启用如果套接字缓冲区有足够空间,write 会立即写入;否则会阻塞,直到有足够空间。
启用如果套接字缓冲区有足够空间,write 会立即写入;否则返回 -1 并设置 errno 为 EAGAIN。
读取端关闭未启用如果写入对象是管道、FIFO 或套接字,且读取端关闭,write 会返回 -1 并设置 errno 为 EPIPE,同时可能会产生 SIGPIPE 信号。
启用如果写入对象是管道、FIFO 或套接字,且读取端关闭,write 会返回 -1 并设置 errno 为 EPIPE,同时可能会产生 SIGPIPE 信号。

关键点:

  • write 返回仅表示数据块已提交给内核,不保证已写入磁盘。
  • 阻塞模式下,若内核缓冲区满,write 会阻塞;非阻塞模式直接返回 EAGAIN。

三、stdio 库的缓冲机制

1. 输入类函数(如 scanf)

  • 何时调用 read:当用户缓冲区无足够数据时触发 read。
  • 缓冲影响:

    • 若有用户缓冲区:read 填充库缓冲区,数据按需提取(如 scanf 遇空格/换行停止)。
    • 若无缓冲区(_IONBF):read 直接填充用户指定内存。

示例对比:

// 例1:关闭缓冲区(_IONBF)
setvbuf(stdin, NULL, _IONBF, 0);
scanf("%s", string);  // read直接填充string,"world"留在内核缓冲区
// 例2:启用缓冲区(默认)
scanf("%s", string);  // read填充库缓冲区,"world"保留在库缓冲区中

2. 输出类函数(如 printf)

  • 何时调用 write:由缓冲模式决定:

    缓冲模式触发条件
    无缓冲(_IONBF)立即调用 write
    行缓冲(_IOLBF)遇到换行符或缓冲区满
    全缓冲(_IOFBF)缓冲区满或手动刷新(fflush)

示例对比:

// 行缓冲(默认)
printf("I'm A.\n");  // 遇换行符立即调用write
write(STDOUT_FILENO, "I'm B.\n", 7);  // 直接输出
// 输出顺序:A → B
// 全缓冲(_IOFBF)
printf("I'm A.\n");  // 缓冲区未满,不调用write
write(STDOUT_FILENO, "I'm B.\n", 7);  // 直接输出
// 输出顺序:B → A(程序退出时刷新缓冲区)

四、close的机制

对于进程打开一个文件而言,有三个关联的数据结构:进程层面的文件描述符表,内核层面的打开文件表,文件系统层面的inode表。
进程层面的文件描述符表记录了fd与fd指向的内核的打开文件表的文件描述(句柄);
内核层面的打开文件表的每个表项(即文件句柄)记录了当前文件IO操作的偏移量,以及文件在文件系统inode表中的位置(下标);
文件系统层面的inode表记录了每个文件的元数据,以及记录锁(系统调用fcntl()维护的POSIX锁)。

close(int fd)的作用就是断开文件描述符fd与文件描述(内核维护的打开文件表的表项)的指向关系,并在进程层面的文件描述符表删除这一表项。而内核层面的打开文件表的表项只有没有任何进程的文件描述符指向它的时候才会被删除。


总结

  • read/write 就是一个搬运者,可阻塞,也可立即返回。
  • stdio 封装了read与write,为用户进程建立缓冲区:

    • 输入类函数:缓冲决定数据暂存位置(库缓冲区或用户内存)。
    • 输出类函数:缓冲决定 write 的触发条件(换行符、缓冲区满或无缓冲)。

- 阅读全文 -
  1. 1
  2. ...
  3. 4
  4. 5
  5. 6
  6. 7

浏览量 : 11276

© 2025 It's Geek KingYoungy. Power By Typecho . Theme by Shiyi

浙ICP备2025160639号  |  浙公网安备33020502001222号

This is just a placeholder img.