1. 管道 (PIPE)

管道实际是用于进程间通信的一段共享内存,创建管道的进程称为管道服务器,连接到一个管道的进程为管道客户机。一个进程在向管道写入数据后,另一进程就可以从管道的另一端将其读取出来。

管道的特点:

  1. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
  2. 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。 比如fork或exec创建的新进程,在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。
  3. 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
  4. 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。 管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

管道只能在本地计算机中使用,而不可用于网络间的通信。

使用格式为:

#include<unistd.h>
int  pipe(int fd[2]);

功能: 创建一个简单的管道,若成功则为数组fd分配两个文件描述符,其中fd[0] 用于读取管道,fd[1]用于写入管道。
返回:成功返回0,失败返回-1;
(1)管道通信是单向的,并且遵守先进先出的原则,即先写入的数据先读出。
(2)管道是一个无结构,无固定大小的字节流。
(3) 管道把一个进程的标准输出和另一个进程的标准输入连接在一起。数据读出后就意味着从管道中移走了,消失了。其它的进程都不能
再读到这些数据。就像我们平常见到的管子水流走了就没有了。 这点很重要!!
(4) pipe这种管道用于两个有亲缘关系的进程之间。eg:父子进程…

  1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。

  2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。

  3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。

#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
void sys_err(const char *str)
{perror(str);exit(1);
}
int main(void)
{pid_t pid;char buf[1024];int fd[2];char *p = "test for pipe\n";if (pipe(fd) == -1)sys_err("pipe");pid = fork();if (pid < 0) {sys_err("fork err");} else if (pid == 0) {close(fd[1]);int len = read(fd[0], buf, sizeof(buf));write(STDOUT_FILENO, buf, len);close(fd[0]);} else {close(fd[0]);write(fd[1], p, strlen(p));wait(NULL);close(fd[1]);}return 0;
}

2. 命名管道(FIFO)

也叫有名管道、FIFO 文件。允许没有亲缘关系的进程间通信。
以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。

命名管道(FIFO)和无名管道(pipe)有一些特点是相同的,不一样的地方在于:

1、FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中。
2、当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
3、FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。

命名管道的创建
所需头文件:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo( const char *pathname, mode_t mode);
功能:
命名管道的创建。
参数:

pathname: 普通的路径名,也就是创建后 FIFO 的名字。 mode: 文件的权限,与打开普通文件的 open() 函数中的
mode 参数相同,相关说明请点此链接。 返回值:

成功:0
失败:如果文件已经存在,则会出错且返回 -1。

1. #include <stdio.h>
2. #include <sys/types.h>
3. #include <sys/stat.h>
4.
5. int main(int argc, char *argv[])
6. {
7.     int ret;
8.
9.     ret = mkfifo("my_fifo", 0666); // 创建命名管道
10.     if(ret != 0){   // 出错
11.         perror("mkfifo");
12.     }
13.
14.     return 0;
15. }


后期的操作,把这个命名管道当做普通文件一样进行操作:open()、write()、read()、close()。但是,和无名管道一样,操作命名管道肯定要考虑默认情况下其阻塞特性。

细看:
https://blog.csdn.net/lianghe_work/article/details/47722175

下面验证的是默认情况下的特点,即 open() 的时候没有指定非阻塞标志( O_NONBLOCK )。

1) open() 以只读方式打开 FIFO 时,要阻塞到某个进程为写而打开此 FIFO open() 以只写方式打开 FIFO
时,要阻塞到某个进程为读而打开此 FIFO。 简单一句话,只读等着只写,只写等着只读,只有两个都执行到,才会往下执行。
2)假如 FIFO 里没有数据,调用 read() 函数从 FIFO 里读数据时 read() 也会阻塞。这个特点和无名管道是一样的。
3)通信过程中若写进程先退出了,就算命名管道里没有数据,调用 read() 函数从 FIFO 里读数据时不阻塞;若写进程又重新运行,则调用
read() 函数从 FIFO 里读数据时又恢复阻塞。
4)通信过程中,读进程退出后,写进程向命名管道内写数据时,写进程也会(收到SIGPIPE 信号)退出。 5)调用 write() 函数向 FIFO 里写数据,当缓冲区已满时 write() 也会阻塞。
4和5这两个特点和无名管道是一样的,这里不再验证,详情请看《无名管道》。

命名管道非阻塞标志操作

命名管道可以以非阻塞标志(O_NONBLOCK)方式打开:
非阻塞标志(O_NONBLOCK)打开的命名管道有以下特点:

  1. 先以只读方式打开,如果没有进程已经为写而打开一个 FIFO, 只读 open() 成功,并且 open() 不阻塞。
  2. 先以只写方式打开,如果没有进程已经为读而打开一个 FIFO,只写 open() 将出错返回 -1。
  3. read()、write() 读写命名管道中读数据时不阻塞。

3. 信号 (signal)

信号是进程间通信机制中唯一的异步通信机制
signal机制可以被理解成进程的软中断, 用信号处理来模拟操作系统的中断功能

软中断是执行中断指令产生的,而硬中断是由外设引发的。https://zhuanlan.zhihu.com/p/85597791
信号全称为软中断信号,也有人称软中断。

软中断信号(signal,又简称为信号)用来通知进程发生了异常事件。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。注意,信号只是用来通知某进程发生了什么事件,并不给该进程传递任何数据。


signal的执行点可以理解成从内核态返回用户态时,在返回时,如果发现待执行进程存在被触发的signal,那么在离开内核态之后(也就是将CPU切换到用户模式),执行用户进程为该signal绑定的signal处理函数,从这一点上看,signal处理函数是在用户进程上下文中执行的。当执行完signal处理函数之后,再返回到用户进程被中断或者system call(软中断或者指令陷阱)打断的地方。

Signal机制实现的比较灵活,用户进程由于中断或者system call陷入内核之后,将断点信息都保存到了堆栈中,在内核返回用户态时,如果存在被触发的signal,那么直接将待执行的signal处理函数push到堆栈中,在CPU切换到用户模式之后,直接pop堆栈就可以执行signal处理函数并且返回到用户进程了。Signal处理函数应用了进程上下文,并且应用实际的中断模拟了进程的软中断过程。

如果信号发送给一个正在睡眠的进程,那么要看该进程进入睡眠的优先级,如果进程睡眠在可被中断的优先级上,则唤醒进程;否则仅设置进程表中信号域相应的位,而不唤醒进程。这一点比较重要,因为进程检查是否收到信号的实际是:一个进程在即将从内核态返回到用户态时,或者在一个进程要进入或离开一个适当的低调度优先级睡眠状态时。

内核处理一个进程收到的信号实际是在一个进程从内核态返回用户态时,所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理

内 核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。

参考:https://blog.csdn.net/Thanksgining/article/details/41824475?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-1.control

3.1 信号列表

列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的),编号为32 ~ 63的信号是后来扩充的,称做可靠信号(实时信号)。不可靠信号和可靠信号的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。

.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 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 35) SIGRTMIN+1
36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5
40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9
44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13
52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5
60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1
64) SIGRTMAX

其中,举例几个常见的信号:
SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
SIGABRT
调用abort函数生成的信号。
SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
kill -9 pid 、kill -SIGKILL
SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
kill pid、kill -15 pid 、kill -SIGTERM
SIGCHLD
子进程结束时, 父进程会收到这个信号。
如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。
SIGSYS
非法的系统调用。

在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:SIGILL,SIGTRAP
默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。

3.2 信号的发送

3.2.1 信号来自内核, 生成信号的请求来自以下3个地方

