Linux 进程间通信 —— system V进程间通信
systemV 共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据.
共享内存的基本原理
每一个进程的虚拟空间中都有一块区域专门用来存放公共资源,这个区域就是“共享区“。
共享内存让不同进程看到相同资源的方式是通过在物理内存中分配一块内存空间,并将该内存空间映射到每个进程的虚拟地址空间中。这种映射是通过将虚拟地址填充到各个进程的页表中来实现的。这样,不同进程就可以通过访问相同的虚拟地址来访问同一块物理内存,从而实现了共享内存的效果。
注意:这里的开辟空间和建立映射关系等都是由系统完成的。
并且此处物理内存上的共享区并不属于任何一个进程,而是被多个进程共享,而且物理内存上存在很多的共享内存,这些共享内存的声明周期是随着操作系统的,即就是当我们使用完这个共享内存之后,该共享内存并不会自动删除,而需要我们手动删除。
共享内存数据结构
因为系统中有大量的共享内存,所以我们需要将这些共享内存使用一个结构体组织起来,以便管理和使用。
struct shmid_ds
{
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
-
struct ipc_perm shm_perm
:- 这是一个
ipc_perm
结构体,用于存储共享内存的操作权限信息,包括拥有者的用户ID、组ID,以及权限位。
- 这是一个
-
int shm_segsz
:- 这是一个整型变量,表示共享内存段的大小,以字节为单位。
-
__kernel_time_t shm_atime
:- 这是一个时间戳,表示最后一次共享内存段被附加(attach)的时间。
-
__kernel_time_t shm_dtime
:- 这是一个时间戳,表示最后一次共享内存段被分离(detach)的时间。
-
__kernel_time_t shm_ctime
:- 这是一个时间戳,表示最后一次共享内存段的状态发生变化的时间,例如权限修改。
-
__kernel_ipc_pid_t shm_cpid
:- 这是一个类型为
__kernel_ipc_pid_t
的变量,表示创建共享内存段的进程的PID。
- 这是一个类型为
-
__kernel_ipc_pid_t shm_lpid
:- 这是一个类型为
__kernel_ipc_pid_t
的变量,表示最后一个操作(attach/detach)共享内存段的进程的PID。
- 这是一个类型为
-
unsigned short shm_nattch
:- 这是一个无符号短整型变量,表示当前附加(attach)到共享内存段的进程数。
-
unsigned short shm_unused
:- 这是一个无符号短整型变量,保留用于向后兼容。
-
void *shm_unused2
:- 这是一个指针,保留用于DIPC(Distributed Inter-Process Communication)。
-
void *shm_unused3
:- 这是一个指针,未使用。
这个结构体用于在用户空间和内核空间之间传递共享内存段的属性信息,包括大小、权限、时间戳等。在操作系统中,这些信息通常由内核来维护和管理。
ipc_perm
shmid_ds
里面的struct ipc_perm
是一个结构体,用于存储共享内存的操作权限信息。它包含了对共享内存段的访问权限、拥有者信息,了解他能帮助我们更好的理解共享内存。其结构如下:
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
而当我们申请了一块共享内存之后呢,为了让不同的进程看到同一个共享内存,此时系统会给用户返回一个 __kernel_key_t
类型用于标识共享内存段的唯一标识符key
;不同的进程可以通过相同的键值来访问同一块共享内存段。
共享内存通信步骤
我们可以吧共享内存的通信分为一下几个步骤:
- 创建共享内存
- 连接共享内存
- 访问共享内存
- 分离共享内存
- 删除共享内存
共享内存的创建
共享内存的创建需要用到shmget函数,原型如下:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数:
- key : 该
key
就是存储在struct ipc_perm
里面的key,这是一个非0整数,表示待创建共享内存在系统当中的唯一标识。这个值通常使用ftok
函数来生成。 - size : 表示待创建共享内存的大小。
- shmflg : 这是一组标志,用于指定共享内存的访问权限和行为。
返回值:
- 当
shmget
函数调用成功时,会返回一个与key
相关的共享内存标识符(非负整数),这个标识符用于后续的共享内存函数操作。 - 如果调用失败,则返回-1。
返回值 和 key 的区别
key和shmid在共享内存管理中各自承担着不同的角色。key主要用于在创建或获取共享内存时提供唯一标识,而shmid则是系统分配给已成功创建或获取的共享内存的内部标识符,用于后续的操作和管理。
ftok
ftok函数的函数原型如下:
key_t ftok(const char *pathname, int proj_id);
参数:
- pathname:这是一个指向文件路径的字符串,该文件必须存在且可访问。
ftok
会从这个路径中提取信息来生成key
。 - proj_id:这是一个整数标识符,通常由用户自定义。它可以是0到255之间的任意整数(虽然proj_id是一个int类型,但ftok只使用其后8位)。这个标识符用于区分使用相同pathname的不同项目或应用。
返回值:
- 成功:
ftok
返回一个唯一的key
值,这个值可以用作 IPC对象 的标识符,如共享内存、消息队列等。 - 失败: (例如,指定的文件不存在或不可访问),
ftok
可能返回-1或特定的错误代码。
ftok
函数结合pathname
和 proj_id
来生成一个唯一的 key
值。具体来说,它会根据文件的索引节点号(inode
)和proj_id
来计算 key
。
key
的构成通常包括文件的设备号(st_dev
)的后两位、索引节点号(st_ino
)的后四位,以及proj_id
的后八位。这些信息被组合起来形成一个唯一的32位整数值,即key
。
参数shmflg
The value shmflg is composed of:
稍微总结下就是:
- IPC_CREAT :如果要创建的共享内存不存在,则创建;要是存在的话,就获取该共享内存返回。
- IPC_EXCL :单独使用没有意义,只有和IPC_CREAT组合才有意义。
- IPC_CREAT | IPC_EXCL : 如果你要创建的共享内存不存在,创建之,如果存在,则出错返回。(这点很重要,因为这就意味着如果我们成功返回了,则这个shm就是全新的)。
至此,我们就可以使用先来创建一个简单的共享内存了。
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#define PATHNAME "/home/qq/bt111/Linux/5_16/6.shm_test"
#define PROJ_ID 0x6666 //标识符
#define SIZE 4096
//转十六进制
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer,sizeof(buffer),"0x%x",key);
return buffer;
}
int main()
{
//使用shmget 得先生成key
//key_t ftok(const char *pathname, int proj_id);
key_t key = ftok(PATHNAME,PROJ_ID);
//返回值小于0 申请失败
if(key < 0)
{
perror("ftok");
return 1;
}
//开辟共享内存,使用shmget
//int shmget(key_t key, size_t size, int shmflg);
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
//打印key
std::cout << "key -> " << ToHex(key) << std::endl;
//打印shmid
std::cout << "shmid -> " << shmid << std::endl;
return 0;
}
我们这里使用了shmget
来创建了一个共享内存,打印出其key的十六进制数和shmid。
我们可以看到,因为我们的shmget
的第三个参数是IPC_CREAT|IPC_EXCL
,所以当我们连续使用一个key来创建共享内存时,因为已经存在此共享内存,shmget
返回 -1 。
在Linux当中,我们可以使用ipcs
命令查看有关进程间通信设施的信息,此时会打印出消息队列(Message Queues
),共享内存(Shared Memory Segments
),信号量(Semaphore Arrays
)相关的信息。同时你也可以带上一下几个选项:
-q
:列出消息队列相关信息。-m
:列出共享内存相关信息。-s
: 列出信号量相关信息。
共享内存的释放
前面我们已经说过,共享内存的生命周期并不随着使用的结束而终止,而是随着操作系统的,只有当操作系统停止工作其才会被释放。这样是比较耗费资源的,所以需要我们自己来手动的释放一下:
手动释放的方式有两种:
- 使用命令释放共享内存
- 在进程通信完毕后调用释放共享内存的函数进行释放。
使用命令释放共享内存
我们可以使用ipcrm -m shmid
命令释放指定id的共享内存资源。
qq@iZ0jl65jmm6w9evbwz2zuoZ:~/bt111/Linux/5_16/6.shm_test$ ipcrm -m 2
调用释放共享内存的函数进行释放。
控制共享内存我们需要用shmctl
函数,shmctl
函数的函数原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/
参数:
- shmid : 共享内存段的标识符,由
shmget
返回。 - cmd : 执行的操作,可以是以下值之一:
-
IPC_STAT
:获取共享内存段的状态信息,并将其存储在“buf中。
-
IPC_SET
:设置共享内存段的状态信息,使用buf中的值。
-
IPC_RMID
:删除共享内存段。
- buf : 指向
shmid_ds
结构的指针,用于传递或接收共享内存段的状态信息。
返回值:
shmctl
调用成功,返回 0。shmctl
调用失败,返回 -1。
示例:
使用shmctl
来释放共享内存,第三个参数传空。
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#define PATHNAME "/home/qq/bt111/Linux/5_16/6.shm_test"
#define PROJ_ID 0x6666 //标识符
#define SIZE 4096
//转十六进制
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer,sizeof(buffer),"0x%x",key);
return buffer;
}
int main()
{
//使用shmget 得先生成key
//key_t ftok(const char *pathname, int proj_id);
key_t key = ftok(PATHNAME,PROJ_ID);
//返回值小于0 申请失败
if(key < 0)
{
perror("ftok");
return 1;
}
//开辟共享内存,使用shmget
//int shmget(key_t key, size_t size, int shmflg);
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
//打印key
std::cout << "key -> " << ToHex(key) << std::endl;
//打印shmid
std::cout << "shmid -> " << shmid << std::endl;
sleep(3);
//删除共享内存
shmctl(shmid,IPC_RMID,nullptr);
sleep(2);
return 0;
}
同时使用下面指令监控:
while :; do ipcs -m ; echo "------------------" ; sleep 1; done;
可以看见,使用shmget申请的共享内存在sleep(2)
之后被shmctl
销毁.
共享内存的关联
将共享内存连接到进程地址空间我们需要用shmat
函数,shmat
函数的函数原型如下:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
- shmid:共享内存段的标识符,由
shmget
返回。 - shmaddr :指定将共享内存段映射到进程地址空间的地址,通常为
NULL
,表示让系统自动选择合适的地址。 - shmflg:指定附加共享内存的选项,通常为0。
返回值:
- shmat调用成功,返回共享内存映射到进程地址空间中的起始地址。
- shmat调用失败,返回(void*)-1。
示例:
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#define PATHNAME "/home/qq/bt111/Linux/5_16/6.shm_test"
#define PROJ_ID 0x6666 // 标识符
#define SIZE 4096
// 转十六进制
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
int main()
{
// 使用shmget 得先生成key
// key_t ftok(const char *pathname, int proj_id);
key_t key = ftok(PATHNAME, PROJ_ID);
// 返回值小于0 申请失败
if (key < 0)
{
perror("ftok");
return 1;
}
// 开辟共享内存,使用shmget
// int shmget(key_t key, size_t size, int shmflg);
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL);
// 打印key
std::cout << "key -> " << ToHex(key) << std::endl;
// 打印shmid
std::cout << "shmid -> " << shmid << std::endl;
sleep(3);
std::cout << "attach begin! " << std::endl;
void *mem = shmat(shmid, NULL, 0);
if (mem == nullptr)
{
perror("shmat");
return 1;
}
sleep(2);
std::cout << "attach end! " << std::endl;
sleep(2);
// 删除共享内存
shmctl(shmid, IPC_RMID, nullptr);
sleep(2);
return 0;
}
在我们shmget
的时候最好在第三个参数上加上权限,比如
int shm = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666); //创建权限为0666的共享内存
此时再运行程序,即可发现关联该共享内存的进程数由0变成了1,而共享内存的权限显示也不再是0,而是我们设置的666权限。
共享内存的去关联
取消共享内存与进程地址空间之间的关联我们需要用shmdt
函数,shmdt
函数的函数原型如下:
int shmdt(const void *shmaddr);
参数:
- shmaddr :指向共享内存段起始地址的指针,通常为通过
shmat
函数返回的地址。
返回值:
- 函数成功时返回0,失败时返回-1,并设置相应的错误码。在分离共享内存后,进程将不再能够访问该共享内存中的数据。
示例:
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#define PATHNAME "/home/qq/bt111/Linux/5_16/6.shm_test"
#define PROJ_ID 0x6666 // 标识符
#define SIZE 4096
// 转十六进制
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
int main()
{
// 使用shmget 得先生成key
// key_t ftok(const char *pathname, int proj_id);
key_t key = ftok(PATHNAME, PROJ_ID);
// 返回值小于0 申请失败
if (key < 0)
{
perror("ftok");
return 1;
}
// 开辟共享内存,使用shmget
// int shmget(key_t key, size_t size, int shmflg);
int shmid = shmget(key, SIZE, IPC_CREAT | IPC_EXCL | 0666);
// 打印key
std::cout << "key -> " << ToHex(key) << std::endl;
// 打印shmid
std::cout << "shmid -> " << shmid << std::endl;
sleep(3);
std::cout << "attach begin! " << std::endl;
void *mem = shmat(shmid, NULL, 0);
if (mem == nullptr)
{
perror("shmat");
return 1;
}
sleep(2);
std::cout << "attach end! " << std::endl;
sleep(2);
std::cout << "去关联开始" << std::endl;
//去关联开始
shmdt(mem);
std::cout << "去关联结束" << std::endl;
sleep(2);
// 删除共享内存
shmctl(shmid, IPC_RMID, nullptr);
sleep(2);
return 0;
}
注意观察nattch
数值的变化
用共享内存实现serve&client通信
了解了共享内存的使用之后呢,现在可以尝试让两个进程通过共享内存进行通信了。在让两个进程进行通信之前,我们可以先测试一下这两个进程能否成功挂接到同一个共享内存上。
服务端负责创建共享内存,创建好后将共享内存和服务端进行关联,之后进入死循环,便于观察服务端是否挂接成功。
client.cc
客户端只需要直接和服务端创建的共享内存进行关联即可,之后也进入死循环,便于观察客户端是否挂接成功。
client.c
//client.c
#include "comm.h"
int main()
{
key_t key = ftok(PATHNAME, PROJ_ID); //获取与server进程相同的key值
if (key < 0){
perror("ftok");
return 1;
}
int shm = shmget(key, SIZE, IPC_CREAT); //获取server进程创建的共享内存的用户层id
if (shm < 0){
perror("shmget");
return 2;
}
printf("key: %x\n", key); //打印key值
printf("shm: %d\n", shm); //打印共享内存用户层id
char* mem = shmat(shm, NULL, 0); //关联共享内存
int i = 0;
while (1){
//不进行操作
}
shmdt(mem); //共享内存去关联
return 0;
}
为了让服务端和客户端在使用ftok函数获取key值时,能够得到同一种key值,那么服务端和客户端传入ftok函数的路径名和和整数标识符必须相同,这样才能生成同一种key值,进而找到同一个共享资源进行挂接。这里我们可以将这些需要共用的信息放入一个头文件当中,服务端和客户端共用这个头文件即可。
comm.h
//comm.h
#include <stdio.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#define PATHNAME "/home/cl/Linuxcode/IPC/shm/server.c" //路径名
#define PROJ_ID 0x6666 //整数标识符
#define SIZE 4096 //共享内存的大小
此时我们就可以让服务端和客户端进行通信了,这里以简单的发送字符串为例。
客户端不断向共享内存写入数据:
因为共享内存其自身的特点,没有额外的拷贝过程,所以共享内存是进程最快的方式,所以共享内存并不保证通信的同步,数据在读取之后也不会清空,所以就能看到一直在之前所读到底字符串后面添加继续打印,。所以很多时候,为了保证数据的同步性,即写入端进程写入数据后才让读端进程读取数据,我们可以让管道做一个提示作用。
管道分别给写端和读端发送信号,使用管道来封装一下。
Shm.cc
//Shm.cc
#ifndef __SHM_HPP__
#define __SHM_HPP__
#include <iostream>
#include <string>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
const int gCreater = 1;
const int gUser = 2;
const std::string gpathname = "/home/whb/code/111/code/lesson22/4.shm";
const int gproj_id = 0x66;
const int gShmSize = 4097; // 4096*n
class Shm
{
private:
key_t GetCommKey()
{
key_t k = ftok(_pathname.c_str(), _proj_id);
if (k < 0)
{
perror("ftok");
}
return k;
}
int GetShmHelper(key_t key, int size, int flag)
{
int shmid = shmget(key, size, flag);
if (shmid < 0)
{
perror("shmget");
}
return shmid;
}
std::string RoleToString(int who)
{
if (who == gCreater)
return "Creater";
else if (who == gUser)
return "gUser";
else
return "None";
}
void *AttachShm()
{
if (_addrshm != nullptr)
DetachShm(_addrshm);
void *shmaddr = shmat(_shmid, nullptr, 0);
if (shmaddr == nullptr)
{
perror("shmat");
}
std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;
return shmaddr;
}
void DetachShm(void *shmaddr)
{
if (shmaddr == nullptr)
return;
shmdt(shmaddr);
std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;
}
public:
Shm(const std::string &pathname, int proj_id, int who)
: _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr)
{
_key = GetCommKey();
if (_who == gCreater)
GetShmUseCreate();
else if (_who == gUser)
GetShmForUse();
_addrshm = AttachShm();
std::cout << "shmid: " << _shmid << std::endl;
std::cout << "_key: " << ToHex(_key) << std::endl;
}
~Shm()
{
if (_who == gCreater)
{
int res = shmctl(_shmid, IPC_RMID, nullptr);
}
std::cout << "shm remove done..." << std::endl;
}
std::string ToHex(key_t key)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", key);
return buffer;
}
bool GetShmUseCreate()
{
if (_who == gCreater)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);
if (_shmid >= 0)
return true;
std::cout << "shm create done..." << std::endl;
}
return false;
}
bool GetShmForUse()
{
if (_who == gUser)
{
_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);
if (_shmid >= 0)
return true;
std::cout << "shm get done..." << std::endl;
}
return false;
}
void Zero()
{
if(_addrshm)
{
memset(_addrshm, 0, gShmSize);
}
}
void *Addr()
{
return _addrshm;
}
void DebugShm()
{
struct shmid_ds ds;
int n = shmctl(_shmid, IPC_STAT, &ds);
if(n < 0) return;
std::cout << "ds.shm_perm.__key : " << ToHex(ds.shm_perm.__key) << std::endl;
std::cout << "ds.shm_nattch: " << ds.shm_nattch << std::endl;
}
private:
key_t _key;
int _shmid;
std::string _pathname;
int _proj_id;
int _who;
void *_addrshm;
};
#endif
client.cc
#include "Shm.hpp"
#include "namedPipe.hpp"
int main()
{
// 1. 创建共享内存
Shm shm(gpathname, gproj_id, gUser);
shm.Zero();
char *shmaddr = (char *)shm.Addr();
sleep(3);
// 2. 打开管道
NamePiped fifo(comm_path, User);
fifo.OpenForWrite();
// 当成string
char ch = 'A';
while (ch <= 'Z')
{
shmaddr[ch - 'A'] = ch;
std::string temp = "wakeup";
std::cout << "add " << ch << " into Shm, " << "wakeup reader" << std::endl;
fifo.WriteNamedPipe(temp);
sleep(2);
ch++;
}
return 0;
}
server.cc
#include "Shm.hpp"
#include "namedPipe.hpp"
int main()
{
// 1. 创建共享内存
Shm shm(gpathname, gproj_id, gCreater);
char *shmaddr = (char*)shm.Addr();
shm.DebugShm();
// // 2. 创建管道
// NamePiped fifo(comm_path, Creater);
// fifo.OpenForRead();
// while(true)
// {
// // std::string temp;
// // fifo.ReadNamedPipe(&temp);
// std::cout << "shm memory content: " << shmaddr << std::endl;
// }
sleep(5);
return 0;
}
namedPipe.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
const std::string comm_path = "./myfifo";
#define DefaultFd -1
#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
#define BaseSize 4096
class NamePiped
{
private:
bool OpenNamedPipe(int mode)
{
_fd = open(_fifo_path.c_str(), mode);
if (_fd < 0)
return false;
return true;
}
public:
NamePiped(const std::string &path, int who)
: _fifo_path(path), _id(who), _fd(DefaultFd)
{
if (_id == Creater)
{
int res = mkfifo(_fifo_path.c_str(), 0666);
if (res != 0)
{
perror("mkfifo");
}
std::cout << "creater create named pipe" << std::endl;
}
}
bool OpenForRead()
{
return OpenNamedPipe(Read);
}
bool OpenForWrite()
{
return OpenNamedPipe(Write);
}
// const &: const std::string &XXX
// * : std::string *
// & : std::string &
int ReadNamedPipe(std::string *out)
{
char buffer[BaseSize];
int n = read(_fd, buffer, sizeof(buffer));
if(n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
int WriteNamedPipe(const std::string &in)
{
return write(_fd, in.c_str(), in.size());
}
~NamePiped()
{
if (_id == Creater)
{
int res = unlink(_fifo_path.c_str());
if (res != 0)
{
perror("unlink");
}
std::cout << "creater free named pipe" << std::endl;
}
if(_fd != DefaultFd) close(_fd);
}
private:
const std::string _fifo_path;
int _id;
int _fd;
};