一、进程概念
ps -elf:查看操作系统的所有进程(Linux命令)
ctrl + z:把进程切换到后台
crtl + c:结束进程
fg:把进程切换到前台
二、获取进程进程号和父进程号
函数原型:
pid_t getpid(void); //pid_t,它是一个有符号整数类型。
pid_t getppid(void);
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid = getpid();
printf("当前进程的进程号为:%d\n", pid);
pid_t ppid = getppid();
printf("当前进程的父进程为:%d\n", ppid);
while(1);
return 0;
}
三、 fork
概念:fork() 是一个在操作系统编程中常用的函数,用于创建一个新的进程。它通过复制调用进程(称为父进程)来创建一个新的进程(称为子进程)。子进程是父进程的副本,它从 fork() 函数返回的地方开始执行。
以下是 fork() 函数的原型:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
fork() 函数没有参数,它返回一个 pid_t 类型的值,表示进程的状态。返回值有以下几种情况:
- 如果返回值是负数(-1),则表示创建子进程失败。
- 如果返回值是零(0),则表示当前代码正在子进程中执行。
- 如果返回值是正数,则表示当前代码正在父进程中执行,返回值是新创建子进程的PID。
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
else if(pid == 0)
{
printf("child pid=%d, getpid=%d, getppid=%d\n", pid, getpid(), getppid());
// while(1)
// {
printf("child\n");
sleep(1);
// }
}
else
{
printf("parent pid=%d, getpid=%d, getppid=%d\n", pid, getpid(), getppid());
// while(1)
// {
printf("parent\n");
sleep(2);
// }
}
printf("helloworld\n");//会输出两次
return 0;
}
四、fork笔试题
详情看下述代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
for(int i = 0; i < 2; i++)
{
fork();
// printf("-\n"); //6个"-",换行符会输出缓冲区里的的数据
printf("-"); // 8个"-",子进程会复制父进程输出缓冲区的数据
}
return 0;
}
五、fork原理
下面输出都为1的原因是,父子进程在不同的空间
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int num = 0;
if(fork() == 0)
{
num++;
printf("child %d\n", num);
}
else
{
num++;
printf("parent %d\n", num);
}
/*
输出为:
child 1
parent 1
*/
return 0;
}
六、多进程读写
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void child_write(int fd)
{
char buf[128] = {0};
while(1)
{
scanf("%s", buf);
if(write(fd, buf, strlen(buf)) == -1)
{
perror("write");
break;
}
lseek(fd, -1 * strlen(buf), SEEK_CUR);
if(!strcmp(buf, "bye"))
break;
memset(buf, 0, 128);
}
//i lseek(fd, -1 * strlen(buf), _CUR);
}
void parent_read(int fd)
{
char buf[128] = {0};
while(1)
{
int ret = read(fd, buf, sizeof(buf));
if(ret == -1)
{
perror("read");
break;
}
else if(ret == 0)
continue;
if(!strcmp(buf, "bye"))
break;
printf("child get: %s\n", buf);
memset(buf, 0, sizeof(buf));
}
}
int main()
{
int fd = open("hello.txt", O_CREAT | O_RDWR, 00400 | 00200);
if(-1 == fd)
{
perror("open");
exit(1);
}
if(fork() == 0)
{
child_write(fd);
}
else
{
parent_read(fd);
}
close(fd);
return 0;
}
七、vfork
vfork 是一个在某些操作系统中提供的系统调用函数,用于创建一个新的进程,并与父进程共享内存空间。与 fork 不同的是,vfork 在创建子进程时不会复制父进程的内存空间,而是与父进程共享同一份内存。这使得 vfork 函数比 fork 函数更高效,因为它不需要复制整个父进程的内存空间。
vfork 函数的语法如下:
#include <unistd.h>
pid_t vfork(void);
返回值:vfork 函数没有参数,返回一个进程ID(PID)。在父进程中,vfork 返回子进程的PID;在子进程中,vfork 返回0。如果 vfork 调用失败,返回-1。
注意事项:
- 子进程的执行:在调用 vfork 后,子进程会暂停父进程的执行,直到子进程调用 exec 函数族中的一个函数或者调用 _exit 函数来终止自己。子进程在执行期间与父进程共享同一份内存空间,因此需要谨慎处理共享资源的访问,以避免出现竞争条件和数据损坏等问题。
- 父进程的阻塞:在调用 vfork 后,父进程会阻塞,直到子进程调用 exec 函数族中的一个函数或者调用 _exit 函数,或者导致异常终止。
- 返回值的使用:根据 vfork 的返回值可以判断当前代码是在父进程还是子进程中执行。在父进程中,返回的是子进程的PID;在子进程中,返回的是0。可以根据这个返回值来区分父子进程的执行路径。
- vfork创建的子进程需要指定退出方式
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid = vfork();
if(pid == -1)
{
perror("vfork");
exit(1);
}
else if(pid == 0) //子进程
{
printf("pid = %d, getpid = %d, getppid = %d\n", pid, getpid(), getppid());
sleep(2);
exit(0);
}
else //父进程
{
printf("pid = %d, getpid = %d, getppid = %d\n", pid, getpid(), getppid());
}
return 0;
}
八、exec系列函数
execl
execl 是一个系统调用函数,用于在当前进程中执行一个新的程序。它会取代当前进程的代码和数据,加载并执行指定的程序文件。
execl 函数的原型如下:
#include <unistd.h>
int execl(const char *path, const char *arg0, ..., (char *) NULL);
参数说明:
path:要执行的程序文件的路径。
arg0:新程序的第一个参数,通常是程序的名称。后续参数是新程序的命令行参数,以 NULL 结尾。
注意事项:当调用 execl 函数成功时,当前进程的代码和数据将被替换,之后的代码将不再执行。因此,如果在 execl 调用之后还有需要执行的代码,应该将其放在 execl 调用之前。
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
if(vfork() == 0)
{
printf("child: pid=%d\n", getpid());
//新的进程一旦启动,父进程就开始执行
execl("/usr/bin/cp", "cp", "-r", "/usr/local", ".", NULL);
printf("hello world!");//不会输出,因为执行了execl就不再执行下面的代码了
}
else
{
printf("parent: pid=%d\n", getpid());
}
return 0;
}
拓展:
另外,execl 函数还有一些变种,如 execlp、execle、execv 等,它们在参数传递和执行方式上有所不同。可以根据具体的需求选择合适的函数来执行新程序。
九、孤儿进程 & 僵尸进程
孤儿进程:孤儿进程(Orphan Process)是指在父进程结束或被终止后,其子进程仍然在运行但失去了父进程的监管和控制。
孤儿进程的状态和行为有以下特点:
- 孤儿进程的父进程 ID(PPID)被设置为 init 进程的进程 ID(通常是 1)。
- 孤儿进程继续在系统中运行,但其父进程已经不存在。
- 孤儿进程的资源(如打开的文件描述符、内存等)不会被释放,因为它没有被正常地回收。
- 孤儿进程的终止状态(退出状态码)将被保存,直到父进程通过调用 wait 或 waitpid 等系统调用来获取。
-
孤儿进程的存在是为了避免子进程在父进程终止后变成僵尸进程(Zombie Process)。当父进程没有及时处理子进程的终止状态时,子进程将变成僵尸进程,占用系统资源。而孤儿进程的终止状态会被保存,直到被新的父进程处理。
-
在编写程序时,可以通过一些方式避免产生孤儿进程,例如在父进程终止之前等待子进程的终止,或者使用适当的进程管理和通信机制来确保子进程的正确终止和资源回收。
僵尸进程:僵尸进程(Zombie Process)是指一个已经终止执行的子进程,但其父进程尚未对其进行完全的资源回收和终止状态获取的进程。
僵尸进程的状态和行为有以下特点:
- 僵尸进程的状态(进程状态码)为 “Z” 或 “Z+”,在进程列表中以 “” 或 “Z” 标识。
- 僵尸进程的父进程仍然存在,但尚未调用相应的系统调用(如 wait 或 waitpid)来获取子进程的终止状态。
- 僵尸进程的资源(如打开的文件描述符、内存等)几乎没有消耗,因为它已经停止执行。
- 僵尸进程的终止状态(退出状态码)仍然保存在系统中,等待父进程来获取。
- 僵尸进程的存在是因为在 Linux 系统中,子进程的终止状态需要被父进程显式地获取。父进程可以通过调用 wait、waitpid 或 waitid 等系统调用来获取子进程的终止状态,并进行相应的资源回收。如果父进程没有及时处理子进程的终止状态,子进程就会变成僵尸进程。
- 僵尸进程一般不会对系统的正常运行产生直接影响,但如果大量的僵尸进程积累,可能会占用系统的进程表资源。因此,及时处理僵尸进程是良好的编程实践。
在编写程序时,可以通过以下方式避免僵尸进程的产生:
- 在父进程中使用 wait、waitpid 或 waitid 等系统调用来获取子进程的终止状态。
- 使用信号处理机制,在父进程中捕获子进程的终止信号(如 SIGCHLD),并在信号处理函数中处理子进程的终止状态。
- 使用进程间通信机制(如管道、信号量、共享内存等)来实现父进程与子进程之间的同步和通信,确保子进程的正确终止和资源回收。
例子:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
if(fork() == 0)
{
sleep(1);
printf("child: pid = %d, ppid = %d\n", getpid(), getppid());
exit(100);
}
else
{
printf("parent: pid = %d\n", getpid());
int status;
wait(&status);
if(WIFEXITED(status)) //判断子进程是否正常结束
{
printf("子进程正常结束\n");
printf("子进程退出状态:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程异常退出\n");
}
}
return 0;
}
十、进程间通信
1.无名管道
概念:无名管道(unnamed pipe)是一种在进程间进行单向通信的机制。它是一种特殊的文件,可以用于在同一台计算机上的父进程和子进程之间传递数据。
注意事项:
-
无名管道只能在具有亲缘关系的进程之间使用,例如父进程和子进程。创建无名管道时,操作系统会为其分配一个文件描述符,父进程和子进程可以使用该文件描述符进行读取和写入操作。
-
无名管道是一种半双工的通信机制,意味着数据只能在一个方向上流动。通常,父进程创建无名管道,并将其传递给子进程,然后父进程关闭管道的写入端,子进程关闭管道的读取端。这样,父进程就可以向子进程发送数据,而子进程可以从管道中读取数据。
pipe:
在C语言中,pipe() 函数用于创建一个无名管道,并返回两个文件描述符,一个用于读取数据,另一个用于写入数据。
函数原型如下:
#include <unistd.h>
int pipe(int pipefd[2]);
参数 pipefd 是一个整型数组,用于存储管道的读取端和写入端的文件描述符。pipefd[0] 表示管道的读取端,pipefd[1] 表示管道的写入端。
例子:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int fd[2] = {0};
if(pipe(fd) == -1)
{
perror("pipe");
exit(1);
}
int num = 0;
if(fork() == 0) //子进程写数据
{
num++;
if(write(fd[1], &num, 4) == -1)
{
perror("write");
exit(1);
}
printf("child: num = %d\n", num);
}
else //父进程读数据
{
if(read(fd[0], &num, 4) == -1) //阻塞,如果管道为空,则程序停在这,直到有数据才可读
{
perror("read");
exit(1);
}
num++;
printf("parent: num = %d\n", num);
int status;
wait(&status);
}
return 0;
}
2.有名管道
概念:有名管道是一种特殊类型的文件,可以用于在不同的进程之间进行双向通信。它可以被多个进程同时打开,允许一个进程向管道写入数据,而另一个进程从管道中读取数据。
要创建一个有名管道,在Unix系统上可以使用mkfifo命令。
mkfifo
mkfifo()函数是在C语言中使用的一个系统调用,用于创建一个有名管道(FIFO)。以下是mkfifo()函数的原型:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
其中:
- pathname 是要创建的有名管道的路径名。
- mode 是要设置的权限模式。
返回值:
mkfifo()函数返回一个整数值,表示操作的成功与否。如果成功创建了有名管道,则返回0;如果出现错误,则返回-1,并设置errno变量来指示具体的错误原因。
例子:
进程读:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
//创建有名管道
if(mkfifo("fifo.tmp", 00400 | 00200) == -1)
{
perror("mkfifo");
exit(1);
}
int fd = open("fifo.tmp", O_RDONLY);
if(-1 == fd)
{
perror("open");
exit(2);
}
char buf[128] = {0};
while(1)
{
if(read(fd, buf, 128) == -1)
{
perror("read");
exit(3);
}
if(!strcmp(buf, "bye"))
{
break;
}
printf("%s\n", buf);
memset(buf, 0, 128);
}
close(fd);
unlink("fifo.tmp"); //删除有名管道
return 0;
}
进程写:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int fd = open("fifo.tmp", O_WRONLY);
if(-1 == fd)
{
perror("open");
exit(1);
}
char buf[128] = {0};
while(1)
{
scanf("%s", buf);
if(write(fd, buf, strlen(buf)) == -1)
{
perror("write");
exit(2);
}
if(!strcmp(buf, "bye"))
break;
memset(buf, 0, 128);
}
close(fd);
return 0;
}
3.信号
1.kill
#include <signal.h>
int kill(pid_t pid, int sig);
参数:
- pid 是要发送信号的进程的进程号,
- sig 是要发送的信号编号。
返回值:
- 如果成功发送信号,则返回 0。
- 如果发送信号失败,则返回 -1,并设置合适的错误码,可以通过 errno 变量获取具体的错误信息。
例子:
#include <stdio.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
int main()
{
//kill(getpid(), SIGINT); //给进程本身发送SIGINT信号
raise(2); //只能给进程本身发信号
while(1);
}
~
2. signal
signal() 函数用于设置信号处理程序,以定义在接收到特定信号时要执行的操作。它的语法如下:
#include <signal.h>
//最后的(int)代表了函数自己传参,不需要自己动手
void (*signal(int sig, void (*handler)(int)))(int);
参数:
- sig 是要设置处理程序的信号编号
- handler 是一个指向函数的指针,该函数用于处理接收到信号时要执行的操作。
例子:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int sig)
{
printf("get %d\n", sig);
}
void handler2(int sig)
{
printf("get %d\n", sig);
alarm(2);
}
int main()
{
signal(SIGINT, SIG_IGN); //忽略SIGINT信号,SIGKILL和SIGSTOP不能被忽略
signal(SIGHUP, handler); //收到SIGHUP信号,调用handler函数
alarm(2); //2秒后进程本身发送SIGALRM信号
signal(SIGALRM, handler2);
while(1);
return 0;
}
拓展(handler函数sig的来源):
在 signal() 函数调用中,我们指定了要使用的信号处理函数,例如 signal(SIGHUP, handler),并将 handler 函数作为参数传递给 signal() 函数。当程序接收到 SIGHUP 信号时,操作系统会调用 handler 函数,并将接收到的信号编号作为参数传递给它。
因此,在 handler 函数中,你可以使用 sig 参数来访问接收到的信号编号,以便根据需要执行相应的操作。
4.消息队列
消息队列是一种进程间通信(IPC:IPC 是 Inter-Process Communication(进程间通信)的缩写)机制,用于在不同进程之间传递数据。它提供了一种异步、解耦的通信方式,允许发送方将消息放入队列,而接收方则从队列中获取消息。
消息队列通常由操作系统内核维护,它具有以下特点:
-
队列结构:消息队列是一个先进先出(FIFO)的队列结构,保证了消息的顺序性。
-
异步通信:发送方将消息放入队列后,可以立即继续执行其他任务,而不需要等待接收方的处理。接收方可以在合适的时机从队列中获取消息进行处理。
-
解耦性:消息队列实现了发送方和接收方的解耦,它们可以独立地进行操作,无需直接交互。这使得系统的各个组件可以更加灵活和独立地进行开发和维护。
-
缓冲能力:消息队列可以作为缓冲区,允许发送方在接收方暂时无法处理消息时将消息保存在队列中,避免数据丢失。
-
多对多通信:多个发送方可以同时向一个消息队列发送消息,多个接收方也可以从同一个消息队列接收消息,实现了多对多的通信模式。
在使用消息队列时,通常需要以下几个关键操作:
-
创建消息队列:使用系统调用(如 msgget)创建一个新的消息队列,并返回一个唯一的标识符。
-
发送消息:发送方使用系统调用(如 msgsnd)将消息放入队列中,需要指定目标队列的标识符、消息类型和消息数据。
-
接收消息:接收方使用系统调用(如 msgrcv)从队列中获取消息,可以指定要接收的消息类型和接收缓冲区。
-
删除消息队列:使用系统调用(如 msgctl)删除不再需要的消息队列。
1. msgget
msgget 是一个系统调用函数,用于创建或获取一个消息队列的标识符。它的原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数说明:
- key:用于标识消息队列的键值。可以使用 ftok 函数生成一个键值,也可以使用一个已存在的键值。不同的键值对应不同的消息队列。
- msgflg:用于指定创建或获取消息队列的标志位。可以使用不同的标志位来控制消息队列的创建、获取和权限等。
返回值:
- 成功时,返回一个非负整数,表示消息队列的标识符(也称为消息队列的描述符)。
- 失败时,返回 -1,并设置相应的错误码。
下面是 msgget 函数的常见用法:
- 创建新的消息队列:
key_t key = ftok("path_to_file", 'A'); // 生成键值
int msgid = msgget(key, IPC_CREAT | 0666); // 创建消息队列
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}
上述代码使用 ftok 函数生成一个键值,然后调用 msgget 函数创建一个新的消息队列。IPC_CREAT 标志位用于创建新的消息队列,0666 表示设置消息队列的权限为读写权限。
- 获取已存在的消息队列:
key_t key = ftok("path_to_file", 'A'); // 生成键值
int msgid = msgget(key, 0); // 获取已存在的消息队列
if (msgid == -1) {
perror("msgget");
exit(EXIT_FAILURE);
}
上述代码使用 ftok 函数生成一个键值,然后调用 msgget 函数获取已存在的消息队列。传递参数为 0 表示不创建新的消息队列,只获取已存在的消息队列。
2.msgsnd
msgsnd 是一个系统调用函数,用于向消息队列发送消息。它的原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数说明:
- msqid:消息队列的标识符,即消息队列的描述符,通过 msgget 函数获取。
- msgp:指向要发送的消息的指针,通常是一个结构体指针,结构体中包含了消息的内容。
- msgsz:要发送的消息的大小(以字节为单位)。
- msgflg:用于指定发送消息的标志位,可以控制消息发送的行为和选项。
返回值:
- 成功时,返回 0。
- 失败时,返回 -1,并设置相应的错误码。
3. msgrcv
msgrcv 是一个系统调用函数,用于从消息队列接收消息。它的原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数说明:
- msqid:消息队列的标识符,即消息队列的描述符,通过 msgget 函数获取。
- msgp:指向接收消息的缓冲区的指针,通常是一个结构体指针,结构体中用于存储接收到的消息内容。
- msgsz:接收消息缓冲区的大小(以字节为单位)。
- msgtyp:指定要接收的消息类型。如果 msgtyp 大于 0,则接收第一个类型为 msgtyp 的消息;如果 msgtyp 等于 0,则接收第一个消息;如果 msgtyp 小于 0,则接收类型小于或等于 msgtyp 绝对值的最小消息。
- msgflg:用于指定接收消息的标志位,可以控制接收消息的行为和选项。
返回值:
- 成功时,返回接收到的消息的大小(以字节为单位)。
- 失败时,返回 -1,并设置相应的错误码。
4.msgctl
msgctl 是一个系统调用函数,用于对消息队列进行控制操作,如创建、删除、获取属性等。它的原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数说明:
- msqid:消息队列的标识符,即消息队列的描述符,通过 msgget 函数获取。
- cmd:指定要执行的操作类型,可以是以下值之一:
- IPC_STAT:获取消息队列的属性,将属性信息存储在 buf 指向的 msqid_ds 结构体中。
- IPC_SET:设置消息队列的属性,将 buf 指向的 msqid_ds 结构体中的属性值应用到消息队列。
- IPC_RMID:删除消息队列。
- buf:指向 msqid_ds 结构体的指针,用于存储消息队列的属性信息或设置消息队列的属性。
返回值:
- 成功时,返回 0。
- 失败时,返回 -1,并设置相应的错误码。
5.综合练习
消息的发送:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
#define MSGKEY 1000
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
//创建消息队列,IPC_CREAT 标志用于创建新的消息队列,这里使用了 IPC_EXCL 标志,如果消息队列已存在,则返回错误。
int msgid = msgget(MSGKEY, IPC_CREAT | IPC_EXCL);
if(-1 == msgid)
{
perror("msgget");
exit(1);
}
struct msgbuf m;
while(1)
{
scanf("%s", m.mtext);
m.mtype = 1; //消息类型
if(msgsnd(msgid, &m, sizeof(m.mtext), 0) == -1)
{
perror("msgsnd");
break;
}
if(!strcmp(m.mtext, "bye"))
break;
memset(&m, 0, sizeof(m));
}
//删除消息队列
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
消息的接收:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <stdlib.h>
#define MSGKEY 1000
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main()
{
//获取消息队列
int msgid = msgget(MSGKEY, 0);
if(-1 == msgid)
{
perror("msgget");
exit(1);
}
struct msgbuf m;
while(1)
{
m.mtype = 1; //消息类型
if(msgrcv(msgid, &m, sizeof(m.mtext), 1, 0) == -1)
{
perror("msgrcv");
break;
}
if(!strcmp(m.mtext, "bye"))
break;
printf("%s\n", m.mtext);
memset(&m, 0, sizeof(m));
}
return 0;
}
5.共享内存
概念: 共享内存(Shared Memory)是一种在多个进程之间共享数据的机制。它允许多个进程访问同一块内存区域,从而实现进程间的数据共享和通信。
在操作系统中,共享内存通过将一块内存映射到多个进程的地址空间来实现。这样,这些进程就可以直接读写这块共享内存,而无需通过其他的进程间通信机制(如管道、消息队列等)来传递数据。
共享内存的使用通常包括以下步骤:
- 创建共享内存:首先,需要创建一块共享内存区域。这可以通过操作系统提供的共享内存函数(如shmget())来完成。在创建时,需要指定共享内存的大小和权限等参数。
- 连接共享内存:创建共享内存后,进程需要连接到这块共享内存,使其在进程的地址空间中可访问。这可以通过共享内存函数(如shmat())来完成。连接共享内存时,需要指定共享内存的标识符。
- 访问共享内存:一旦连接到共享内存,进程就可以像访问普通内存一样访问共享内存。可以使用指针或其他适当的数据结构来读写共享内存中的数据。
- 分离共享内存:当进程不再需要访问共享内存时,应该将其与共享内存的连接断开,这可以通过共享内存函数(如shmdt())来完成。分离共享内存后,进程将无法再访问共享内存中的数据。
- 删除共享内存:当所有使用共享内存的进程都完成了对共享内存的访问后,可以使用共享内存函数(如shmctl())来删除共享内存。删除共享内存将释放相关的系统资源。
1.shmget
shmget() 是一个 POSIX 共享内存函数,用于创建或打开一个共享内存对象。
下面是 shmget() 函数的原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数说明:
- key:共享内存的键值,用于唯一标识共享内存对象。可以使用 - ftok() 函数生成键值,也可以使用已知的键值。
- size:共享内存的大小,以字节为单位。
- shmflg:标志位,用于指定共享内存的访问权限和行为。可以使用 IPC_CREAT 标志创建共享内存,也可以与其他标志位进行按位或操作。
返回值:
- 成功时,返回共享内存的标识符(非负整数),用于后续的共享内存操作。
- 失败时,返回 -1,并设置 errno 错误码来指示具体错误原因。
2.shmat
shmat() 是一个 POSIX 共享内存函数,用于将共享内存对象连接到进程的地址空间。
下面是 shmat() 函数的原型:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
- shmid:共享内存的标识符,由 shmget() 返回。
- shmaddr:指定共享内存连接的地址。通常设置为 NULL,让操作系统自动选择一个合适的地址。
- shmflg:标志位,用于指定共享内存的连接方式。可以使用 SHM_RDONLY 标志以只读方式连接共享内存,也可以与其他标志位进行按位或操作。
返回值:
- 成功时,返回指向共享内存连接的指针。
- 失败时,返回 -1,并设置 errno 错误码来指示具体错误原因。
3.shmctl
shmctl() 是一个 POSIX 共享内存函数,用于控制共享内存对象的属性和操作。
下面是 shmctl() 函数的原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数说明:
- shmid:共享内存的标识符,由 shmget() 返回。
- cmd:控制命令,用于指定要执行的操作。常用的命令包括 IPC_STAT(获取共享内存的状态)、IPC_SET(设置共享内存的状态)和 IPC_RMID(删除共享内存)。
- buf:指向 struct shmid_ds 结构体的指针,用于存储或传递共享内存的状态信息。
返回值:
- 成功时,返回 0 或执行命令的特定返回值(例如 IPC_STAT 命令返回共享内存的状态信息)。
- 失败时,返回 -1,并设置 errno 错误码来指示具体错误原因。
4.综合练习:实现2个进程的共同读写num
include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <unistd.h>
#define SHMKEY 1000
#define SHMSIZE 4096
int main()
{
//创建共享内存
int shmid = shmget(SHMKEY, SHMSIZE, IPC_CREAT | IPC_EXCL);
if(-1 == shmid)
{
perror("shmget");
exit(1);
}
//映射,NULL表示系统自动分配一块空闲内存
void *addr = shmat(shmid, NULL, 0);
if((void *)-1 == addr)
{
perror("shmat");
exit(2);
}
int num = 100;
//写入共享内存
*(int *)addr = num;
while(1)
{
num = *(int *)addr;
if(num <= 0)
break;
printf("%d get %d\n", getpid(), num);
num--;
// usleep(100000); //要暂停指定的微秒数
*(int *)addr = num;
usleep(500000); //要暂停指定的微秒数
// sleep(1);
}
//删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#define SHMKEY 1000
#define SHMSIZE 4096
int main()
{
usleep(100000);
//获取共享内存
int shmid = shmget(SHMKEY, SHMSIZE, 0);
if(-1 == shmid)
{
perror("shmget");
exit(1);
}
//映射,NULL表示系统自动分配一块空闲内存
void *addr = shmat(shmid, NULL, 0);
if((void *)-1 == addr)
{
perror("shmat");
exit(2);
}
int num;
while(1)
{
num = *(int *)addr;
if(num <= 0)
break;
printf("%d get %d\n", getpid(), num);
num--;
// usleep(100000); //要暂停指定的微秒数
*(int *)addr = num;
usleep(500000); //要暂停指定的微秒数
// sleep(1);
}
return 0;
}
6.信号量
信号量(Semaphore)是一种用于多线程编程中的同步原语,用于控制对共享资源的访问。它是一个计数器,用于表示可用的资源数量。信号量可以用来解决多线程中的互斥和同步问题。
信号量有两种类型:二进制信号量和计数信号量。
- 二进制信号量(Binary Semaphore)也被称为互斥锁(Mutex),它的值只能为0或1。当值为1时,表示资源可用;当值为0时,表示资源不可用。二进制信号量常用于实现互斥访问共享资源的机制,即同一时间只允许一个线程访问资源。
- 计数信号量(Counting Semaphore)是一个整数计数器,可以有一个初始值,并且可以增加或减少。计数信号量常用于控制对一组资源的访问,例如有限数量的线程可以同时访问某个资源。
信号量的操作包括两个基本操作:P(等待)和V(释放)。
-
P(等待)操作用于申请资源。如果信号量的值大于0,则该线程可以继续执行,并将信号量的值减1;如果信号量的值等于0,则该线程将被阻塞,直到有其他线程释放资源。
-
V(释放)操作用于释放资源。当一个线程完成对共享资源的访问后,可以调用V操作来增加信号量的值,表示资源已经可用。
1. semget
semget 是一个系统调用函数,用于创建或获取一个信号量集合(Semaphore Set)。它的用法如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
参数说明:
- key:用于标识信号量集合的键值,可以使用 ftok 函数生成。
- nsems:信号量集合中的信号量数量。
- semflg:标志位,用于指定信号量集合的创建和访问权限,可以使用 IPC_CREAT、IPC_EXCL 等标志位的按位或操作。
- IPC_CREAT:如果指定的 key 对应的信号量集合不存在,则创建一个新的信号量集合。
- IPC_EXCL:与 IPC_CREAT 一起使用,如果指定的 key 对应的信号量集合已经存在,则返回错误。
- 0666:指定信号量集合的权限,这里表示读写权限为所有用户。
返回值:
- 成功时,返回一个非负整数,表示信号量集合的标识符(Semaphore ID)。
- 失败时,返回 -1,并设置 errno 错误码来指示具体错误原因。
semget 函数的主要功能是创建或获取一个信号量集合。如果指定的 key 对应的信号量集合已经存在,则返回该信号量集合的标识符。如果不存在,则根据给定的 key 创建一个新的信号量集合,并返回其标识符。
2.semctl
semctl 是一个系统调用函数,用于对信号量集合(Semaphore Set)进行控制操作。它可以用于获取信号量集合的信息、修改信号量的值以及删除信号量集合。semctl 的用法如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
参数说明:
- semid:信号量集合的标识符(Semaphore ID),由 semget 返回。
- semnum:信号量在集合中的索引,从0开始。
- cmd:控制命令,用于指定要执行的操作。常用的命令包括:
- GETVAL:获取指定信号量的值。
- SETVAL:设置指定信号量的值。
- IPC_RMID:删除信号量集合。
- …:可变参数,可以根据不同的命令传递不同的参数
返回值:
- 成功时,根据命令不同而有不同的返回值。例如,GETVAL 命令返回信号量的值,SETVAL 命令返回0。
- 失败时,返回 -1,并设置 errno 错误码来指示具体错误原因。
3.semop
semop 是一个系统调用函数,用于对信号量集合(Semaphore Set)进行 P(等待)和 V(释放)操作。它可以用于申请和释放信号量资源。semop 的用法如下:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
参数说明:
- semid:信号量集合的标识符(Semaphore ID),由 semget 返回。
- sops:指向一个 sembuf 结构体数组的指针,用于指定要执行的操作。
- nsops:sops 数组的大小,即要执行的操作的数量。
返回值:
- 成功时,返回0。
- 失败时,返回 -1,并设置 errno 错误码来指示具体错误原因。
sembuf 结构体定义如下:
struct sembuf {
unsigned short sem_num; // 信号量在集合中的索引
short sem_op; // 操作类型:负值表示 P 操作,正值表示 V 操作,0 表示等待直到信号量为0
short sem_flg; // 操作标志:常用的标志有 IPC_NOWAIT(非阻塞)和 SEM_UNDO(进程退出时自动释放)
};
sem_op参数:
- 负值(如 -1)表示 P(等待)操作,即申请资源。
- 正值(如 1)表示 V(释放)操作,即释放资源。
- 0 表示等待直到信号量的值为0。
sem_flg:
- IPC_NOWAIT:如果无法立即执行操作,则不等待,立即返回错误。
- SEM_UNDO:在进程退出时自动释放被操作的信号量。这样可以避免进程异常退出时信号量资源未被释放的问题。
4.综合练习:进程的同步
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#define SHMKEY 1000
#define SEMKEY 1000
#define SHMSIZE 4096
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
void sem_p(int semid)
{
struct sembuf s;
s.sem_num = 0; //表示第0个信号量
s.sem_op = -1; //减一操作(P操作)
s.sem_flg = SEM_UNDO; //如果进程异常退出,信号量会恢复初值(变为1)
if(semop(semid, &s, 1) == -1)
{
perror("semop");
}
}
void sem_v(int semid)
{
struct sembuf s;
s.sem_num = 0; //表示第0个信号量
s.sem_op = 1; //加一操作(V操作)
s.sem_flg = SEM_UNDO; //如果进程异常退出,信号量会恢复初值(变为1)
if(semop(semid, &s, 1) == -1)
{
perror("semop");
}
}
int main()
{
//创建共享内存
int shmid = shmget(SHMKEY, SHMSIZE, IPC_CREAT | IPC_EXCL);
if(-1 == shmid)
{
perror("shmget");
exit(1);
}
//映射,NULL表示系统自动分配一块空闲内存
void *addr = shmat(shmid, NULL, 0);
if((void *)-1 == addr)
{
perror("shmat");
exit(2);
}
//创建信号量,1表示创建一个信号量
int semid = semget(SEMKEY, 1, IPC_CREAT | IPC_EXCL);
if(-1 == semid)
{
perror("semget");
exit(3);
}
union semun s;
s.val = 1; //二值信号
//初始化信号量,0表示第一个信号量
if(semctl(semid, 0, SETVAL, s) == -1)
{
perror("semctl");
exit(4);
}
int num = 100;
//写入共享内存
*(int *)addr = num;
while(1)
{
sem_p(semid); //P操作
num = *(int *)addr;
if(num <= 0)
{
sem_v(semid); //V操作
break;
}
printf("%d get %d\n", getpid(), num);
num--;
// usleep(100000); //要暂停指定的微秒数
*(int *)addr = num;
usleep(100000); //要暂停指定的微秒数
// sleep(1);
sem_v(semid);//V操作
}
usleep(500000);
//删除共享内存
shmctl(shmid, IPC_RMID, NULL);
//删除信号量
semctl(semid, 0, IPC_RMID);
return 0;
}
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#define SHMKEY 1000
#define SHMSIZE 4096
#define SEMKEY 1000
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};
void sem_p(int semid)
{
struct sembuf s;
s.sem_num = 0; //表示第0个信号量
s.sem_op = -1; //减一操作(P操作)
s.sem_flg = SEM_UNDO; //如果进程异常退出,信号量会恢复初值(变为1)
if(semop(semid, &s, 1) == -1)
{
perror("semop");
}
}
void sem_v(int semid)
{
struct sembuf s;
s.sem_num = 0; //表示第0个信号量
s.sem_op = 1; //加一操作(V操作)
s.sem_flg = SEM_UNDO; //如果进程异常退出,信号量会恢复初值(变为1)
if(semop(semid, &s, 1) == -1)
{
perror("semop");
}
}
int main()
{
usleep(100000);
//获取共享内存
int shmid = shmget(SHMKEY, SHMSIZE, 0);
if(-1 == shmid)
{
perror("shmget");
exit(1);
}
//映射,NULL表示系统自动分配一块空闲内存
void *addr = shmat(shmid, NULL, 0);
if((void *)-1 == addr)
{
perror("shmat");
exit(2);
}
//获取信号量,1表示创建一个信号量
int semid = semget(SEMKEY, 1, 0);
if(-1 == semid)
{
perror("semget");
exit(3);
}
union semun s;
s.val = 1; //二值信号
int num = 100;
while(1)
{
sem_p(semid); //P操作
num = *(int *)addr;
if(num <= 0)
{
sem_v(semid); //V操作
break;
}
printf("%d get %d\n", getpid(), num);
num--;
// usleep(100000); //要暂停指定的微秒数
*(int *)addr = num;
usleep(100000); //要暂停指定的微秒数
// sleep(1);
sem_v(semid);//V操作
}
return 0;
}