(1)用户
用户可以通过输入Ctrl-C, Ctrl-\等命令,或是终端驱动程序分配给信号控制字符的其他任何键来请求内核产生信号。
(2)内核 当进程执行出错时, 内核给进程发送一个信号。
例如,非法段存取,浮点数溢出,亦或是一个非法指令,内核也利用信号通知进程特定事件发生。
(3)进程
一个进程可以通过系统调用kill给另外一个进程发送信号, 一个进程可以和另一个进程通过信号通信。

发送信号的主要函数有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。
下面只举kill的用法

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int signo)

该系统调用可以用来向任何进程或进程组发送任何信号。参数pid的值为信号的接收进程

pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程

Sinno是信号值,当为0时(即空信号),实际不发送任何信号,但照常进行错误检查,因此,可用于检查目标进程是否存在,以及当前进程是否具有向目标发送信号的权限(root权限的进程可以向任何进程发送信号,非root权限的进程只能向属于同一个session或者同一个用户的进程发送信号)。
Kill()最常用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。下面是一些可能返回的错误代码:

EINVAL:指定的信号sig无效。
ESRCH:参数pid指定的进程或进程组不存在。注意,在进程表项中存在的进程,可能是一个还没有被wait收回,但已经终止执行的僵死进程。
EPERM: 进程没有权力将这个信号发送到指定接收信号的进程。因为,一个进程被允许将信号发送到进程pid时,必须拥有root权力,或者是发出调用的进程的UID 或EUID与指定接收的进程的UID或保存用户ID(savedset-user-ID)相同。如果参数pid小于-1,即该信号发送给一个组,则该错误表示组中有成员进程不能接收该信号。

3.3 信号捕获处理

进程能够通过系统调用signal告诉内核, 它要如何处理信号, 进程有3个选择。

#define SIG_DFL ((void (*) (int)) 0) *语句A*
#define SIG_IGN ((void (*) (int)) 1)
#define SIG_ERR ((void (*) (int)) -1)
SIG_ERR(-1):信号处理函数里, 失败返回SIG_ERR。
SIG_DFL(0):默认信号处理程序
SIG_IGN(1):忽略信号的处理程序

(1)接收默认处理(通常是消亡)
例如,SIGINT的默认处理是消亡, 进程并不一定要使用signal接收默认处理,但是进程能够通过以下调用来恢复默认处理。
signal(SIGINT, SIG_DFL);
(2)忽略信号
程序可以通过以下调用来告诉内核, 它需要忽略SIGINT。
signal(SIGINT, SIG_IGN);
(3)信号处理函数
程序能够告诉内核,当程序到来时应该调用哪个函数。
signal(signum, functionname);


SIG_ERR:信号处理函数里, 失败返回SIG_ERR。

3.3.1 信号处理例子

(1)默认处理信号

#include<stdio.h>
#include<signal.h>int main()
{             signal(SIGINT,SIG_DFL); //该进程如果收到信号,则消亡int i;for( i = 0; i<10;++i){printf("hello world\n");sleep(1);}return 0;
}

这里我们使用ctrl +c,给进程发信号,进程收到信号后,由于定义SIG_DFL,所以进程会消亡。

(2)忽略信号

#include<stdio.h>
#include<signal.h>int main()
{          signal(SIGINT,SIG_IGN);int i;for( i = 0; i<10;++i){printf("hello world\n");sleep(1);}return 0;
}

3)使用信号处理处理函数

#include<stdio.h>
#include<signal.h>int main()
{         void f(int);      signal(SIGINT,f);int i;for( i = 0; i<10;++i){printf("hello world\n");sleep(1);}return 0;
}   void f(int signum)
{   printf("SIGINT\n");
}


3.4 sigaction函数使用

信号中,signal是初级用法,sigaction是高级用法

我们已经成功完成了信号的收发,那么为什么会有高级版出现呢?其实之前的信号存在一个问题就是,虽然发送和接收到了信号,可是总感觉少些什么,既然都已经把信号发送过去了,为何不能再携带一些数据呢?
正是如此,我们需要另外的函数来通过信号传递的过程中,携带一些数据。咱么先来看看发送的函数吧。

#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);struct sigaction {void       (*sa_handler)(int); //信号处理程序,不接受额外数据,SIG_IGN 为忽略,SIG_DFL 为默认动作void       (*sa_sigaction)(int, siginfo_t *, void *); //信号处理程序,能够接受额外数据和sigqueue配合使用sigset_t   sa_mask;//阻塞关键字的信号集,可以再调用捕捉函数之前,把信号添加到信号阻塞字,信号捕捉函数返回之前恢复为原先的值。int        sa_flags;//影响信号的行为SA_SIGINFO表示能够接受数据
};//回调函数句柄sa_handler、sa_sigaction只能任选其一

signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式(如果不为NULL的话)。

sigaction 是一个系统调用,根据这个函数原型,我们不难看出,在函数原型中,第一个参数signum应该就是注册的信号的编号;第二个参数act如果不为空说明需要对该信号有新的配置;第三个参数oldact如果不为空,那么可以对之前的信号配置进行备份,以方便之后进行恢复。

sigaction是信号接收函数,与之对应的是信号发生函数sigqueue

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
union sigval {int   sival_int;void *sival_ptr;
};

信号处理

struct sigaction act;
act.sa_sigaction = handler;
act.sa_flags = SA_SIGINFO;    //如果设置了SA_SIGINFO属性,说明使用的处理函数是sa_sigaction,而不是sa_handler,否则,系统会默认使用 sa_handler 所指向的信号处理函数。
sigaction(SIGIO, &act, NULL);//sa_sigaction 和 sa_handler 使用的是同一块内存空间,相当于 union,所以只能设置其中的一个,不能两个都同时设置。

举例:

demo.c

#include<signal.h>
#include<stdio.h>
#include <unistd.h>void handler(int signum, siginfo_t * info, void * context){if(signum == SIGIO)printf("SIGIO   signal: %d\n", signum);else if(signum == SIGUSR1)printf("SIGUSR1   signal: %d\n", signum);elseprintf("error\n");if(context){printf("content: %d\n", info->si_int);printf("content: %d\n", info->si_value.sival_int);}
}int main(void){struct sigaction act;act.sa_sigaction = handler;//信号处理程序,能够接受额外数据和sigqueue配合使用act.sa_flags = SA_SIGINFO;//影响信号的行为SA_SIGINFO表示能够接受数据sigaction(SIGIO, &act, NULL);sigaction(SIGUSR1, &act, NULL);for(;;){sleep(10000);}return 0;
}

使用这个函数之前,必须要有几个操作需要完成

  • 如果要获取数据,使用 sigaction 函数安装信号处理程序时,制定SA_SIGINFO 的标志。
  • sigaction 结构体中的 sa_sigaction 成员提供了信号捕捉函数。如果实现的是 sa_handler 成员,那么将无法获取额外携带的数据。

sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。可以使用 value 参数向信号处理程序传递整数值或者指针值。

sigqueue 函数不但可以发送额外的数据,还可以让信号进行排队(操作系统必须实现了 POSIX.1的实时扩展),对于设置了阻塞的信号,使用 sigqueue 发送多个同一信号,在解除阻塞时,接受者会接收到发送的信号队列中的信号,而不是直接收到一次。

但是,信号不能无限的排队,信号排队的最大值受到SIGQUEUE_MAX的限制,达到最大限制后,sigqueue 会失败,errno 会被设置为 EAGAIN。

send.c

