进程间通信

进程之间通讯

信号

信号是一种通过软件模拟硬件中断的实现方式,中断的优先级是最高的。

信号源

可以通过 kill -l 命令查看所有信号源。

  • ctrl + c : SIGINT => 2
  • ctrl + \ : SIGQUIT => 3
  • abort : 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 函数等待闹钟信号。当闹钟信号到达时,程序将打印消息并退出。

定时器

更精确的时间控制可以通过 getitimersetitimer 函数来实现。

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;
}

管道通讯

管道通讯是一种在进程间传输数据的方式,分为匿名管道和有名管道两种。

匿名管道

匿名管道是一种没有名字的管道,只能用于父子进程或兄弟进程之间的通讯。

特点

  • 半双工模式:管道一端只能用于写入数据,另一端用于读取数据。
  • 异步通知:写入数据后,读取端可以立即或稍后读取数据。

分类

  1. 单工:管道只能发送或接收数据。
  2. 半双工:管道一端发送数据,另一端接收数据。
  3. 全双工:管道两端可以同时发送和接收数据。

创建匿名管道

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 信号。

管道读写规则

  1. 当没有数据可读时
    • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
    • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
  2. 当管道满的时候
    • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  3. 如果所有管道写端对应的文件描述符被关闭,则read返回0
  4. 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  5. 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  6. 当要写入的数据量大于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_OKW_OKX_OKF_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;
}
  1. shmid:

    • 类型:int
    • 描述:共享内存标识符。这是通过 shmget 函数成功创建共享内存段后返回的标识符。它唯一地标识了系统中的共享内存段。
  2. cmd:

    • 类型:int
    • 描述:要执行的命令。以下是一些常用的命令值:
      • IPC_STAT: 获取共享内存段的 shmid_ds 结构,并将其存储在 buf 指向的位置。
      • IPC_SET: 设置共享内存段的 shmid_ds 结构中的某些成员。要修改的成员必须通过 buf 指针传递。
      • IPC_RMID: 销毁共享内存段。当最后一个进程解除与共享内存段的连接时,该段将被删除。
      • IPC_INFO: 返回有关系统范围内的共享内存限制和参数的信息。
      • SHM_INFO: 返回有关系统范围内的共享内存使用情况的信息。
  3. 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: 最后一次执行 shmatshmdt 操作的进程 ID。
      • shm_nattch: 当前连接到共享内存段的进程数。

当使用 shmctl 时,根据 cmd 的不同,buf 参数的用法也不同。以下是两种常见情况:

  • 如果 cmdIPC_RMID

    • buf 应该是 NULL,因为不需要获取或设置任何属性。这个命令用于销毁共享内存段。
  • 如果 cmdIPC_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_STATIPC_SETIPC_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。
      • 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。
  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值