C多进程
这篇文章主要是想针对多进程的创建和一些通信手段来进行一下记录
创建子进程
关于创建子进程的原型一般都是用的这个,直接fork,这个函数在父进程中调用,在父子进程中各有一个pid_t类型的返回值,父进程中得到的是子进程的ID,子进程中得到的是0值。当然调用失败就是-1。
//创建进程,然后复制出另一份进程
#include <unistd.h>
pid_t fork();
根据不同的fork返回值,父子进程可以分出自己专属的代码区域段。例子如下:
#include <stdio.h>
#include <unistd.h>
int i = 10;
int main() {
pid_t pid;
pid = fork();
if (pid == 0) {
i++;
printf("I' m the subprocess.The i:%d\n", i);
} else {
i--;
printf("I' m the parent process.The i:%d\n", i);
}
return 0;
}
一般来说,写代码的理想状态是最后的程序正常跑,更理想的就是完全不出错,不过那个太理想了。比如多进程程序中,当父进程结束了,子进程没有被父进程获取状态信息,从而使得进程号依然保留在系统中,占用系统定数的进程号;又比如父进程都结束运行了,子进程还在继续跑,由init进程来接管。这两种情况,前者被叫僵尸进程,后者被称为孤儿进程(这个概念其实我挺犯迷糊,如果有冲突那就是你对,记得提点一声)。所以,父进程在结束之前,要对子进程负责,要查询子进程的结束状态,并确保子进程跑完了才跑路。
wait一下
简单的方案,就是父进程一直等,实现这个功能的函数原型如下:
#include <sys/wait.h>
pid_t wait(int *statloc);
//配合使用的宏
WIFEXITED(statloc); //子进程正常终止,返回非0值
WEXITSTATUS(statloc); //子进程正常终止,返回退出码
WIFSIGNALED(statloc); //因为未捕获信号而终止,返回非0值
WTERMSIG(statloc); //配合前一个宏,返回信号值
WIFSTOPPED(statloc); //子进程意外终止,返回非0
WSTOPSIG(statloc); //子进程意外终止,返回信号值
上面函数的通用解读就是,wait函数的调用会阻塞父进程,一直等着子进程跑完返回状态信息到statloc才对父进程放行。而对于子进程的结束信息的解读,就是上面对应的宏来进行。不过wait的阻塞让很多人不满,所以他们实现了另一种wait:
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
使用waitpid处理僵尸进程:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
int status, i=0;
pid = fork();
if (pid == 0) {
i--;
printf("subprocess: %d\n", i);
sleep(5);
return 6;
} else {
//因为只有一个子进程,就不明确指定了
while (!waitpid(-1, &status, WNOHANG)) {
i++;
printf("parent process, %d sec\n", i);
sleep(1);
}
if (WIFEXITED(status))
printf("Subprocess was ended and return a value :%d\n", WEXITSTATUS(status));
}
return 0;
}
进程间通信
比较简单的通信方式,是创建管道,管道和socket套接字同属系统资源,创建了管道,就是使得两个管道在系统提供的内存进行通信。实现的原型如下:
#include <unistd.h>
int pipe(int filedes[2]);
所谓管道,是有着两个口子的,这里的管道也一样,filedes就是一个包含了两个文件描述符的数组,一般传入的这个参数是空的,函数调用结束后就成了新创建的管道的入口和出口。
嗯,所以这个管道的使用,其实就是这两个描述符的使用,filedes数组中,第一个是管道入口,第二个是管道出口,这个要注意。
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid;
int fds[2];
char str[20];
pipe(fds);
pid = fork();
if (pid != 0) {
write(fds[1], "balabala", sizeof("balabala"));
printf("parent process.\n");
sleep(3);
} else {
read(fds[0], str, 20);
printf("subprocess, get mes: %s\n", str);
}
return 0;
}
例子是父进程发送信息,子进程接收信息,实际上反过来也可以,不限定。但信息放进管道,父子进程其实都可以读取,就像写了信息在文本,谁都可以读取。管道的单向只体现在它的信息是从fds[1]进,fds[0]出。为了保证
信息的受众是对端从而实现双方通信,往往实现两个管道,然后一个管道负责发,一个负责收,这样就不需要预测运行流程。
管道是很便利,但它往往适用于关联进程(像父子进程),想要无关联的通信还需要其他机制,比如下面的3种System V IPC。
System V IPC
针对共享资源的多进程访问,这种独占式的访问会引发大问题,谁先谁后无法控制,这种引发竞争的代码段,被称为临界区。对进程的同步,就是确保进入临界区只有一个进程。
信号量
它是一个特殊的整数值变量,只支持两种操作,一个是取,一个是放,分别是P原语和V原语的解读。因为针对多进程同步和多线程同步都有信号量的概念,虽然语义一致,但实现不一样,姑且把多进程间信号量称为信号量,多线程间信号量称为POSIX信号量。对于信号量的初始化决定了其行为,但最常用的就是二进制信号量,用0和1来代表空置和占用的意义。linux中的实现,往往在sys/sem.h头文件中,三个系统调用设计成操作一组信号量而不是单个信号量,三个系统调用分别是semget、semop和semctl;而POSIX信号量的实现都在semaphore.h头文件中。
信号量的创建
#include <sys/sem.h>
//申请信号集,申请成功就返回信号量标记值,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
semget的参数key具有唯一性,num_sems则是申请的system V信号量集的信号量数,sem_flags制定了信号量的读写权限。在semget创建信号量成功后,相关联的内核数据结构体semid_ds也会被创建且初始化,具体存储的信息就是创建信号量集的进程的用户ID和组ID,以及信号量集的信号量数还有信号量的读写权限等。
信号量赋初值
具体操作需要依赖semctl函数:
#include <sys/sem.h>
int semctl(int sem_id, int sem_num, int command, ...)
sem_id,当然就是信号量集的标识符了,sem_num于信号量集的意义就像下标之于数组,是标记某某某信号量,command则是执行的命令了。因为这里要用它来赋初值,所以调用起来就是 semctl(sem_id, 0, SETVAL, sem_union),这个调用其实就是执行SETVAL指示的赋值操作,而sem_union就是携带着想要赋值给信号量的初值。不过先不对这个结合体做过多阐述。系统了解一下semop先:
#include <sys/sem.h>
int semop(int sem_id, struct sembuf *semops, size_t num_sem_ops);
semop函数是对信号量进行PV操作的关键,但具体如何改变要看传参semops,也就是sembuf这种结构体
struct sembuf {
unsigned short int sem_num; //对应信号量在信号量集中的索引
short int sem_op; //指定操作类型
short int sem_flag; //标志位
};
可选值为正整型、0和负整型的sem_op以及可选值为IPC_NOWAIT和SEM_UNDO的sem_flag配合起来就决定了semop函数的调用结果。
共享内存
很容易理解的一个机制,就是一块内存,进程间可以共享,它的实现都在sys/shm.h中,使用的函数包括shemget、shmat、shmdt、shmctl:
#include <sys/shm.h>
//创建共享内存或者获取已存在的共享内存
int shmget(key_t key, size_t size, int shmflag);
//size,字节为单位,指定内存的大小,获取已存在的共享内存可以设置为0;
//shmflag,支持SHM_HUGETLB和SHM_NORESERVE,前者表示用“大页面”来分配空间给共享内存,后者表示不为共享内存保留交换分区,这样内存不足的时候继续写入就会发起SIGSEGV信号
函数调用成功就返回共享内存的标识符,失败返回-1,然后同样地,内核中有个相关的数据结构shmid_ds会被创建且初始化。在共享内存创建成功后,需要把它关联到进程的地址空间中,用完了需要进行分离:
//关联操作,返回共享内存被关联到进程中的具体地址,失败会返回(void*)-1
void *shmat(int shm_id, const void *shm_addr, int shmflag);
//分离原本关联好的共享内存,成功就回0,失败回-1
int shmdt(const void *shm_addr);
shmget成功调用返回的标识符就可用于shm_id,shm_addr则是进程内指针,具体函数调用效果还是要看shmflag
- shm_addr为NULL,关联地址由系统选择,这样更加兼容
- shm_addr非空,shmflag没有设置SHM_RND,共享内存关联到shm_addr指向地址
- shm_addr非空,shmflag设置了SHM_RND
嗯,shmflag标志位还可以设置SHM_RDONLY,表示进程只读该共享内存,没设置就读写都可(共享内存创建时就会设置读写权限);SHM_REMAP,已经关联呢,就重新关联;SHM_EXEC,指定可读
关于关联成功和取消关联关系,都会使得shmid_ds的内核数据发生变动,比如关联成功:
shm_nattach加一、shm_lpid设置为调用进程的PID、shm_atime设置为当前时间;
取消关联成功,就:
shm_nattach减一、shm_lpid设为调用进程的PID、shm_dtime会设置成当前时间;
这么来看,其实关联和非关联都是一个记录,看看什么时候发生变动,变动的操作者是谁,至于区分开两者就是前面的shm_nattach了。
嗯,和信号量一样,共享内存的关联也是准备工作,要用还是要有个函数来进行调用,共享内存的就是shmctl,这个函数重点关注command参数,这个是具体如何用的关键:
int shmctl(int shm_id, int command, struct shmid_ds *buf);
关于command参数参见下表:
| 参数 | 意思 | 函数调用成功的返回值 |
|---|---|---|
| IPC_STAT | 共享内存相关的内核数据结构shmid_ds复制到buf中 | 0 |
| IPC_SET | buf的部分数据复制到共享内存相关的内核数据结构shmid_ds中, 刷新shmid_ds.shm_ctime |
0 |
| IPC_RMID | 标记上删除,当最后一个进程用完调用shmdt分离后,共享内存 就被删了 |
0 |
| IPC_INFO | 获取共享内存的系统配置,存在转换成shminfo结构体类型的buf中 | 内核中共享内存信息数组被使用项的最大index值 |
| SHM_INFO | 和IPC_INFO类似,但得到的是已分配的共享内存占用的资源信息 (嗯,这里要把buf转换成shm_info型) |
同上 |
| SHM_STAT | 类似IPC_STAT,但此时shm_id是用来表示内核中共享内存信息数组的 | 内核共享内存信息数组索引为shm_id的标识符 |
| SHM_LOCK | 禁止共享内存被移动到交换分区 | 0 |
| SHM_UNLOCK | 和上面的相反,允许共享内存被移动到交换分区 | 0 |
暂时先写就这么点吧,后面再来更新
一些相关的内核数据结构:
//system v信号量
#include <sys/sem.h>
//描述IPC对象权限
struct ipc_perm {
key_t key; //键值
uid_t uid; //持有者的有效用户ID
gid_t gid; //持有者的组ID
uid_t cuid; //创建者的用户ID
gid_t cgid; //创建者的组ID
mode_t mode; //访问权限
...
};
//system v信号量的内核数据结构
struct semid_ds {
struct ipc_perm sem_perm; //重点关注信号量的操作权限
unsigned long int sem_nsems; //信号量集的信号量数
time_t sem_otime; //最后一次调用semop时间
time_t sem_ctime; //最后一次调用semctl时间
...
};
#include <sys/shm.h>
//共享内存的内核数据结构
struct shmid_ds {
struct ipc_perm shm_perm; //共享内存操作权限
size_t shm_segsz; //共享内存大小,以字节为单位
__time_t shm_atime; //对共享内存最后一次调用shmat的时间
__time_t shm_dtime; //对共享内存最后一次调用shmdt的时间
__time_t shm_ctime; //对共享内存最后一次调用shmctl的时间
__pid_t shm_cpid; //创建者PID
__pid_t shm_lpid; //最后一次执行shmat或者shmdt的进程PID
...
};
#include <sys/msg.h>
//消息队列的内核数据结构
struct msqid_ds {
struct ipc_perm msg_perm; //消息队列操作权限
time_t msg_stime; //最后一次调用msgsnd时间
time_t msg_rtime; //最后一次调用msgrcv时间
time_t msg_ctime; //最后一次被修改时间
unsigned long __msg_cbytes; //消息队列中已有的字节数
msgqnum_t msg_qnum; //消息队列已有消息数
msglen_t msg_qbytes; //消息队列允许的最大字节数
pid_t msg_lspid; //最后执行msgsnd的进程PID
pid_t msg_lrpid; //最后执行msgrcv的进程PID
};
C多进程的更多相关文章
- Python中的多进程与多线程(一)
一.背景 最近在Azkaban的测试工作中,需要在测试环境下模拟线上的调度场景进行稳定性测试.故而重操python旧业,通过python编写脚本来构造类似线上的调度场景.在脚本编写过程中,碰到这样一个 ...
- 取代SharedPreferences的多进程解决方案
Android的SharedPreferences用来存储一些键值对, 但是却不支持跨进程使用. 跨进程来用的话, 当然是放在数据库更可靠啦, 本文主要是给作者的新库PreferencesProvid ...
- python 多进程使用总结
python中的多进程主要使用到 multiprocessing 这个库.这个库在使用 multiprocessing.Manager().Queue时会出问题,建议大家升级到高版本python,如2 ...
- Nginx深入详解之多进程网络模型
一.进程模型 Nginx之所以为广大码农喜爱,除了其高性能外,还有其优雅的系统架构.与Memcached的经典多线程模型相比,Nginx是经典的多进程模型.Nginx启动后以daemon ...
- Python的多线程(threading)与多进程(multiprocessing )
进程:程序的一次执行(程序载入内存,系统分配资源运行).每个进程有自己的内存空间,数据栈等,进程之间可以进行通讯,但是不能共享信息. 线程:所有的线程运行在同一个进程中,共享相同的运行环境.每个独立的 ...
- 进击的Python【第十章】:Python的socket高级应用(多进程,协程与异步)
Python的socket高级应用(多进程,协程与异步)
- PHP的pcntl多进程
PHP使用PCNTL系列的函数也能做到多进程处理一个事务.比如我需要从数据库中获取80w条的数据,再做一系列后续的处理,这个时候,用单进程?你可以等到明年今天了...所以应该使用pcntl函数了. 假 ...
- 初探PHP多进程
h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h ...
- gdb进程调试,多进程调试
1.单进程的调试 常规的通过gdb cmd这种方式开启调试,特别说明的是通过attach的方法附加到一个指定的进程上去进行调试,这种方法适合于调试一个已经运行的进程,具体用法: gdb -p [pi ...
- python高级之多进程
python高级之多进程 本节内容 多进程概念 Process类 进程间通讯 进程同步 进程池 1.多进程概念 multiprocessing is a package that supports s ...
随机推荐
- BigTable-列族存储
BigTable 其实就是 Google 设计的分布式结构化数据表. Bigtable 的设计动机: 需要存储的数据种类繁多,包括URL.网页内容.用户的个性化设置在内的数据都是Google需要经常处 ...
- 再谈Redux
2025年再聊前端状态管理似乎是一件不必要的事,毕竟相关文章已堆积得如山如海.但在这些文章或视频内容中,我并没有找到自己喜欢的方案,准确的说是使用方式.所以这篇文章不做技术分析,主要聊聊个人对状态管理 ...
- 头文件中 ifndef/define/endif 有什么用?
1. 相同的声明可以多次出现,重复声明不是错误! 定义不可以,无论是单个文件还是多个文件,某个特定作用域(比如全局变量),不可以重复定义变量. 2. 类/结构体的定义可以在多个文件中多次出现,但是不可 ...
- Java中,将ResultSet映射为对象和队列及其他辅助函数
关于对象关系映射(ORM)在数据库访问中用到的最多,在Java中,很多库都试图将一个ResultSet映射为一个自定义的Java Bean对象或队列,下面是我的实现 1 从ResultSet中读取数据 ...
- thewall靶机
includes.php 内有文件读取漏洞 一开始是想着直接用为协议写入一句话木马但是后来发现不行 因为他的文件读取方式长这样 点击查看代码 <?php include ('/var/www/h ...
- 认识soui4js(第2篇):代码编辑及调试
开始 假定您使用向导在d:\jsdemo目录创建一个工程,您也已经安装好了vscode, 那么您应该可以看到下面的界面效果: 工程生成后,主要包含一个soui资源包及一个main.js 要运行这个程序 ...
- 识别与防御CSRF漏洞
识别与防御CSRF漏洞 CSRF(Cross-Site Request Forgery,跨站请求伪造),通常也被称为"一键攻击"或"会话劫持",其缩写为CSRF ...
- 通讯录管理系统(C++基础知识实现)
通讯录管理系统 描述:本人C++小白一枚,正在学习C++基础知识,给大家分享一款使用C++基础知识实现的通讯录管理系统,一起努力进步,大佬轻点喷. 1. 知识点 (1) 预处理器指令 (#includ ...
- [CF576E] Painting Edges 题解
模版题的升级了. 使用二分图经典判定方法(一个点拆成两个点 \(x,x+n\),连边 \((x,y)\) 就是连接 \((x,y+n),(x+n,y)\),那么是否是二分图就等价于判断 \(x,x+n ...
- 还在手动更改SpringBoot的环境yml配置文件?老鸟带你可视化配置
问题说明: 在SpringBoot开发时.SpringBoot的特性:'约定大于配置',我们只需要在**application.yml **配置当前的环境变量属与那个文件 比如测试环境 'applic ...