#include <sys/types.h>
#include <signal.h>
#include<stdio.h>
#include <unistd.h>int main(int argc, char** argv){if(4 != argc){printf("[Arguments ERROR!]\n");printf("\tUsage:\n");printf("\t\t%s <Target_PID> <Signal_Number> <content>\n", argv[0]);return -1;}int pid = atoi(argv[1]);int sig = atoi(argv[2]);if(pid > 0 && sig > 0){//int sigqueue(pid_t pid, int sig, const union sigval value);union sigval val;val.sival_int = atoi(argv[3]);printf("send: %d\n", atoi(argv[3]));sigqueue(pid, sig, val);}else{printf("Target_PID or Signal_Number MUST bigger than 0!\n");}return 0;
}

4. 消息队列(Message queues)

4.1 什么是系统V IPC

System V,曾经也被称为 AT&T System V,是Unix操作系统众多版本中的一支,引入了三种高级进程间的通信机制:消息队列、共享内存和信号量

三种系统V IPC:消息队列、信号量以及共享内存(共享存储器)之间有很多相似之处。

每个内核中的 I P C结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符
( i d e n t i f i e r )加以引用。

无论何时创建I P C结构(调用m s g g e t、 s e m g e t或s h m g e t) ,都应指定一个关键字(k e y),关键字的数据类型由系统规定为 k e y _ t,通常在头文件< s y s / t y p e s . h >中被规定为长整型。关键字由内核变换成标识符。

以上简单介绍了IPC,对接下来介绍的消息队列、信号量和共享内存有助于理解。

4.2 什么是消息队列

消息队列,Unix的通信机制之一,可以理解为是一个存放消息(数据)容器。将消息写入消息队列,然后再从消息队列中取消息,一般来说是先进先出的顺序。可以解决两个进程的读写速度不同(处理数据速度不同),系统耦合等问题,而且消息队列里的消息哪怕进程崩溃了也不会消失。

消息队列本质上是位于内核空间的链表,链表的每个节点都是一条消息。每一条消息都有自己的消息类型,消息类型用整数来表示,而且必须大于 0。每种类型的消息都被对应的链表所维护:


其中数字 1 表示类型为 1 的消息,数字2、3、4 类似。彩色块表示消息数据,它们被挂在对应类型的链表上。

值得注意的是,刚刚说过没有消息类型为 0 的消息,实际上,消息类型为 0 的链表记录了所有消息加入队列的顺序,其中红色箭头表示消息加入的顺序。

特点:

  • 生命周期随内核,消息队列会一直存在,需要我们显示的调用接口删除或使用命令删除
  • 消息队列可以双向通信
  • 克服了管道只能承载无格式字节流的缺点

4.2 消息队列相关的函数

// 生成一个key(键值)
key_t ftok(const char *pathname, int proj_id);
// 创建和获取 ipc 内核对象
int msgget(key_t key, int flags);
// 将消息发送到消息队列
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 从消息队列获取消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 查看、设置、删除 ipc 内核对象(用法和 shmctl 一样,shmctl是共享内存的)
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

最简单的消息内存的使用流程

①ftok函数生成键值

②msgget函数创建消息队列

③msgsnd函数往消息队列发送消息

④msgrcv函数从消息队列读取消息

⑤msgctl函数进行删除消息队列

一个消息数据应该由以下一个结构体组成,举个例子

struct mymesg{long int mtype; //类,消息队列可以控制读取相应类型的数据,这时就不一定是先进先出的顺序了,文章后面会继续介绍
char mtext[size_t]; //数据,传递的数据存放在这里面
};

1.ftok函数生成键值

ftok 把一个已存在的路径名和一个整数标识符转换成IPC键值

每一个消息队列都有一个对应的键值(key)相关联(共享内存、信号量也同样需要)。
所需头文件#include<sys/ipc.h>
函数原型 key_t ftok(const char *path ,int id);

  • path为一个已存在的路径名
  • d为0~255之间的一个数值,代表项目ID,自己取

返回值:成功返回键值(相当于32位的int)。出错返回-1
例如:key_t key = ftok( “/tmp”, 66);

ftok的典型实现是调用stat函数,然后组合以下三个值:
① pathname所在的文件系统的信息(stat结构的st_dev成员)。
② 该文件在本文件系统内的索引节点号(stat结构的st_ino成员)。
③ proj_id的低序8位(不能为0)。
上述三个值的组合产生一个32位键。
参考:https://blog.csdn.net/andylauren/article/details/78821655

2.msgget函数创建消息队列

所需头文件#include<sys/msg.h>
函数原型 int msgget(key_t key,int flag);

key为ftok生成的键值
flag为所需要的操作和权限,可以用来控制创建一个消息队列。

  • flag的值为IPC_CREAT:如果不存在key值的消息队列,且权限不为0,则创建消息队列,并返回一个消息队列ID。如果存在,则直接返回消息队列ID。
  • flag的值为 IPC_CREAT | IPC_EXCL:如果不存在key值的消息队列,且权限不为0,则创建消息队列,并返回一个消息队列ID。如果存在,则产生错误。

返回值:成功返回消息队列ID;出错返回-1

例如:int id = msgget(key,IPC_CREAT|IPC_EXCL|0666);创建一个权限为0666(所有用户可读可写,具体查询linux权限相关内容)的消息队列,并返回一个整形消息队列ID,如果key值已经存在有消息队列了,则出错返回-1。

int id = msgget(key,IPC_CREAT|0666);创建一个权限为0666(所有用户可读可写,具体查询linux权限相关内容)的消息队列,并返回一个消息队列ID,如果key值已经存在有消息队列了,则直接返回一个消息队列ID。

3.msgsnd函数往消息队列发送消息

所需头文件#include<sys/msg.h>
函数原型 int msgsnd(int msgid,const void *ptr,size_t nbytes,int flag);

msgid:为msgget返回的消息队列ID值
ptr:为消息结构体mymesg指针 nbytes:为消息结构体mymesg里的字符数组mtext大小,sizeof(mtext) flag:值可以为0、IPC_NOWAIT

  • 为0时,当消息队列满时,msgsnd将会阻塞,直到消息能写进消息队列或者消息队列被删除。
  • 为IPC_NOWAIT时,当消息队列满了,msgsnd函数将不会等待,会立即出错返回EAGAIN

返回值:成功返回0;错误返回-1 例如:msgsnd(id,(void *)&ckxmsg,512,0);

4.msgrcv函数从消息队列读取消息

所需头文件#include<sys/msg.h>
函数原型 ssize_t msgrcv(int msgid,void *ptr,size_t nbytes,long type,int flag);

msgid:为msgget返回的消息队列ID值
ptr:为消息结构体mymesg指针
nbytes:为消息结构体mymesg里的字符数组mtext大小,sizeof(mtext)type:在结构体mymesg里我们定义了一个long int mtype,用于分别消息的类型type ==0 返回队列中的第一个消息type > 0 返回队列中消息类型为type的第一个消息type < 0 返回队列中消息类型值小于等于type绝对值的消息,如果这种消息有若干个,则取类型值最小的消息flag:可以为0、IPC_NOWAIT、IPC_EXCEPT为0时,阻塞式接收消息,没有该类型的消息msgrcv函数一直阻塞等待为IPC_NOWAIT时,如果没有返回条件的消息调用立即返回,此时错误码为ENOMSG为IPC_EXCEPT时,与msgtype配合使用返回队列中第一个类型不为msgtype的消息

返回值:成功返回消息数据部分的长度;错误返回-1
例如:msgrcv(id,(void *)&ckxmsg,512,1,0);

5.msgctl函数对消息队列进行控制

简单的操作就是删除消息队列了,也可以获取和改变消息队列的状态
所需头文件#include<sys/msg.h>

函数原型int msgctl(int msgid, int cmd, struct msqid_ds *buf);

