linux用户空间 - 多进程编程(三)
- 作者: 雪山肥鱼
- 时间:20210606 15:11
- 目的:管道、信号、Sys V vs Posix,Unix 域Socket、Socket Pair
# Pipe 管道
## 管道简单举例
## 模拟shell | 效果
## pipe 总结
# 信号的处理
## 捕捉 SIGSEGV 段错误信号
# 信号引起的 race condition
## 信号的block 处理
# SYS V IPC
## 由血缘关系的进程间通讯
## 无血缘关系的进程通讯
### 共享内存代码举例
### 信号量举例
# POSIX IPC
## 共享内存代码举例
## mmap 用于血缘关系
Pipe 管道
管道用于有学园关系的进程之间。
管道.png
管道的pipe 系统调用实际上就是创建出来两个文件描述符。
当父进P1程创建出 fd[2] 时,子进程P2 会继承父进程的所有,所以也会得到pipe 的 2个 文件描述符。
所以毫无瓜葛的两个进程,一定不会访问到彼此的pipe。无法用管道进行通信。
管道一般是单工的。f[0]读,f[1]写
- p2 可以往 fd[1] 里写, p1 从fd[0] 中读
- 规范: p1进程 需要 close(fd[1]).
- 规范: p2进程 需要 close(fd[0]).
管道也可以适用于 兄弟进程(只要有血缘即可)。由于管道是单工的,当两个进程之间需要双向通信,则需要两跟管道。
管道简单举例
int main() {
int fd[2];
int ret = pipe(fd);
if(ret < 0 ) {
perror("pipe\n");
}
pid_t id = fork();
if(id < 0) {
perror("fork\n");
} else if (id == 0) {
close(fd[0]);
char * buf = "hello world\n";
write(fd[1], buf, strlen(buf) +1);
while(1);
} else {
close(fd[1]);
char buf[100];
read(fd[0], buf, 100);
printf("%s\n", buf);
while(1);
}
}
模拟shell | 效果
int main(int argc, char ** argv) {
int f_des[2];
int pid;
if(argc !=3) {
printf("Usage:%s command1 command 2\n, argv[0]");
return 1;
}
if(pipe(f_des == -1) {
perror("can not create the IPC pipe");
return 1;
}
pid = fork();
if(pid == -1) {
perror("can not create new process");
return 1;
} else if(pid == 0 ) {
// 将 stdout 指向的文件描述符关闭,将f1 复制给 f2,返回f2
dup2(f_des[1], STDOUT_FILENO):
close(f_des[0]);
close(f_des[1]);
if(execlp(argv[1], argv[1], NULL) == -1) {
perror("in child process, cannot execute the command");
return 1;
}
return 1
} else {
dup2(f_des[0], STDIN_FILENO);
close(f_des[0]);
close(f_des[1]);
if(execlp(argv[2], argv[2], NULL) == -1) {
perror("in parent process, cannot execute the command");
return 1;
}
return 1;
}
}
执行
./a.out ls cat => ls | cat
- ls 的输出 正常 会 写道标准输出中,但是 标准输出已被关闭,同时被赋值了 f_des[1] ls 的输出 不会写道屏幕上
- cat 的输入 正常会从标准输入中拿,但是 标准输入已经被关闭,同时被赋值了 f_des[0] 无需手动 键入 cat 的 输入 参数
- 也就是说 为了不影响程序的 在屏幕的输入输出,此时管道的两侧 分别为 stdout[本质为f_des[1] ] 和 stdin[ [本质为fdes[0]] ] 文件描述符
- cat 得到 输入后,由于 cat 进程的stdout 依旧建在,所以将结果输出到屏幕上。
pipe总结
- 多进程模型中,进场用管道进行通讯。
- 2次内存拷贝,p2 的内存 需要拷贝到 管道中,p1需要读,那么也要从管道中将内容拷贝到p1的buffer中。
- 也就是说并不适用大容量的数据交互
信号的处理
- 缺省
- 捕获
- 忽略
void sigusr1(int sig) {
printf("got the SIGUSR1 signal...\n");
exit(0);
}
int main(void) {
if(signal(2, sigusr1/*SIG_IGN*/) == SIG_ERR) {
perror("Can not reset the SIGINT signal handler");
return 1;
}
if(signal(SIGUSR1, sigusr1) == SIG_ERR) {
perror("Can not reset the SIGUSR1 signal handler");
return 1;
}
return 1;
}
ctrl-c(2号信号) + SIGUSR1 信号 绑了一个新函数。则 ctrl-c 无效。
查看进程的信号
ps -C a.out s // 进程信号的情况
image.png
号信号被捕获。
将2号信号忽略掉
忽略2号信号.png
kill -l //查看所有信号
9号信号 kill 和19号信号 stop 不能乱搞,只能用缺省。
其它信号甚至段信号也都可以捕获。
捕获SIGSEGV 段错误信号
static jmp_buf env;// 用于保存现场
void segvSignal(int sig) {
longjmp(env, 1);
}
int main(int argc, char ** argv) {
int r = setjmp(env);
if (r == 0 ) {
signal(SIGSEGV, segvSignal);
printf("making segment fault");
int *p = NULL;
(*p) = 0;
} else {
printf("after segment fault\n");
}
return 0;
}
- 第一次运行setjump,保存运行现场后,返回0
- 第二次调用long jump, 回再次jump到 运行现场,也就是再次进入 setjmp。
- 但是 不会返回0. 返回的是传入的参数 1.
- 则会走下面的 != 1 的分支中。
改变程序的执行现场,修改PC指针,有些像goto,只不过返回非0值
运行结果
making segment fault
after segment fault
程序不会死。
如果不忽略 page fault
则会产生 core dump.
信号会引起 race condition
- 信号是异步的
- 信号访问其它线程正在访问的资源
- 可以考虑屏蔽信号
struct two_long {
long a,b;
} data;
int main(void ) {
static struct two_long_zeros = {0, 0}, one = {1,1};
signal(SIGALRM, signal_handler);
data = zeros;
alarm(1);
while(1)
{data = zeros; data = ones;}
}
return 1;
不停的给data 赋值,同时每隔1s会有信号进来,打印 data的值。
理论上打印出来的结果同时为0,同时为1
但并非如此,是 0,1,交替随机的。
image.png
signal 异步的,随时都可以进来,所以打印出来的结果,并不是我想要的。
信号的block处理
struct two_long {
long a,b;
} data;
int main(void ) {
sigset_t bset, oset;
static struct two_long_zeros = {0, 0}, one = {1,1};
signal(SIGALRM, signal_handler);
data = zeros;
alarm(1);
sigemptyset(&bset);
sigaddset(&bset, SIGALRM);
while(1) {
//sigprocmask 设置bset为新的屏蔽字,将原阿里的信号屏蔽字返回给oset
if(sigprocmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set mask failed\n");
data = zeros;
if(sigprocmask(SIG_SETMASK, &oset, NULL) != 0)
printf("!! Set mask failed");
if(sigprocmask(SIG_BLOCK, &bset, &oset) != 0)
printf("!! Set mask failed");
data = one;
if(sigprocmask(SIG_SETMASK, &oset, NULL) != 0)
printf("!! Set mask failed");
{data = zeros; data = ones;}
}
return 1;
信号对于应用程序来说,很像中断对于内核,都是访问临界区数据
信号被屏蔽,延后执行。
写多线程的程序时,不要以为只有线程之间有竞争,其实信号也会有竞争
SYS V IPC
system v 的IPC 年代有些久远。
ipcs //可以看到 system v 的ipc的 key id 等。
有血缘关系的进程 key_t 都是相同的。
有血缘关系的进程间通讯
Key 是私有key IPV PRIVATE
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <string.h>
#define MAXSIZE 4096
int main()
{
int shmid;
char *p = NULL;
pid_t pid;
if ((shmid = shmget(IPC_PRIVATE, MAXSIZE, 0666)) == -1) {
perror("shmget");
exit(-1);
}
if ((pid = fork()) == -1) {
perror("fork");
exit(-1);
}
if (pid == 0) {
if ((p = shmat(shmid, NULL, 0)) == (void *)-1) {
perror("shmat");
exit(-1);
}
strcpy(p, "hello\n");
system("ipcs -m");
if (shmdt(p) == -1) {
perror("shmdt");
exit(-1);
}
system("ipcs -m");
} else {
getchar();
if ((p = shmat(shmid, NULL, 0)) == (void *)-1) {
perror("shmat");
exit(-1);
}
printf("%s\n", (char *)p);
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("RM");
exit(-1);
}
}
return 0;
}
}
无血缘关系的进程间通讯
可能用消息队列,可能用共享内存,可能用信号量进行通讯。
利用 _pathname 路径,约定好一条路径。和tcp/ip地址很像,来生成一个key_t key, 用msg_get shm_get 得到共享内存or 信号量。
int id 可以理解为文件描述符 fd。
其中Sys V 的共享内存 最为常用。
共享内存 代码举例:
一定要检查路径,如果仅仅有2个进程,你没有创建路径,两者都是 -1(相当于大家约定好了),那当然能通信拉。但更多的进程出现,则会有问题。
/*sharemem_r.c*/
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
char name[4];
int age;
} people;
main(int argc, char** argv)
{
int shm_id,i;
key_t key;
people *p_map;
char* name = "/dev/shm/myshm2";
key = ftok(name, 1);
if(key == -1) {
perror("ftok error");
exit(-1);
}
shm_id = shmget(key,4096*1024,/*IPC_CREAT*/0666);
if(shm_id == -1)
{
perror("shmget error");
return;
}
p_map = (people*)shmat(shm_id,NULL,0);
for(i = 0;i<10;i++)
{
printf( "name:%s\n",(*(p_map+i)).name );
printf( "age %d\n",(*(p_map+i)).age );
}
if(shmdt(p_map) == -1)
perror(" detach error ");
}
/*sharemem_w.c*/
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
char name[4];
int age;
} people;
main(int argc, char** argv)
{
int shm_id,i;
key_t key;
char temp;
people *p_map;
char* name = "/dev/shm/myshm2";
key = ftok(name, 1);
if(key==-1) {
perror("ftok error");
exit(-1);
}
shm_id=shmget(key,4096*1024,IPC_CREAT|0666);
printf("shm_id:%d\n",shm_id);
if(shm_id==-1)
{
perror("shmget error");
return;
}
p_map=(people*)shmat(shm_id,NULL,0);
temp='a';
printf("%08x\n", p_map);
for(i = 0;i<10;i++)
{
temp+=1;
memcpy((*(p_map+i)).name,&temp,1);
(*(p_map+i)).age=20+i;
}
memset((void *)p_map+100,0, 4096*1024 - 100);
if(shmdt(p_map)==-1)
perror(" detach error ");
sleep(100);
system("ipcs");
shmctl(shm_id, IPC_RMID, NULL) ;
printf("--------------------------------------------------\n");
system("ipcs");
}
一定要检查返回值
信号量举例
依然依靠key,但是api 实在是太挫了。P&V 操作都是 semop. (posix 的 ipc跟为简洁)
/*sem.c*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
union semun{
int val;
struct semid_ds * buf;
unsigned short int * array;
struct seminfo * __buf;
};
int main(int argc,char* argv[])
{
int semid;
pid_t pid;
int proj_id;
key_t key;
int num;
int i,j;
union semun arg;
static struct sembuf acquire={0,-1,SEM_UNDO};
static struct sembuf release={0,1,SEM_UNDO};
if(argc!=2){
printf("Usage : %s num\n",argv[0]);
return -1;
}
num=atoi(argv[1]);
proj_id=2;
key=ftok("/tmp/beijing",proj_id);
if(key==-1){
perror("cannot generate the IPC key");
return -1;
}
semid=semget(key,1,IPC_CREAT | IPC_EXCL | 0666);
if(semid==-1){
perror("cannot create semaphore set");
return -1;
}
static unsigned short start_var=1;
arg.array=1;
if(semctl(semid,0,SETVAL,arg)==-1){
perror("cannot set semaphore set");
return -1;
}
for(i=0;i<num;i++){
pid=fork();
if(pid<0){
perror("cannot create new process");
return -1;
}else if(pid==0){
semid=semget(key,1,0);
if(semid==-1){
perror("cannot let the process get the access right");
_exit(-1);
}
for(j=0;j<2;j++){
sleep(i*10);
if(semop(semid,&acquire,1)==-1){
perror("cannot acquire the resource");
_exit(-1);
}
printf("====enter the critical section=====\n");
printf("---pid : % ld ---\n",(long)getpid());
sleep(5);
printf("====leave the critical section=====\n");
if(semop(semid,&release,1)==-1){
perror("cannot release the resource");
_exit(-1);
}
}
_exit(0);
}
}
for(i=0;i<num;i++)
wait(NULL);
if(semctl(semid,0,IPC_RMID,0)==-1){
perror("cannot remove the semaphore set");
return -1;
}
return 0;
}
POSIX IPC
共享内存 代码举例
POSIX 共享内存当然也需要一个名字,但并不是路径。
无论读进程还是写进程,都需要传入相同的名字。
如果是unbuntu 会在以下路径生成文件
- /dev/shm/xxx
- /run/shm/xxx
- /var/run/shm/xxx
/*posix-shm-r.c*/
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <stdlib.h> /* For O_* constants */
#include <string.h>
#include <stdio.h>
#define MAX_LEN 10000
struct region { /* Defines "structure" of shared memory */
int len;
char buf[MAX_LEN];
};
struct region *rptr;
int fd;
int main(int argc, char **argv)
{
/* Create shared memory object and set its size */
fd = shm_open(argv[1], O_RDWR, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("shm open failed\n"); /* Handle error */;
return -1;
}
if (ftruncate(fd, sizeof(struct region)) == -1) {
perror("ftruncate failed\n") /* Handle error */;
return -1;
}
/* Map shared memory object */
rptr = mmap(NULL, sizeof(struct region),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (rptr == MAP_FAILED) {
perror("mmap failed\n") /* Handle error */;
return -1;
}
/* Now we can refer to mapped region using fields of rptr;
for example, rptr->len */
printf("%d %s\n", rptr->len, rptr->buf);
}
/*posix-shm-w.c*/
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <stdlib.h> /* For O_* constants */
#include <string.h>
#define MAX_LEN 10000
struct region { /* Defines "structure" of shared memory */
int len;
char buf[MAX_LEN];
};
struct region *rptr;
int fd;
int main(int argc, char **argv)
{
/* Create shared memory object and set its size */
fd = shm_open(argv[1], O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
if (fd == -1) {
perror("shm open failed\n"); /* Handle error */;
return -1;
}
if (ftruncate(fd, sizeof(struct region)) == -1) {
perror("ftruncate failed\n") /* Handle error */;
return -1;
}
/* Map shared memory object */
rptr = mmap(NULL, sizeof(struct region),
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (rptr == MAP_FAILED) {
perror("mmap failed\n") /* Handle error */;
return -1;
}
/* Now we can refer to mapped region using fields of rptr;
for example, rptr->len */
memset(rptr->buf, 0, MAX_LEN);
rptr->len=1000;
strcpy(rptr->buf, "hello world");
while(1);
}
其实 2和3 是1 的符号链接。只要保证是一个就能互相通信
mmap 用于血缘关系
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char **argv)
{
pid_t pid;
char *p;
p = (char *)mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0); //vma
//私有并不共享,写时拷贝,父进程的值依旧是xxxxx,并非是hello world
/*p = (char *)mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); */
strcpy(p, "XXXXXXXXXXXXXXXXX");
pid = fork();
if (pid == -1) {
exit(-1);
} else if (pid == 0) { //shared vma
sprintf(p, "%s", "Hello World");
munmap(p, 4096);
_exit(0);
} else {
sleep(2);
printf("%s\n", p);
munmap(p, 4096);
}
return 0;
}
关键点,mmap 内存的属性修改为 private 后,产生写时copy,虚拟地址一样,但是物理地址已经不同了
当然 如果子进程修改了程序背景,执行了 exec,那么完全不一样了,直接修改了内存逻辑。