linux 进程间通信方式

1. 用户态和内核态进程通信

    1)系统调用,最常用, 比如针对IO,有linux同步io接口(libaio最终也是调用linux同步io系统接口),linux aio ,linux io_uring

    2)procfs 进程文件系统,伪文件系统,不占外部空间,仅占用少量内存,挂载点/proc, 这个目录下看到的文件,其实是一个内核变量,用户读写这个文件,就相当于读写内核变量。

         e.g1 cat /proc/cpuinfo, 查看内核信息; 

         e.g2 echo 2 > /proc/sysrq-trigger,修改内核参数;

    3)sysctl 为/proc/sys下的变量提供额外的修改方式, sysctl -w ip_forward =1  等价于echo 1 > /proc/sys/net/ipv4/ip_forward ;

          当然,也可以使用/etc/syctl.conf来进行批量修改

    4)sysfs,一种虚拟文件系统, 挂载点/sys,也是通过修改其下的文件,来达到修改内核变量的目的,和procfs不同的是,sysfs是将原本在procfs中的关于设备和驱动的部分独立出来,以设备树的形式呈现给用户。

    5)netlink,本质上就是一种socket,iscsid进程与内核iscsi通信就是使用这种方式

          netlink_socket = socket(AF_NETLINK, socket_type, netlink_family)

    6)  ioctl,用户态控制设备的接口,在这里,我们可以实现自己定义的系统调用

         long unlocked_ioctl(struct file *file, unsigned int request, unsigned long arg);

       第一个参数是打开的设备文件的文件描述符,通常是open系统调用的返回值;第二个参数request是可以自定义的请求号,在内核态中,可以根据request的值,来调用约定的函数,“实现”自定义的系统调用;第三个就是入参,传递进内核。  如果要传递给目标函数的参数直接存储在arg中,则直接读取arg再调用即可。如果要传递给目标函数的参数是arg所指向的一段用户态内存,则需要从用户态拷贝到内核态。较少的数据可以用get_user和put_user来读写,较多的数据可以用copy_from_user和copy_to_user来读写。准备好参数之后,调用目标函数

    7)信号SIGINT

         段错误等等内核发送给用户态进程的信号,可以捕捉

2. 用户态进程间通信, 通常IPC就是说的用户态进程间通信

    1)匿名管道:管道的本质实际上就是一个内核缓冲区,进程以先进先出(FIFO)的方式从缓冲区中存取数据。管道一端的进程将数据写入缓冲区中,写入的内容每次都添加在缓冲区的末尾;管道的另一端则从缓冲区中读取数据,每次读取的时候都是从缓冲区的头部读取数据

         缺点:1.半双工,数据单项流动,弱要双向流动,需要创建两个管道 2. 因为无名字,所以只适用于父子进程;3.效率低下;4.管道缓冲区大小有限制,一个页面大小;5.传输的数据无格式的数据流,这就要求管道的写入端和读取端实现约定号数据格式

         用法:int pipe(int fd[2]) ,一个是管道的读取端描述符fd[0], 一个是管道的写入端描述符fd[1], 由于写入和读取fd都在一个进程中,因此,必须创建子进程,然后两个进程分别用一个fd,这样就实现进程间通信了。 

         特殊地,比如在shell中,A|B这类命令时, A、B都是shell的子进程,A、B不存在父子关系,因此实际上是在shell进程中创建了管道,然后分别把fd[0]和fd[1]传递给两个子进程的。

         e,g ps -ef | grep redis  

    2)命名管道(FIFO): 为了克服匿名管道不能用于无父子关系的进程的情况,命名管道应运而生。命名管道与匿名管道之间的区别在于,命令管道提供了一个路径名与之相关联,从而以文件的形式存在于文件系统中。这样,即使与创建命名管道进程不存在父子关系的进程,只要可以访问该路径,就能够通过彼此该命名管道进行通信。这样就实现了不存在父子进程间的通信。**命名管道的名字存在于文件系统当中,而内容存在于内存当中。下面是以Linux为例创建命名管道的流程。

           1. 使用mkfifo创建命名管道 mkfifo mypipe;

           2. 向管道写入数据 echo “hello” > mypipe  //卡住

           3. cat < mypipe //读出后,echo不再卡住

           命名管道的阻塞问题:命名管道在打开时需要确认对方的存在,否则将阻塞,即若是以读方式打开某管道,在此之前,必须有一个进程以写方式打开管道,否则阻塞。另外,还可以以读写(O_RDWR)模式打开命名管道,即当前进程读,当前进程写,不会出现阻塞。

    3)消息队列:本质是保存在内核中的消息链表,在发送数据时,会被分成一个一个独立的数据单元,称之为消息体。消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型。每个消息体都是固定大小的存储块。如果进程从消息队列中读取了消息,则内核会将该消息从消息队列中移除。

           与管道的不同:管道生命周期随进程的创建而建立,进程销毁而结束。消息队列与内核有关,除非内核重启或者显示删除消息队列;数据不同,管道无格式字节流,消息队列是固定大小的存储块。

           缺点:通信不及时;消息体大小有限制,不适合大数据传输。涉及到用户态和内核态数据拷贝开销。

           process1:msgid = msgget((key_t)1234,0666 | IPC_CREAT); msgsnd(msgid,(void *)&some_data,MAX_TEXT,0) ;

           process2: msgid = msgget((key_t)1234,0666 | IPC_CREAT); msgrcv(msgid,(void *)&some_data,BUFSIZ,msg_to_receive,0) == -1)

    4)共享内存:消息队列的读取过程会发生用户态和内核态之间的消息拷贝过程,使用共享内存这种方式就可以很好地解决这个问题。共享内存可以使得多个进程可以直接读写在同一块内存空间中,这是效率最高的进程间通信方式。其实就是一块物理内存,然后对应两个进程的不同虚拟地址空间; open-iscsi中,iscsid进程与日志进程就用的共享内存。

    5)信号量:

           process1:创或取 sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT); 初始化 semctl(sem_id, 0, SETVAL, sem_union);加或减,取决于第二个参数,semop(sem_id, &sem_b, 1)  ;

           process2:创或取 sem_id = semget((key_t)1234, 1, 0666 | IPC_CREAT); 加或减,取决于第二个参数,semop(sem_id, &sem_b, 1)  sem_b 1;

          虽然共享内存大大提高进程间通信的速度,但也带来了新的问题。假如某个时刻多个进程同时对同一个变量进行修改就会产生冲突。为了防止多进程竞争共享资源,需要一些进程间同步机制,使得在某一时刻只有一个进程可以访问共享资源。信号量就是其中之一。信号量其实就是一个整型的计数器,主要用于实现进程间的互斥和同步,而不是用于缓存进程间的通信数据

    6)信号: 参考内核与用户态通信,在用户态进程中执行kill命令,去把别的进程kill掉,脚本中用得很多。

    7)socket:略

      

猜你喜欢

转载自blog.csdn.net/Hahafly1234/article/details/112653230