msgid就是msgget函数返回的消息队列IDcmd有三个,常用删除消息队列的为IPC_RMID;IPC_STAT:取此队列的msqid_ds结构,并将它存放在buf指向的结构中;IPC_SET:改变消息队列的状态,把buf所指的msqid_ds结构中的uid、gid、mode复制到消息队列的msqid_ds结构内。(内核为每个消息队列维护着一个结构,结构名为msqid_ds,这里就不讲啦,里面存放着消息队列的大小,pid,存放时间等一些参数)buf就是结构体msqid_ds

返回值:成功返回0;错误返回-1
例如:msgctl(id,IPC_RMID,NULL);删除id号的消息队列

4.3 消息队列测试demo

下面为一个简单的程序,一个service和一个client,service往消息队列里写数据,client从消息队列里读数据,当service输入QUIT时删除消息队列,并且俩程序都退出。
1.service.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/msg.h>
#include<sys/ipc.h>
struct mymesg{long int mtype;char mtext[512];
};
int main()
{int id = 0;struct mymesg ckxmsg;key_t key = ftok("/tmp",66);id = msgget(key,IPC_CREAT | 0666);if(id == -1){printf("create msg error \n");return 0;}while(1){char msg[512];memset(msg,0,sizeof(msg));ckxmsg.mtype = 1;printf("input message:");fgets(msg,sizeof(msg),stdin);strcpy(ckxmsg.mtext,msg);if(msgsnd(id,(void *)&ckxmsg,512,0) < 0){printf("send msg error \n");return 0;}if(strncmp(msg,"QUIT",4) == 0)break;}if(msgctl(id,IPC_RMID,NULL) < 0){printf("del msg error \n");return 0;}return 0;
}

2.client.c

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/msg.h>
#include<sys/ipc.h>
struct mymesg{long int mtype;char mtext[512];
};
int main()
{int id = 0;struct mymesg ckxmsg;key_t key = ftok("/tmp",66);id = msgget(key,0666|IPC_CREAT);if(id == -1){printf("open msg error \n");return 0;}while(1){if(msgrcv(id,(void *)&ckxmsg,512,1,0) < 0){printf("receive msg error \n");return 0;}printf("data:%s\n",ckxmsg.mtext);if(strncmp(ckxmsg.mtext,"QUIT",4) ==0)break;}return 0;
}

运行结果
service

client

5. 信号量

参考:https://blog.csdn.net/qinxiongxu/article/details/7830537

5.1 信号量介绍

信号量的使用主要是用来保护共享资源,使得资源在一个时刻只有一个进程(线程)
所拥有。

信号量的值为正的时候,说明它空闲。所测试的线程可以锁定而使用它。若为0,说明
它被占用,测试的线程要进入睡眠队列中,等待被唤醒。

信号量广泛用于进程或线程间的同步和互斥同步是一种更为复杂的互斥,而互斥是一种特殊的同步,同步互斥的定义,参考https://blog.csdn.net/liming0931/article/details/82902084

5.2 分类:

在学习信号量之前,我们必须先知道——Linux提供两种信号量:
(1) 内核信号量,由内核控制路径使用
(2) 用户态进程使用的信号量,这种信号量又分为POSIX信号量和SYSTEM
V信号量。POSIX信号量又分为有名信号量和无名信号量。

  • 有名信号量,其值保存在文件中, 所以它可以用于线程也可以用于进程间的同步。
  • 无名信号量,其值保存在内存中。倘若对信号量没有以上的全面认识的话,你就会很快发现自己在信号量的森林里迷失了方向。

5.3 内核信号量

参考:http://blog.chinaunix.net/uid-24219701-id-3286606.html

Linux内核的信号量在概念和原理上和用户态的System V的IPC机制信号量是相同的,不过他绝不可能在内核之外使用,因此他和System V的IPC机制信号量毫不相干。

1.内核信号量的构成

内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。然而,
当内核控制路径试图获取内核信号量锁保护的忙资源时,相应的进程就被挂起。只有在资源
被释放时,进程才再次变为可运行。
内核信号量是struct semaphore类型的对象,它在<asm/semaphore.h>中定义:

struct semaphore {atomic_t count;int sleepers;wait_queue_head_t wait;}

count:相当于信号量的值,大于0,资源空闲;等于0,资源忙,但没有进程等待这
个保护的资源;小于0,资源不可用,并至少有一个进程等待资源。
wait:存放等待队列链表的地址,当前等待资源的所有睡眠进程都会放在这个链表中。
sleepers:存放一个标志,表示是否有一些进程在信号量上睡眠。

2.内核信号量中的等待队列(删除,没有联系)
上面已经提到了内核信号量使用了等待队列wait_queue来实现阻塞操作。
当某任务由于没有某种条件没有得到满足时,它就被挂到等待队列中睡眠。当条件得到满足
时,该任务就被移出等待队列,此时并不意味着该任务就被马上执行,因为它又被移进工
作队列中等待CPU资源,在适当的时机被调度。
内核信号量是在内部使用等待队列的,也就是说该等待队列对用户是隐藏的,无须用
户干涉。由用户真正使用的等待队列我们将在另外的篇章进行详解。

3.内核信号量的相关函数

定义信号量
struct semaphore sem;
初始化信号量
void sema_init (struct semaphore *sem, int val);
void init_MUTEX(struct semaphore *sem);//初始化为0static DECLARE_MUTEX(button_lock);     //定义互斥锁获得信号量
void down(struct semaphore * sem);
int down_interruptible(struct semaphore * sem);
int down_trylock(struct semaphore * sem);
释放信号量
void up(struct semaphore * sem);

举例:

在2.6.36版本后的内核就没有DECLARE_MUTEX这个宏了,取而代之的是DEFINE_SEMAPHORE宏,在后来同互斥信号量相关的init_MUTEX、init_MUTEX_LOCKED也从<linux/semaphore.h>文件中移除了。
变成#define DECLARE_MUTEX(name) __DECLARE_SEMAPHORE_GENERIC(name,1)

4.POSIX 信号量与SYSTEM V信号量的比较

  • 对POSIX来说,信号量是个非负整数。常用于线程间同步。
    而SYSTEM V信号量则是一个或多个信号量的集合,它对应的是一个信号量结构体,
    这个结构体是为SYSTEM V IPC服务的,信号量只不过是它的一部分。常用于进程间同步。
  • POSIX信号量的引用头文件是“<semaphore.h>”,而SYSTEM V信号量的引用头文件是
    “<sys/sem.h>”。
  • 从使用的角度,System V信号量是复杂的,而Posix信号量是简单。比如,POSIX信
    号量的创建和初始化或PV操作就很非常方便。

5.3 POSIX信号量详解

参考https://blog.csdn.net/qq_35433716/article/details/86382733

在 POSIX 标准中,信号量分两种,一种是无名信号量,一种是有名信号量。无名信号量一般用于线程间同步或互斥,而有名信号量一般用于进程间同步或互斥。

它们的区别和管道及命名管道的区别类似,无名信号量则直接保存在内存中,而有名信号量要求创建一个文件。

信号量用于互斥:

信号量用于同步:

1.无名信号量

无名信号量的创建就像声明一般的变量一样简单,例如:sem_t sem_id。然后再初
始化该无名信号量,之后就可以放心使用了。

常见的无名信号量相关函数:
#include <semaphore.h>
信号量数据类型为:sem_t。
1)初始化信号量

int sem_init(sem_t *sem, int pshared, unsigned int value);

2)信号量 P 操作(减 1)

int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);//以非阻塞的方式1、将信号量的值减 1。操作前,先检查信号量(sem)的值是否为 0,若信号量为 0,此函数会阻塞,直到信号量大于 0 时才进行减 1 操作
2、以非阻塞的方式来对信号量进行减 1 操作。若操作前,信号量的值等于 0,则对信号量的操作失败,函数立即返回。

