美文网首页
linux用户空间 - 多进程编程(三)

linux用户空间 - 多进程编程(三)

作者: 404Not_Found | 来源:发表于2021-06-05 20:40 被阅读0次
  • 作者: 雪山肥鱼
  • 时间: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
  1. ls 的输出 正常 会 写道标准输出中,但是 标准输出已被关闭,同时被赋值了 f_des[1] ls 的输出 不会写道屏幕上
  2. cat 的输入 正常会从标准输入中拿,但是 标准输入已经被关闭,同时被赋值了 f_des[0] 无需手动 键入 cat 的 输入 参数
  3. 也就是说 为了不影响程序的 在屏幕的输入输出,此时管道的两侧 分别为 stdout[本质为f_des[1] ] 和 stdin[ [本质为fdes[0]] ] 文件描述符
  4. cat 得到 输入后,由于 cat 进程的stdout 依旧建在,所以将结果输出到屏幕上。

pipe总结

  1. 多进程模型中,进场用管道进行通讯。
  2. 2次内存拷贝,p2 的内存 需要拷贝到 管道中,p1需要读,那么也要从管道中将内容拷贝到p1的buffer中。
  3. 也就是说并不适用大容量的数据交互

信号的处理

  • 缺省
  • 捕获
  • 忽略
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 年代有些久远。

system V.png
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 会在以下路径生成文件

  1. /dev/shm/xxx
  2. /run/shm/xxx
  3. /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,那么完全不一样了,直接修改了内存逻辑。

相关文章

  • 转载---Binder

    知识储备 Linux进程空间划分 一个进程空间分为 用户空间 & 内核空间(Kernel),即把进程内 用户 & ...

  • Binder(一)Linux进程通信

    用户空间、内核空间 Linux分为内核进程和用户进程:1、内核进程共享一块内存空间,称为内核空间。2、内核进程不能...

  • Android多进程通信之 Binder

    进程空间划分 在 Linux 中一个进程空间可以分为用户空间和内核空间,不同的进程它们的用户空间数据不可共享,但是...

  • 16 Binder-1

    1 Binder 1.1 知识储备 1.1.1 ### Linux进程空间划分 一个进程空间分为 用户空间 & 内...

  • 面试准备——Binder相关

    Linux中的进程通信方式 进程间,用户空间的数据不可共享,所以用户空间相当于私有空间 进程间,内核空间的数据可共...

  • Android进程整理

    init进程(1号进程),是Linux系统的用户空间进程,或者说是Android的第一个用户空间进程。 下面列举常...

  • 进程和计划任务详解(一)

    学习内容: 1、进程相关知识(用户空间、内核空间、进程创建、进程优先级、进程内存)2、Linux进程查看及管理工具...

  • 中级Android开发应该了解的Binder原理

    一、基础概念 Linux的进程空间是相互隔离的。 Linux将内存空间在逻辑上划分为内核空间与用户空间。Linux...

  • Linux 下传统的进程间通信原理

    Linux 下传统的进程间通信原理 在Linux中跨进程通信涉及到几个基本的概念 进程间隔离 进程空间划分:用户空...

  • Binder原理

    Linux进程划分 用户空间内核空间用户空间是不共享的空间,内核空间是共享的空间,所以两个用户空间传递数据就需要内...

网友评论

      本文标题:linux用户空间 - 多进程编程(三)

      本文链接:https://www.haomeiwen.com/subject/issheltx.html