【转】进程间的五种通信方式
每一个进程想要访问物理内存,都是通过访问进程虚拟地址空间当中的虚拟地址,借助页表的映射来访问的。这里的虚拟地址空间和页表都是进程级的,保证了进程之间的数据独立,不会相互干扰。但是,进程之间也是要相互合作的,简单的理解进程间通信就是多个进程对同一份公共资源进行操作,而通信最重要的前提是保证进程能看到同一份资源。
IPC 概念
进程间通信常用的几种方式
管道:速度慢,容量有限,只有父子进程或兄弟进程能通讯,是半双工的通信,数据只能单向流动
FIFO:任何进程间都能通讯,但速度慢
消息队列:
共享内存:能够很容易控制容量,速度快,但是要保持同步,比如一个进程在写的时候,另一个进程要注意读的问题。
信号:不能传递复杂消息,用于通知接收进程某个事件已经发生。主要作为进程间以及同一进程不同线程之间的同步手段。通过给进程发送相应信号,该进程接收到此信号后运行信号处理函数或者作出默认或忽略回应
套接字:可用于不同机器间的进程通信
特点:
函数原型:
1 #include <unistd.h>
2 int pipe(int fd[2]); // 返回值:若成功返回0,失败返回-1
fd ‐ 传出参数:
fd[0] ‐ 读端
fd[1] ‐ 写端
返回值:
0:成功
‐1:创建失败
读操作
写操作
#include
#include
#include
int main()
{
int ret;
int fd[2];
ret = pipe(fd); // 创建匿名管道
if (ret == -1)
{
printf("create pipe failed!~\n");
exit(1);
}
pid_t pid = fork(); // 创建父子进程
if (pid == -1)
{
printf("fork failed!~");
exit(1);
}
// 父进程 执行 ps aux
if (pid > 0)
{
close(fd[0]); // 操作写端要先关闭读端
dup2(fd[1], STDOUT_FILENO); // 把原本输出在 STDOUT 的内容重定向至管道写端
execlp("ps", "ps", "aux", NULL);//原本输出在 STDOUT 的内容重定向至管道写端
perror("execlp");
exit(1);
}
// 子进程 执行 grep "bash"
else if (pid == 0)
{
close(fd[1]); // 操作读端要先关闭写端
dup2(fd[0], STDIN_FILENO); // 把原本输出在 STDIN 的内容重定向至管道读端
execlp("grep", "grep", "bash", "--color=auto", NULL);//原本输出在 STDIN 的内容重定向至管道读端
perror("execlp");
}
close(fd[0]);
close(fd[1]);
return 0;
}
特点:
函数原型:
1 #include <sys/stat.h>
2 // 返回值:成功返回0,出错返回-1
3 int mkfifo(const char *pathname, mode_t mode);
其中的 mode 参数与open
函数中的 mode 相同。
一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。
mode:
返回值:
#include
#include
#include
#include
int main()
{ // 创建FIFO管道
if(mkfifo("./file", 0666) == -1 && errno!=EEXIST){
printf("mkfifo failuer\n");
perror("why");
}
return 0;
}
当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK
)的区别:
O_NONBLOCK
(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。【只读只写都会阻塞,只有当另一个进程读写该FIFO文件时才会打通】【管道的创建就是为了交换数据,要阻塞防止一直读取数据】O_NONBLOCK
,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。mkfifo.c
#include
#include
#include
#include
#include
int main()
{
int ret;
int fd;
int nread;
char readBuf[50] = {0};
ret = mkfifo("/home/fangjiarong/test/myfifo", 0777); // 创建 fifo,以绝对路径创建
if (ret == -1)
{
perror("mkfifo");
return -1;
}
else
{
printf("creat fifo succeed!~\n");
}
fd = open("./myfifo", O_RDONLY); // 打开 fifo,要与另一个进程相同的路径
if (fd < 0)
{
perror("fd");
return -1;
}
else
{
printf("open fifo succeed!~\n");
}
nread = read(fd, readBuf, sizeof(readBuf)); //只读程序会阻塞,等待只写程序完成
printf("read %d byte from fifo %s:\n", nread, readBuf);
close(fd);
return 0;
}
write.c
#include
#include
#include
#include
#include
#include
int main()
{
char *str = "Hello World!~";
int fd;
fd = open("./myfifo", O_WRONLY);// 打开 fifo,要与另一个进程相同的路径
if (fd < 0)
{
perror("fd");
return -1;
}
else
{
printf("open fifo succeed!~\n");
}
write(fd, str, strlen(str));
close(fd);
return 0;
}
消息队列结构体:
// 消息结构体
struct msg_form {
long mtype;//消息类型
char mtext[256];//消息zwe
};
函数原型:
#include
int msgget(key_t key, int flag);
// 创建或打开消息队列
参数:
返回值:
在以下两种情况下,msgget
将创建一个新的消息队列:
IPC_CREAT
标志位。(消息队列的权限也写在flag位置)IPC_PRIVATE
。int msgsnd(int msqid, const void *ptr, size_t size, int flag);
// 添加消息
参数:
返回值:
int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
//从一个消息队列中获取消息
参数:
返回值:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// 控制消息队列
参数:
msqid:消息队列的队列ID
cmd:cmd常用参数为IPC_RMID。表示移除链表
buf:是指向 msgid_ds 结构的指针,它指向消息队列模式和访问权限的结构,一般第三个参数写null
返回值:
key_t ftok( const char * fname, int id )
//key = ftok(".", 1); 这样就是将fname设为当前目录。
在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。
如指定文件的索引节点号为65538,换算成16进制为0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。
查询文件索引节点号的方法是:(当前路径下的索引号)
ls -i
显示当前消息队列信息
ipcs -q
实现流程:
msg_service.c
#include
#include
#include
#include
#include
#include
#include
struct msgbuf // 消息队列结构体
{
long mtype;
char mtext[128];
};
int main()
{
struct msgbuf sendbuf, readbuf;
int msgId;
key_t key;
int readret;
pid_t pid;
key = ftok("a.c", 1); // 获取 key 值
msgId = msgget(key, IPC_CREAT | 0755); // 创建消息队列
if (msgId == -1)
{
printf("create message queue failed!~\n");
perror("msgget");
return -1;
}
printf("create msgage queue succeeded!~ msgId = %d\n", msgId);
system("ipcs -q"); // 显示当前消息队列信息
// init msgbuf
sendbuf.mtype = 100;
pid = fork();
// parent process write 100
if (pid > 0)
{
while (1)
{
memset(sendbuf.mtext, 0, 128);
printf("please input to message queue:\n");
fgets(sendbuf.mtext, 128, stdin);
// send message to message queue
msgsnd(msgId, (void *)&sendbuf, strlen(sendbuf.mtext), 0);
}
}
// child process read 200
if (pid == 0)
{
while (1)
{
memset(readbuf.mtext, 0, 128);
msgrcv(msgId, (void *)&readbuf, 128, 200, 0);
printf("datas from client: %s\n", readbuf.mtext);
}
}
return 0;
}
msg_client.c
#include
#include
#include
#include
#include
#include
#include
struct msgbuf // 消息队列结构体
{
long mtype;
char mtext[128];
};
int main()
{
struct msgbuf sendbuf, readbuf;
int msgId;
key_t key;
int readret;
pid_t pid;
key = ftok("a.c", 1); // 获取 key 值
msgId = msgget(key, IPC_CREAT | 0755); // 创建消息队列
if (msgId == -1)
{
printf("create message queue failed!~\n");
perror("msgget");
return -1;
}
printf("create msgage queue succeeded!~ msgId = %d\n", msgId);
system("ipcs -q"); // 显示当前消息队列信息
// init msgbuf
sendbuf.mtype = 200;
pid = fork();
// child process write 200
if (pid == 0)
{
while (1)
{
memset(sendbuf.mtext, 0, 128);
printf("please input to message queue:\n");
fgets(sendbuf.mtext, 128, stdin);
// send message to message queue
msgsnd(msgId, (void *)&sendbuf, strlen(sendbuf.mtext), 0);
}
}
// parent process read 100
if (pid > 0)
{
while (1)
{
memset(readbuf.mtext, 0, 128);
msgrcv(msgId, (void *)&readbuf, 128, 100, 0);
printf("datas from service: %s\n", readbuf.mtext);
}
}
return 0;
}
共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。
特点:
函数原型:
#include
int shmget(key_t key, size_t size, int flag);
// 创建或获取一个共享内存:
IPC_CREAT
标志位// 连接共享内存到当前进程的地址空间
void *shmat(int shm_id, const void *addr, int flag);
// 断开与共享内存的连接
int shmdt(void *addr);
// 控制共享内存的相关信息
int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
查看系统中的共享内存
ipcs -m
删除系统中的共享内存的命令
ipcrm -m shmid
//写入shmid号
实现步骤
shmWrite.c
#include
#include
#include
#include
#include
#include
#include
int main()
{
int shmId;
int key;
char *p;
key = ftok("a.c", 1); // 生成 key 值
if (key < 0)
{
perror("ftok");
return -1;
}
printf("ftok succeed!~ key = %x\n", key);
shmId = shmget(key, 128, IPC_CREAT | 0777); // 创建共享内存
if (shmId < 0)
{
perror("share memory:");
return -1;
}
printf("create share memory succeeded!~ shmId = %d\n", shmId);
system("ipcs -m");
p = (char *)shmat(shmId, NULL, 0); // 共享内存映射,配置可读可写的方式由系统自动映射
if (p == NULL)
{
perror("share memory function");
return -1;
}
memset(p, 0, 128);
// write to share memory
while (1)
{
fgets(p, 128, stdin);
}
return 0;
}
shmRead.c
#include
#include
#include
#include
#include
#include
#include
int main()
{
int shmId;
int key;
char *p;
key = ftok("a.c", 1);
if (key < 0)
{
perror("ftok");
return -1;
}
printf("ftok succeed!~ key = %x\n", key);
shmId = shmget(key, 128, 0);
if (shmId < 0)
{
perror("share memory:");
return -1;
}
printf("create share memory succeeded!~ shmId = %d\n", shmId);
system("ipcs -m");
p = (char *)shmat(shmId, NULL, 0);
if (p == NULL)
{
perror("share memory function");
return -1;
}
while (1)
{
sleep(5);
printf("share memory data = %s\n", p);
memset(p, 0, 128);
}
// shmdt(p);
return 0;
}
Linux 信号(signal)
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。主要作为进程间以及同一进程不同线程之间的同步手段。通过给进程发送相应信号,该进程接收到此信号后运行信号处理函数或者作出默认或忽略回应
对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号,为 Linux 提供了一种处理异步事件的方法。比如,终端用户输入了 ctrl+c 来中断程序,会通过信号机制停止一个程序。
信号的名字和编号:
每个信号都有一个名字和编号,这些名字都以“SIG”开头
信号定义在signal.h
头文件中,信号名都定义为正整数。
具体的信号名称可以使用kill -l
来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN
信号名 | 含义 | 默认操作 |
---|---|---|
SIGHUP | 该信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一会话内的各个作业与控制终端不再关联。 | 终止 |
SIGINT | 该信号在用户键入INTR字符(通常是Ctrl-C)时发出,终端驱动程序发送此信号并送到前台进程中的每一个进程。 | 终止 |
SIGQUIT | 该信号和SIGINT类似,但由QUIT字符(通常是Ctrl-)来控制。 | 终止 |
SIGKILL | 该信号在一个进程企图执行一条非法指令时(可执行文件本身出现错误,或者试图执行数据段、堆栈溢出时)发出。 | 终止 |
SIGFPE | 该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出及除数为0等其它所有的算术的错误。 | 终止 |
SIGKILL | 该信号用来立即结束程序的运行,并且不能被阻塞、处理和忽略。 | 终止 |
SIGALRM | 该信号当一个定时器到时的时候发出。 | 终止 |
SIGSTOP | 该信号用于暂停一个进程,且不能被阻塞、处理或忽略。ctrl+z |
暂停 |
SIGTSTP | 该信号用于暂停交互进程,用户可键入SUSP字符(通常是Ctrl-Z)发出这个信号。 | 暂停 |
SIGCHLD | 子进程改变状态时(exit函数),父进程会收到这个信号 | 忽略 |
SIGABORT | 该信号用于结束进程 | 终止 |
信号通信的框架
信号的使用 :
kill 9 PID
来杀死进程。比如,我在后台运行了一个 top 工具,通过 ps aux | grep top 命令可以查看他的 PID,通过 kill 9 来发送了一个终止进程的信号来结束了 top 进程。如果查看信号编号和名称,可以发现9对应的是 9) SIGKILL,正是杀死该进程的信号。而以下的执行过程实际也就是执行了9号信号的默认动作——杀死进程。对于信号来说,最大的意义不是为了杀死信号,而是实现一些异步通讯的手段 (即捕捉信号)
信号的处理:
SIGKILL
和SIGSTOP
)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程,显然是内核设计者不希望看到的场景man 7 signal
来查看系统的具体定义kill
sigqueue
高级版会携带参数,而不只是入门版的只关注函数
#include
#include
int kill(pid_t pid, int sig);
#include
#include
函数原型:
int raise(int sig);
发信号给自己 == kill(getpid(), sig)
函数原型
#include
unsigned int alarm(unsigned int seconds)
alarm 与 raise 函数的比较:
相同点:
不同点:
#include
int sigqueue(pid_t pid, int sig, const union sigval value);
//联合体参数成员
union sigval {
int sival_int;//发送信号携带的参数为int
void *sival_ptr; //发送信号携带的参数为指针
};
int main(int argc, char** argv){
int pid = atoi(argv[1]);
int sig = atoi(argv[2]);
}
使用这个函数之前,必须要有几个操作需要完成:
signal
sigaction
功能:注册信号处理函数,当系统收到该信号时,系统会执行信号处理函数
#include
typedef void (*sighandler_t)(int);
//sighandler_t表示函数指针,该指针指向函数,函数的返回值为void
//参数是 int 类型,显然也是信号产生的类型,方便使用一个函数来处理多个信号
// typedef主要用于变量类型的定义别名
sighandler_t signal(int signum, sighandler_t handler);
//signum 显然是信号的编号,handler 是中断函数的指针
//返回值是sighandler_t,即函数指针
//第二个参数的数据类型是sighandler_t,表示函数指针,此时传递函数的地址值
//此时signnum参数收集的是信号名称对应的信号编号
void handler(int signum){
printf("get signum=%d\n",signum);
}
signal第二个参数也可以是宏:
利用信号处理函数实现一个进程kill另一个进程
int main(int argc, char** argv){
int pid = atoi(argv[1]);//将字符串转化为整型
int signum = atoi(argv[2]);
char cmd[128]={0};//command
sprintf(cmd,"kill -%d %d",signum,pid);
//sprintf指的是字符串格式化命令,函数声明为 int sprintf(char *string, char *format [,argument,...]);,
//主要功能是把格式化的数据写入某个字符串中,即发送格式化输出到 string 所指向的字符串。sprintf 是个变参函数
system(cmd);//该函数表示执行一条指令,该指令以字符串形式作为参数
return 0;
}
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//第一个参数signum应该就是注册的信号的编号;
//第二个参数act如果不为空说明需要对该信号有新的配置;函数配置写在第二个参数里面;结构体指针,将结构体取地址值
//第三个参数oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。可以为NULL
//需要配置的结构体中的成员
struct sigaction {
void (*sa_handler)(int);
//信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作
void (*sa_sigaction)(int, siginfo_t *, void *);
//信号处理函数,能够接受额外数据和sigqueue配合使用
//第一个参数信号的编号名称
//第二个参数siginfo_t *是结构体指针,主要适用于记录接收信号的一些相关信息。例如si_int 或si_value.sival_int。要用->指向成员
//第三个参数void* 是接收到信号所携带的额外数据;指针为空的时候没有数据,反之则有,有数据的时候才操作siginfo结构体
sigset_t sa_mask;
//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。
int sa_flags;
//影响信号的行为SA_SIGINFO表示能够接受数据
};
//回调函数句柄sa_handler、sa_sigaction只能任选其一
//signinfo_t结构体
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused
hardware-generated signal
(unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
int si_band; /* Band event */
int si_fd; /* File descriptor */
}
union sigval {
int sival_int;
void *sival_ptr;
};
理解:
struct sigaction {
void (*sa_sigaction)(int, siginfo_t *, void *);
//变量类型为void (*) (int ,siginfo_t*,void*),变量名为sa_sigaction
int sa_flags;//变量类型为int,变量名为sa_flags
}
执行流程:
#include
#include
#include
#include
#include
#include
void sig_usr1(int signum) // 10号信号处理函数
{
int i = 0;
while (i < 5)
{
i++;
printf("receive signum = %d, i = %d\n", signum, i);
sleep(1);
}
}
void sig_chld(int signum) // 17号信号处理函数
{
printf("receive signum = %d \n", signum);
printf("回收子进程\n");
wait(NULL); // 回收子进程,防止其变成僵尸进程
}
int main()
{
pid_t pid;
pid = fork();
if (pid > 0) // 父进程
{
int i = 0;
signal(10, sig_usr1); // 10号 SIGUSR1 信号
signal(17, sig_chld); // 17号 SIGCHLD 信号
while (1)
{
i++;
printf("parent process i = %d\n", i);
sleep(1);
}
}
else if (pid == 0) // 子进程
{
sleep(3);
kill(getppid(), 10);
exit(0); // 相当于给父进程发送17号信号 kill(getppid, 17)
}
return 0;
}
信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。
它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
临界资源:多个进程共享资源,然而资源一次只能给一个进程使用则称为临界资源,例如输入机,打印机等
解决了共享内存中多个进程同时操作共享内存的问题,当一个进程在操作时另一个进程不能使用该共享内存,即信号量用于管理临界资源,
将信号量类比于钥匙,房间类比于临界资源,p操作拿钥匙,v操作放回钥匙
信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。
信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
支持信号量组。
#include
#include
#include
// 创建或获取一个信号量组或获取一个已经存在的信号量的键值。
int semget(key_t key, int num_sems, int sem_flags);
#include
#include
#include
// 初始化信号量
int semctl(int semid, int sem_num, int cmd, ...);
// 联合体,用于semctl初始化
union semun
{
int val; /*for SETVAL*/
struct semid_ds *buf;
unsigned short *array;
}
#include
//pv操作函数都是调用semop函数
// 对信号量组进行操作,改变信号量的值:也就是使用资源还是释放资源使用权
int semop(int semid, struct sembuf semoparray[], size_t numops);
//struct sembuf结构体
struct sembuf
{
short sem_num; // 信号量组中对应的序号,0~sem_nums-1
short sem_op; // 信号量值在一次操作中的改变量,p操作时-1 v操作时-1
short sem_flg; // IPC_NOWAIT, SEM_UNDO(当进程终止的时候自动结束对钥匙的操作),0阻塞,1非阻塞
}
//定义结构体变量
struct sembuf sops;
sops.sem_num =
sops.sem_op =
sops.sem_flg =
//定义结构体数组
struct sembuf sops[2];
sops[0].sem_num =
sops[0].sem_op =
sops[0].sem_flg =
参数说明:
返回值
代码实例:
#include
#include
#include
#include
#include
#include
#define SEM_READ 0
#define SEM_WRITE 1
union semun
{
int val;
};
void P_operation(int index, int semId)
{
struct sembuf sop;
sop.sem_num = index; //信号灯编号
sop.sem_op = -1; // P 操作
sop.sem_flg = 0; // 阻塞
semop(semId, &sop, 1);
}
void V_operation(int index, int semId)
{
struct sembuf sop;
sop.sem_num = index; //信号灯编号
sop.sem_op = 1; // V 操作
sop.sem_flg = 0; // 阻塞
semop(semId, &sop, 1);
}
int main()
{
key_t key;
key = ftok("b.c", 123); // 获取 key 值
pid_t pid;
int semId; // 信号灯 ID
int shmId; // 共享内存 ID
char *shamaddr;
semId = semget(key, 2, IPC_CREAT | 0755); // 创建信号量
if (semId < 0)
{
perror("semget error");
return -1;
}
shmId = shmget(key, 128, IPC_CREAT | 0755); // 创建共享内存
if (shmId < 0)
{
perror("shmget error");
return -1;
}
// Init semaphore
union semun myun;
// Init semaphore read
myun.val = 0;
semctl(semId, SEM_READ, SETVAL, myun); // 对 SEM_READ 信号量设置初始值
// Init semaphore write
myun.val = 1;
semctl(semId, SEM_WRITE, SETVAL, myun); // 对 SEM_WRITE 信号量设置初始值
pid = fork();
// child process
if (pid == 0)
{
while (1)
{
shamaddr = (char *)shmat(shmId, NULL, 0); // 共享内存映射
P_operation(SEM_READ, semId); // P 操作
printf(" get share memory is: %s\n", shamaddr); // 操作对共享资源
V_operation(SEM_WRITE, semId); // V 操作
}
}
// parent process
else if (pid > 0)
{
while (1)
{
shamaddr = (char *)shmat(shmId, NULL, 0);
P_operation(SEM_WRITE, semId);
printf("please input to share memory\n");
fgets(shamaddr, 32, stdin);
V_operation(SEM_READ, semId);
}
}
return 0;
}