3)信号量 V 操作(加 1)

int sem_post(sem_t *sem);
将信号量的值加 1 并发出信号唤醒等待线程(sem_wait())。

4)获取信号量的值

int sem_getvalue(sem_t *sem, int *sval);
获取 sem 标识的信号量的值,保存在 sval 中。

5)销毁信号量

int sem_destroy(sem_t *sem);
删除 sem 标识的信号量。

信号量用于互斥实例:


#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>sem_t sem; //信号量void printer(char *str)
{sem_wait(&sem);//减一while(*str){putchar(*str);  fflush(stdout);str++;sleep(1);}printf("\n");sem_post(&sem);//加一}void *thread_fun1(void *arg)
{char *str1 = "hello";printer(str1);}void *thread_fun2(void *arg)
{char *str2 = "world";printer(str2);}int main(void)
{pthread_t tid1, tid2;sem_init(&sem, 0, 1); //初始化信号量,初始值为 1//创建 2 个线程pthread_create(&tid1, NULL, thread_fun1, NULL);pthread_create(&tid2, NULL, thread_fun2, NULL);//等待线程结束,回收其资源pthread_join(tid1, NULL);pthread_join(tid2, NULL); sem_destroy(&sem); //销毁信号量return 0;}


信号量用于同步实例:


#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>sem_t sem_g,sem_p;   //定义两个信号量
char ch = 'a';void *pthread_g(void *arg)  //此线程改变字符ch的值
{while(1){sem_wait(&sem_g);ch++;sleep(1);sem_post(&sem_p);}}void *pthread_p(void *arg)  //此线程打印ch的值
{while(1){sem_wait(&sem_p);printf("%c",ch);fflush(stdout);sem_post(&sem_g);}}int main(int argc, char *argv[])
{pthread_t tid1,tid2;sem_init(&sem_g, 0, 0);   //初始化信号量sem_init(&sem_p, 0, 1);pthread_create(&tid1, NULL, pthread_g, NULL);pthread_create(&tid2, NULL, pthread_p, NULL);pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;}


2.有名信号量
所需头文件:

#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>

(1)创建一个有名信号量。

当有名信号量存在时使用:
sem_t *sem_open(const char *name, int oflag);
当有名信号量不存在时使用:
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);

参数:

name:信号量文件名。注意,不能指定路径名。因为有名信号量,默认放在/dev/shm 里。 flags:sem_open()函数的行为标志。
mode:文件权限(可读、可写、可执行)的设置。
value:信号量初始值。

返回值:

成功:信号量的地址
失败:SEM_FAILED

2)关闭有名信号量

#include <semaphore.h>
int sem_close(sem_t *sem);

3)删除有名信号量文件

int sem_unlink(const char *name);

4)有名信号量P 操作(减 1加一)——和无名一样

int sem_wait(sem_t *sem);   减一
int sem_post(sem_t *sem);   加 1

有名信号量实现进程间互斥功能:


#include<stdio.h>
#include<semaphore.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include <sys/wait.h>
void printer(sem_t *sem, char *str)
{sem_wait(sem); //信号量减一while(*str!='\0'){putchar(*str);  fflush(stdout);str++;sleep(1);}printf("\n"); sem_post(sem); //信号量加一}int main(int argc, char *argv[])
{pid_t pid;sem_t *sem = NULL;pid = fork(); //创建进程if(pid<0){ //出错perror("fork error");}else if(pid == 0){ //子进程//跟open()打开方式很相似,不同进程只要名字一样,那么打开的就是同一个有名信号量sem = sem_open("name_sem", O_CREAT|O_RDWR, 0666, 1); //信号量值为 1if(sem == SEM_FAILED){ //有名信号量创建失败perror("sem_open");return -1;}char *str1 = "hello";printer(sem, str1); //打印sem_close(sem); //关闭有名信号量_exit(1);}else if(pid > 0){ //父进程//跟open()打开方式很相似,不同进程只要名字一样,那么打开的就是同一个有名信号量sem = sem_open("name_sem", O_CREAT|O_RDWR, 0666, 1); //信号量值为 1if(sem == SEM_FAILED){//有名信号量创建失败perror("sem_open");return -1;}char *str2 = "world";printer(sem, str2); //打印sem_close(sem); //关闭有名信号量wait(NULL); //等待子进程结束}sem_unlink("name_sem");//删除有名信号量return 0;}


有名信号量实现进程间同步功能(print2 先打印,再到 print1 打印):
print1.c 代码如下:


#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>
#include <stdio.h>void print(sem_t *print1, sem_t *print2)
{int i = 0;while(1){sem_wait(print1);i++;printf("int print1 i = %d\n", i);sem_post(print2);}}int main(int argc, char **argv)
{   sem_t *print1, *print2;print1 = sem_open("sem_print1", O_CREAT, 0777, 0);  if(SEM_FAILED == print1){perror("sem_open");}print2 = sem_open("sem_print2", O_CREAT, 0777, 1);    if(SEM_FAILED == print2){perror("sem_open");}print(print1, print2);return 0;}

print2.c 代码如下:


#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>
#include <stdio.h>void print(sem_t *print1, sem_t *print2)
{int i = 0;while(1){sem_wait(print2);i++;printf("in print2 i = %d\n", i);sleep(1);sem_post(print1);}}int main(int argc, char **argv)
{   sem_t *print1, *print2;print1 = sem_open("sem_print1", O_CREAT, 0777, 0);  if(SEM_FAILED == print1){perror("sem_open");}print2 = sem_open("sem_print2", O_CREAT, 0777, 1);  if(SEM_FAILED == print2){perror("sem_open");}print(print1, print2);return 0;}


删除有名信号量示例代码如下:


#include <semaphore.h>
#include <stdio.h>void sem_del(char *name)
{int ret;ret = sem_unlink(name);if(ret < 0){perror("sem_unlink");}}int main(int argc, char **argv)
{sem_del("sem_print1"); //删除信号量文件sem_print1sem_del("sem_print2"); //删除信号量文件sem_print2return 0;}

6. 共享内存

参考https://blog.csdn.net/qq_35433716/article/details/86260653

6. 1 概述

共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个更改。

共享内存的特点:
1)共享内存是进程间共享数据的一种最快的方法。

一个进程向共享的内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。

2)使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥。

若一个进程正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据。

6.2 常用函数

1)创建共享内存
所需头文件:

#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size,int shmflg);

功能:

创建或打开一块共享内存区。

参数:

key:进程间通信键值,ftok() 的返回值。
size:该共享存储段的长度(字节)。
shmflg:标识函数的行为及共享内存的权限,其取值如下:
IPC_CREAT:如果不存在就创建
IPC_EXCL: 如果已经存在则返回失败
位或权限位:共享内存位或权限位后可以设置共享内存的访问权限,格式和 open() 函数的 mode_t 一样(open()的使用请点此链接),但可执行权限未使用。

返回值:

成功:共享内存标识符。
失败:-1。

示例代码如下:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define BUFSZ 1024int main(int argc, char *argv[])
{int shmid;key_t key;key = ftok("./", 2015); if(key == -1){perror("ftok");}//创建共享内存shmid = shmget(key, BUFSZ, IPC_CREAT|0666);  if(shmid < 0) { perror("shmget"); exit(-1); } return 0;}


2)共享内存映射

#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

功能:

共享内存属性的控制。

参数:

shmid:共享内存标识符。
cmd:函数功能的控制,其取值如下:
IPC_RMID:删除。(常用
)IPC_SET:设置 shmid_ds 参数,相当于把共享内存原来的属性值替换为 buf 里的属性值。IPC_STAT:保存 shmid_ds 参数,把共享内存原来的属性值备份到 buf 里。SHM_LOCK:锁定共享内存段( 超级用户 )。SHM_UNLOCK:解锁共享内存段。
SHM_LOCK 用于锁定内存,禁止内存交换。并不代表共享内存被锁定后禁止其它进程访问。其真正的意义是:被锁定的内存不允许被交换到虚拟内存中。这样做的优势在于让共享内存一直处于内存中,从而提高程序性能
buf:shmid_ds 数据类型的地址,用来存放或修改共享内存的属性。

实战示例

接下来我们做这么一个例子:创建两个进程,在 A 进程中创建一个共享内存,并向其写入数据,通过 B 进程从共享内存中读取数据。

写端代码如下:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>#define BUFSZ 512int main(int argc, char *argv[])
{int shmid;int ret;key_t key;char *shmadd;//创建key值key = ftok("./", 2015); if(key == -1){perror("ftok");}//创建共享内存shmid = shmget(key, BUFSZ, IPC_CREAT|0666); if(shmid < 0) { perror("shmget"); exit(-1); }//映射shmadd = shmat(shmid, NULL, 0);if(shmadd < 0){perror("shmat");_exit(-1);}//拷贝数据至共享内存区printf("copy data to shared-memory\n");bzero(shmadd, BUFSZ); // 共享内存清空strcpy(shmadd, "how are you, mike\n");return 0;
}

读端代码如下:


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>#define BUFSZ 512int main(int argc, char *argv[])
{int shmid;int ret;key_t key;char *shmadd;//创建key值key = ftok("./", 2015); if(key == -1){perror("ftok");}system("ipcs -m"); //查看共享内存//打开共享内存shmid = shmget(key, BUFSZ, IPC_CREAT|0666);if(shmid < 0) { perror("shmget"); exit(-1); } //映射shmadd = shmat(shmid, NULL, 0);if(shmadd < 0){perror("shmat");exit(-1);}//读共享内存区数据printf("data = [%s]\n", shmadd);//分离共享内存和当前进程ret = shmdt(shmadd);if(ret < 0){perror("shmdt");exit(1);}else{printf("deleted shared-memory\n");}//删除共享内存shmctl(shmid, IPC_RMID, NULL);  system("ipcs -m"); //查看共享内存return 0;
}

6. 套接字

参考:https://blog.csdn.net/baobao1767640830/article/details/106200463

6.1 预备知识

1、了解IP地址

1)IP协议有两个版本,IPv4和IPv6,现在用得比较多的是IPv4。

2)IP地址是在IP协议中用来标识网络中不同主机的地址。

3)对于IPv4版本来说,IP地址是一个4字节,32位的整数。

4)通常使用“点分十进制”的字符串表示IP地址,例如:“192.168.181.129”,其中用点分割的每一个数字表示一个字节,范围为0-255。

5)在IP数据报头部,有两个IP地址,分别叫做原IP地址和目的IP地址。
IP地址能够把信息发送到对方的机器上,但是还需要一个其他的标识符来区分这个数据要给哪个程序进行解析,它就是端口号。

2、认识端口号

1)端口号是具有网络功能的应用软件的标识号。

2)端口号用来标识一个进程,告诉操作系统当前的这个数据要交给哪一个进程来处理。

3)端口号是一个16位的整数,可以标识的范围是0-65535。其中,0-1023是公认端口号,即已经公认定义或为将要公认定义的软件保留的,而1024-65535是并没有公共定义的端口号,用户可以自己定义这些端口的作用。

4)IP地址+端口号能够标识网络上的某一台主机的某一个进程

5)一个进程能够占用多个端口号,但是一个端口号只能被一个进程占用(理解?)

6.2 套接字概述

  • socket也是一种进程间的通信机制,不过它与其他通信方式主要的区别是:它可以实现不同主机间的进程通信。

  • 套接字连接的过程如同(客户)打一个电话到一个大公司,接线员(服务器进程)接听电话并把它转接到你要找的部门,然后再从那里转到你要找的人(服务器套接字),然后接线员(服务器进程)再继续转接其它(客户)的电话。

  • 正如管道有两种类型(命名和无名)一样,套接字也有两种类型。IPC 套接字(即 Unix 套接字)给予进程在相同设备(主机)上基于通道的通信能力;而网络套接字给予进程运行在不同主机的能力,因此也带来了网络通信的能力。网络套接字需要底层协议的支持,例如 TCP(传输控制协议)或 UDP(用户数据报协议)。

  • 与之相反,IPC 套接字依赖于本地系统内核的支持来进行通信;特别的,IPC 通信使用一个本地的文件作为套接字地址。尽管这两种套接字的实现有所不同,但在本质上,IPC 套接字和网络套接字的 API 是一致的。

地址:为了使网络上的计算机通过唯一标识进行相互间通信
端口:解决多个程序要同时访问网络
IP协议:确定到达目的地的路由
UDP协议:向应用程序提供一种面向无连接的服务
TCP协议:提供一种面向连接的,可靠的数据传输服务

6.3 套接字接口

套接字接口是一组函数,用以创建网络应用,存放在“sys/socket.h”函数库中。
(1)socket函数
函数功能:创建一个套接字描述符。
函数原型:

int socket(int demain, int type, int protocol)

domain:地址域(版本号IPV4)。通常使用AF_INET表示32位IP地址,AF_INET是网络套接字,AF_UNIX是本地套接字
type:套接字类型,分为两种。SOCK_STREAM表示字节流类型(TCP);SOCK_DGRAM表示数据报类型(UDP)
protocol:协议。存在三种形式,IPPROTO_TCP表示TCP协议;IPPROTO_UDP表示UDP协议;“0”表示接受任何协议。

返回值:若成功返回0,失败返回-1。

socket函数目的是打开一个网络通讯端口,如果成功就像open一样返回一个文件描述符,应用程序可以像读写文件一样用read/write在网络上收发数据。

(2)bind函数
函数功能:将套接字地址和套接字描述符联系起来。
函数原型:

int bind(int sockfd, const struct socketaddr *serveraddr, socklen_t addrlen)

sockfd:套接字描述符
addr:套接字的地址
addlen:IPv4结构体的大小,即sizeof(sockaddr_in)

返回值:成功返回0,失败返回-1。

初始化举例:

struct sockaddr_in serveraddrBzero(&serveraddr, sizeof(serveraddr));Serveraddr.sin_family = AF_INET;Serveraddr.sin_port = htons(SERV_PORT);Serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

1)bzero表示将整个结构体清零。
2)网络地址为INADDR_ANY表示本地的任意IP地址。因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到某个客户端建立了连接释才确定下来到底用哪个IP地址。
3)端口号SERV_PORT在这里定义为9999。

注意:

  • a.服务器程序所监听的网络端口号和网络地址通常是固定不变,客户端得知服务器程序的地址和端口号后就可以向服务器发起连接请求。所以服务器需要绑定一个固定的网络地址和端口号。

  • b.“struct sockaddr*”是通用指针类型,实际上serveraddr参数可以接受多种协议的sockaddr结构体,所以需要第三个参数addrlen指定结构体的长度

  • c.客户端不是不允许调用bind函数,只是没有必要调用bind函数固定一个端口。否则,如果在同一台机器上启动多个客户端,就会出现端口号被占用导致不能正确建立连接。

  • d.服务器也不是必须调用bind函数,但是如果服务器不调用bind函数,内核就会自动给服务器分配监听端口,每次启动服务器时端口号都不一样,客户端要连接服务器就会遇到麻烦。

(3)listen函数
函数功能:服务器告知内核,套接字被服务器使用了,转化为监听套接字,接受客户端的连接请求。
函数原型:

int listen(int sockfd, int backlog)

sockfd:套接字描述符。
backlog:表示同一时间最大的并发数。

listen函数声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。 一般backlog的值不会太大,backlog的设置主要是为了提高客户端与服务器的连接效率,太大了会消耗很多内存,得不偿失。

(4)connect函数
函数功能:客户端通过调用connect函数来建立和服务器的连接。
函数原型:

int connect(int clientfd, const struct sockaddr *serve_addr, socklen_t addrlen)

clientfd:客户端套接字描述符。
serve_addr:服务器套接字地址。
addrlen:IPv4结构体的大小,即sizeof(struct sockaddr_in)。

onnect函数和bind函数的参数形式一样,区别在于bind的参数是自己的地址,而connect的参数是对方的地址

(5)accept函数
函数功能:服务器通过调用accept函数接收来自客户端的连接请求。
函数原型:

int accept(int listenfd, struct sockaddr* cli_addr, int *addrlen)

sockfd:等待客户端的连接请求侦听描述符
cli_addr:客户端套接字地址
addrlen:套接字地址长度

注意:
1.三次握手完成后,服务器调用accept函数接受连接。

2.服务器如果调用accept函数时还没有客户端的连接请求,就阻塞等待直到客户端连接上来。

3.addr是一个传出参数,accept函数返回时传出客户端的地址和端口号。

4.如果给addr参数传NULL,则表示不关心客户端的地址。

5.addrlen参数是一个传入传出参数,传入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出的问题,传出的是客户端地址结构的实际长度。

6. 4 套接字地址结构

1、sockaddr结构
sockaddr结构是套接字的总体结构,包括16位的地址类型和14字节的地址数据,如下图所示。

问题:在connect、bind和accept函数中要求一个指向与协议相关的的套接字地址结构的指针。如何能接受各种类型的套接字地址结构?

解决办法:定义套接字函数要求一个指向通用sockaddr结构的指针,然后要求应用程序将与协议特定的结构的指针强制转换成这个通用的指针结构。

2、sockaddr_in结构


IPv4地址用sockaddr_in结构体表示,包括16位的地址类型、16位的端口号和32位的IP地址,其中IPv4的地址用AF_INET表示。

sockaddr_in结构体内容如下所示:

struct sockaddr_in {short int           sin_family;  /* Address family */
unsigned short int  sin_port;    /* Port number */
struct in_addr      sin_addr;   /* Internet address */
unsigned char       sin_zero[8];  /* Same size as struct sockaddr */
};

参数: sin_family:指代协议族,用AF_INET指代IPv4。
sin_port:端口号,要使用网络字节序(大端存储)。
sin_addr:IP地址,使用in_addr这个数据结构。
sin_zero :是为了让sockaddr与sockaddr_in两个数据结构保持大小相同而保留的空字节。

数据的转换

1、网络字节序

网络字节序指数据在网络中的存储方式。
内存中的多字节数据对于内存地址有大端小端之分,磁盘文件中的多字节数据对于文件偏移量有大端小端之分,网络数据流同样有大端小端之分。
TCP/IP协议规定:网络数据流应采用大端存储,即高字节低地址。

字节顺序转换函数:
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换,它们存在<arpa/inet.h>头文件中。

1)uint32_t htonl(uint32_t hostlong);
2)uint16_t htons(uint16_t hostshort);
3)uint32_t ntohl(uint32_t netlong);
4)uint16_t ntohs(uint16_t netshort);

注意:
h表示host,n表示network,l表示32位长整数,s表示16位短整数,to表示转换。即:htonl表示将32位整数由主机字节序转换为网络字节序,返回网络字节序的值;ntohl表示将32位整数由网络字节序转换为主机字节序,返回主机字节序的值。htons和ntohs函数为16位无符号整数执行相应的转换

2、地址转换函数
网络字节顺序的IP地址是二进制的数据,为了方便使用需要转换为点分十进制的字符串。例如:128.2.194.242就是地址0x8002c2f2的点分十进制表示。应用程序可以使用以下库函数实现IP地址与点分十进制串的转换,它们存放在<arpa/inet.h>头文件中。

int inet_pton(AF_INET, const char *src, void *dst);
/*
该函数将一个点分十进制串转换为一个二进制的网络字节顺序的IP地址。如果src没有指向一个合法的点分十进制字符串,那么该函数返回0。成功返回1,失败返回-1。
*/
const char* inet_ntop(AF_INET, const void* src, char* dst,socklen_t size);
/*
该函数将一个二进制的网络字节顺序的IP地址转换为它对应的点分十进制的字符串,并把得到的以null结尾的字符串复制到dst。成功返回指向点分十进制的指针,失败返回NULL
*/

6.5 套接字模型

套接字分为基于UDP协议的套接字和TCP协议的套接字

//服务器
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>#define _PORT_ 9999
#define _BACKLOG_ 10int main()
{int sock = socket(AF_INET, SOCK_STREAM, 0);if(sock < 0){printf("create socket error, errno is: %d, errstring is: %s\n", errno, strerror(errno));}struct sockaddr_in server_socket;struct sockaddr_in client_socket;bzero(&server_socket, sizeof(server_socket));server_socket.sin_family = AF_INET;server_socket.sin_port = htons(_PORT_);server_socket.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sock, (struct sockaddr*)&server_socket, sizeof(struct sockaddr_in)) < 0){printf("bind error, error code is: %d, error string is:%s\n", errno, strerror(errno));close(sock);return 1;}if(listen(sock, _BACKLOG_) < 0){printf("listen error, error code is:%d, error string is: %s\n", errno, strerror(errno));close(sock);return 2;}printf("bind and listen success, wait accept...\n");for(; ;){socklen_t len = 0;int client_sock = accept(sock, (struct sockaddr*)&client_socket, &len);if(client_sock < 0){printf("accept error, errno is: %d, error string is; %s\n", errno, strerror(errno));close(sock);return 3;}char buf_ip[INET_ADDRSTRLEN];     memset(buf_ip, '\0', sizeof(buf_ip));inet_ntop(AF_INET, &client_socket.sin_addr, buf_ip, sizeof(buf_ip));//存放客户端套接字的地址printf("get connect, ip is: %s, port is: %d\n", buf_ip, ntohs(client_socket.sin_port));while(1){char buf[1024];memset(buf, '\0', sizeof(buf));read(client_sock, buf, sizeof(buf));printf("client say:%s\n", buf);printf("server say:");memset(buf, '\0', sizeof(buf));fgets(buf, sizeof(buf), stdin);buf[strlen(buf)-1] = '\0';write(client_sock, buf, strlen(buf)+1);printf("plase wait...\n");}}close(sock);return 0;
}//客户端
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>#define SERVER_PORT 9999
#define SERVER_IP "192.168.181.129"
int main(int argc, char *argv[])
{if(argc != 2){printf("Usage: /client IP\n");return 1;}char *str = argv[1];char buf[1024];memset(buf, '\0', sizeof(buf));struct sockaddr_in server_sock;int sock = socket(AF_INET, SOCK_STREAM, 0);bzero(&server_sock, sizeof(server_sock));server_sock.sin_family = AF_INET;inet_pton(AF_INET, SERVER_IP, &server_sock.sin_addr);server_sock.sin_port = htons(SERVER_PORT);int ret = connect(sock, (struct sockaddr*)&server_sock, sizeof(server_sock));if(ret < 0){printf("connect failed..., error code is:%d, error string is:%s\n", errno, strerror(errno));return 1;}printf("connect success...\n");while(1){printf("client say:");fgets(buf, sizeof(buf), stdin);buf[strlen(buf)-1] = '\0';write(sock, buf, sizeof(buf));   if(strncasecmp(buf, "quit", 4) == 0){printf("quit!\n");break;}printf("please wait...\n");read(sock, buf, sizeof(buf));printf("server say:%s\n", buf);}close(sock);return 0;
}


若再启动一个客户端尝试连接服务器,发现第二个客户端无法与服务器连接成功。因为调用accept接受一个请求之后,就在while循环里一直尝试read,没有调用accept函数接受客户端的连接请求,而导致连接失败。

解决方法:

1)通过每个请求,创建子进程的方式来支持多连接
示例代码:

注意理解:接收请求时,先由父进程创建子进程,再由子进程创建孙子进程,然后由孙子进程来处理与客户端的交互,为什么?
答:假设不创建孙子进程,由子进程处理与客户端的交互,这时父进程一直在等待子进程的退出而不执行它下面的代码,显然是不行的。创建了孙子进程,由孙子进程处理与客户端的交互,子进程退出,父进程回收子进程,孙子进程被init进程领养。
2)通过每个请求,创建一个线程的方式来支持多连接

#include<stdlib.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<pthread.h>typedef struct Arg
{int fd;struct sockaddr_in addr;
}Arg;void *CreateWorker(void* ptr)
{Arg* arg = (Arg*)ptr;ProcessRequest(arg->fd, &arg->addr);free(arg);return NULL;
}void ProcessRequest(int client_fd, struct sockaddr_in *client_addr)
{char buf[1024] = {0};for( ; ;){ssize_t read_size = read(client_fd, buf,  sizeof(buf));ssize_t read_size = read(client_fd, buf, sizeof(buf));if(read_size < 0){perror("read error!\n");continue;}if(read_size == 0){printf("client: %s say bye!\n", inet_ntoa(client_addr->sin_addr));close(client_fd);break;}buf[read_size] = '\0';printf("client:%s say :%s\n", inet_ntoa(client_addr->sin_addr), buf);write(client_fd, buf, strlen(buf));}return 0;
}int main(int argc, char *argv[])
{if(argc != 3){perror("Usage;./tid_server IP PORT");return 1;}struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(atoi(argv[2]));addr.sin_addr.s_addr = inet_addr(argv[1]);int fd = socket(AF_INET, SOCK_STREAM, 0);if(fd < 0){perror("socket error!\n");return 1;}int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));if(ret < 0){perror("bind error!\n");return 1;}ret = listen(fd, 5);if(ret < 0){perror("listen error!\n");return 1;}for(; ;){struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);int client_fd = accept(fd, (struct sockaddr*)&client_addr, &len);if(client_fd < 0){perror("accept error!\n");continue;}pthread_t tid = 0;Arg* arg = (Arg*)malloc(sizeof(Arg));arg->fd = client_fd;arg->addr = client_addr;pthread_create(&tid, NULL, CreateWorker, (void*)arg);pthread_detach(tid);}return 0;
}

linux内核学习1:进程间通信IPC相关推荐

  1. 我的Linux内核学习笔记

    在开始今天的内容之前,其实有一些题外话可以和大家分享一下.自从工作以来,我个人一直都有一个观点.那就是怎么样利用简单的代码来说明开发中的问题,或者是解释软件中的原理,这是一个很高的学问.有些道理看上去 ...

  2. Linux内核学习--内存管理模块

    Linux内核学习--内存管理模块 首先,Linux内核主要由五个部分组成,他们分别是:进程调度模块.内存管理模块.文件系统模块.进程间通信模块和网络接口模块. 本部分所讲的内存是内存管理模块,其主要 ...

  3. linux网卡配子接口,linux 内核学习(2).

    linux 内核学习(2). (2011-07-18 01:45:46) 标签: 杂谈 linux内核源码树基本构造 由于linux的原代码继续在改变,因而不可能给出太翔实的内容,只能指出一个特异的驱 ...

  4. Linux内核学习路线

    [推荐阅读] 手把手教你如何编写一个Makefile文件 一文讲解,Linux内核--内存管理(建议收藏) 当Linux内存耗尽时,改如何处理! 一文看懂页面置换算法 内核学习路线 很多同学接触Lin ...

  5. 【转载】linux内核学习书籍

    [转载]linux内核学习书籍 1, 关于操作系统理论的最初级的知识.不需要通读并理解<操作系统概念><现代操作系统>等巨著,但总要知道分时(time-shared)和实时(r ...

  6. linux内核学习之三:linux中的32位与64位

    linux内核学习之三:linux中的"32位"与"64位" 在通用PC领域,不论是windows还是linux界,我们都会经常听到"32位" ...

  7. Linux疑难杂症解决方案100篇(十五)-万字长文带你深入Linux 内核学习:环境搭建和内核编译

    一.linux内核学习之一:环境搭建--安装Debian7.3 本系列文章假设读者已对linux有一定的了解,其实学习linux内核不需要有很深的关于linux的知识,只需要了解以下内容:linux基 ...

  8. 操作系统进程学习(Linux 内核学习笔记)

    操作系统进程学习(Linux 内核学习笔记) 进程优先级 并非所有进程都具有相同的重要性.除了大多数我们所熟悉的进程优先级之外,进程还有不同的关键度类别,以满足不同需求.首先进程比较粗糙的划分,进程可 ...

  9. Linux内核学习-字符设备驱动学习(二)

    在Linux内核学习-字符设备驱动学习(一)中编写字符设备驱动的一种方法,但是需要手动创建设备节点. 有没有能够自动的创建设备节点的呢? 有!使用class_create()和device_creat ...

  10. Linux内核学习编译流程

    一.前言 linux内核学习 1.安装vmware虚拟机或者virtualbox,再安装发行版本linux 2.www.kernel.org,挑选一个内核版本 3.进行解压并编译 4.自己写一些模块( ...

最新文章

  1. Simplivity存储家族推新:满足所有闪存需求和更好的灾难恢复
  2. Netty入门之Netty的基本介绍和IO模型
  3. 把阿里巴巴的核心系统搬到云上,架构上的挑战与演进是什么?
  4. Windows Service:用C#创建Windows Service
  5. 美术外包管理从原始1.0到工业4.0
  6. Qt 有层级关系的qss样式,使用id定位到具体控件
  7. ADO.NET,浅显
  8. 九个数的全排列(避免重复出现)
  9. Linux完全删除用户
  10. go语言结构体标签的意义
  11. map和json数组以及JSON对象和的区别以及相互转换
  12. 要打理好自己的钱袋子
  13. 大数据处理的基本流程
  14. SD-WAN 系列--企业专线、企业互联网
  15. 类名+单书名号——泛型,什么是泛型?如何使用泛型
  16. Python3 post请求上传文件
  17. 10G SR光模块取消ER调试可行性分析
  18. PT100所谓的二线制,三线制,四线制如何接线(详解)
  19. 不联网也传染!新型病毒通过USB无线传输传播
  20. python微信好友分析_基于python实现微信好友数据分析(简单)

热门文章

  1. 如何使用计算机自带的刻录软件,win7自带刻录软件怎么用?win7如何用自带刻录软件...
  2. 开发动态网站所需的构件
  3. 儿童监控录像软件:用电脑摄像头即可监控儿童
  4. 【3D视觉创新应用竞赛作品系列​】 基于点云的视觉引导系统
  5. libcurl.dll丢失怎么办?libcurl.dll丢失的解决方法
  6. c语言怎样画sin点阵,《C语言及程序设计》实践参考——输出点阵图
  7. 17素材网免费下载方法,不用会员,免费超简单方法!
  8. 福建省烟草专卖准运证网络管理系统
  9. 【评委评语】星光评委精彩点评大集合!
  10. 腾讯出品的软件有哪些?这6款软件你都知道吗?