进程间通信方式 超详细解析全站最全
进程间通信概述
进程间通信有如下一些目的:
数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
IPC发展
linux进程间通信(IPC)由以下几部分发展而来
早期UNIX进程间通信、基于System V进程间通信、基于Socket进程间通信和POSIX进程间通信。
UNIX进程间通信方式包括:管道、FIFO、信号
System V进程间通信方式包括:System V消息队列、System V信号灯、System V共享内存。
POSIX进程间通信包括:posix消息队列、posix信号灯、posix共享内存。
当前IPC技术
现在linux使用的进程间通信方式:
(1)管道(pipe)和命名管道(FIFO)
(2)信号(signal)
(3)消息队列
(4)共享内存
(5)信号量
(6)套接字(socket)
A:信号
1.信号的概念
信号是UNIX系统响应某些状况而产生的事件,进程在接收到信号时会采取相应的行动。
信号是因为某些错误条件而产生的,比如内存段冲突、浮点处理器错误或者非法指令等
它们由shell和终端管理器产生以引起中断。
进程可以生成信号、捕捉并响应信号或屏蔽信号
2.信号名称
信号的名称是在头文件 signal.h里定义的
3.signal库函数
如果想让程序能够处理信号,可以使用signal库函数,要引入头文件<signal.h>
原型:
void (*signal(int sig, void (*func)(int))) (int);
signal是一个带sig和func两个参数的函数,准备捕捉或屏蔽的信号由参数sig给出,接收到指定信号时将要调用的函数由func给出。func这个函数必须有一个int类型的参数(即接收到的信号代码),它本身的类型是voidfunc也可以是下面两个特殊值:SIG_IGN 屏蔽该信号SIG_DFL 恢复默认行为
4.signal示例
通过Ctrl-C组合键发出中断信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int ctrl_c_count=0;
/* 定义一个函数指针,用来保存signal调用的返回值 */
void(* old_handler)(int);
void ctrl_c(int);
main()
{int c;old_handler=signal(SIGINT,ctrl_c);while ((c=getchar())!='\n');signal(SIGINT,old_handler);for(;;);
}
void ctrl_c(int signum)
{/* 信号处理完毕,信号的默认处理方式被还原,所以要重新关联 */signal(SIGINT,ctrl_c);++ctrl_c_count;printf("ctrl-c count = %d\n",ctrl_c_count);
}
5.发送信号
进程可以通过调用kill向包括它本身在内的另一个进程发送信号。如果程序没有发送该信号的权限,对kill的调用就将失败。
int kill(pid_t pid, int sig);
kill函数的作用是把参数sig给定的信号发送给标识号为pid的进程。要想发送一个信号,发送者进程必须拥有相应的权限。这通常意味着两个进程必须拥有同样的用户ID
6.不可靠信号
linux信号机制基本上是从unix系统中继承过来的。早期unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,它的主要问题是:
进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失。
linux支持不可靠信号,但是对不可靠信号机制做了改进:在调用完信号处理函数后,不必重新调用该信号的安装函数(信号安装函数是在可靠机制上的实现)。因此,linux下的不可靠信号问题主要指的是信号可能丢失。
7.可靠信号
随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种unix版本分别在这方面进行了研究,力图实现"可靠信 号"。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。
同时,信号的发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction()。
8.信号在内核中的表示
执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。信号在内核中的表示可以看作是这样的:
9.实时信号
早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31,SIGRTMAX=63),将来可能进一步增加,这需要得到内核的支持。前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。实时信号是POSIX标准的一部分,可用于应用进程。
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。
10.信号集操作函数
#include <signal.h>/* Clear all signals from SET. */int sigemptyset(sigset_t *set);// 清空信号集/* Set all signals in SET. */int sigfillset(sigset_t *set);// 所有信号加进去 32 33 是没有的int sigaddset(sigset_t *set, int signo);// 增加信号int sigdelset(sigset_t *set, int signo);//删除信号集的某个信号int sigismember(const sigset_t *set, int signo);// 信号是否在集合里面/* Return 1 if SIGNO is in SET, 0 if not. */
11.sigprocmask
#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
功能:读取或更改进程的信号屏蔽字。
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
how参数的含义
12.sigpending
#include <signal.h>int sigpending(sigset_t *set);
功能: sigpending读取当前进程的未决信号集,通过set参数传出。
返回值:若成功则为0,若出错则为-1
13.信号操作函数示例
#include <unistd.h>
#include <signal.h>
#include <stdio.h>void printsigset(sigset_t* set)
{int i;for (i=1;i<64;i++){if (sigismember(set,i))putchar('1');elseputchar('0');}puts("");
}void handler(int sig)
{if (sig==SIGQUIT){sigset_t s;sigemptyset(&s);sigaddset(&s,SIGINT);sigprocmask(SIG_UNBLOCK,&s,NULL);}if (sig==SIGINT){printf("recv a signal %d\n",sig);}
}int main(void)
{sigset_t s;sigset_t p;signal(SIGINT,handler);signal(SIGQUIT,handler);sigemptyset(&s);sigaddset(&s,SIGINT);sigprocmask(SIG_BLOCK,&s,NULL);for (;;){sigpending(&p);printsigset(&p);sleep(1);}
}
14.sigaction库函数
包含头文件<signal.h>
功能:sigaction函数用于改变进程接收到特定信号后的行为。
原型:
int sigaction(int signum,const struct sigaction *act,const struct sigaction *old);
参数
该函数的第一个参数为信号的值,可以为除sigkill及sigstop外的任何一 个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)
第二个参数是指向结构sigaction的一个实例的指针,在结构 sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理
第三个参数oldact指向的对象用来保存原来对相应信号 的处理,可指定oldact为null。
返回值:函数成功返回0,失败返回-1
15.sigaction结构体
第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等
struct sigaction
{ union{ __sighandler_t _sa_handler; void (*_sa_sigaction)(int,struct siginfo *, void *);
}_u
sigset_t sa_mask;
unsigned long sa_flags;
void (*sa_restorer)(void);
}
联合数据结构中的两个元素_sa_handler以*_sa_sigaction指定信号关联函数,即用户指定的信号处理函数。除了可以是用户自定义的处理函数外,还可以为sig_dfl(采用缺省的处理方式),也可以为sig_ign(忽略信号)。由_sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;由_sa_sigaction是 指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使 用(posix没有规范使用该参数的标准),第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值。
16.sigqueue库函数
功能:新的发送信号系统调用,主要是针对实时信号提出的支持信号带有参数,与函数sigaction()配合使用。
原型:
int sigqueue(pid_t pid, int sig, const union sigval value);
参数:sigqueue的第一个参数是指定接收信号的进程id,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。
返回值成功返回0,失败返回-1
17.sigval联合体
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。
typedef union sigval{ int sival_int; void *sival_ptr;
}sigval_t;
采用联合数据结构,说明siginfo_t结构中si_value要么持有一个4字节的整数值,要么持有一个指针,这就构成了与信号相关的数据。
在信号的处理函数中,包含这样的信号相关数据指针,但没有规定具体如何对这些数据进行操作,操作方法应该由程序开发人员根据具体任务事先约定。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。
18.abort函数
包含头文件<stdlib.h>
功能:向进程发送sigabort信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。
原型:
void abort(void);
说明:即使sigabort被进程设置为阻塞信号,调用abort()后,sigabort仍然能被进程接收。该函数无返回值。
19.alarm函数
功能:专门为sigalrm信号而设,在指定的时间seconds秒后,将向进程本身发送sigalrm信号,又称为闹钟时间。
原型:
unsigned int alarm(unsigned int seconds)
参数:seconds为零,那么进程内将不再包含任何闹钟时间。
返回值,如果调用alarm()前,进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0
说明:进程调用alarm后,任何以前的alarm()调用都将无效。
20.setitimer函数
包含头文件<sys/time.h>
功能setitimer()比alarm功能强大,支持3种类型的定时器
原型:
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));
参数
第一个参数which指定定时器类型
第二个参数是结构itimerval的一个实例,结构itimerval形式
第三个参数可不做处理。
返回值:成功返回0失败返回-1
21.三种定时器
itimer_real: 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程
itimer_virtual 设定程序执行时间,经过指定的时间后,内核将发送SIGVTALRM信号给本进程
itimer_prof 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送SIGPROF信号给本进程
B:管道
1.什么是管道
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
我们通常把是把一个进程的输出连接或“管接”(经过管道来连接)到另一个进程的输入。
2.在shell中使用管道
链接shell命令:把一个进程的输出直接馈入另一个的输入,命令格式如下
cmd1 | cmd2
cmd1的标准输入来自终端键盘
cmd1的标准输出馈入cmd2做为它的标准输出
cmd2的标准输出连接到终端屏幕上
shell所做的工作从最终效果上看是这样的:重新安排标准输入和输出流之间的连接,使数据从键
盘输入流过两个命令再输出到屏幕
3.管道特点
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
4.进程管道
不需要使用shell的底层函数pipe函数
需要使用shell来解释给定命令的高级函数popen和pclose.popen函数实际上调用了pipe函数。
5.pipe函数
包含头文件<unistd.h>
功能:创建一无名管道
原型
int pipe(int file_descriptor[2]);
参数:file_descriptor:文件描述符数组,其中file_descriptor[0]表示读端,file_descriptor[1]表示写端
返回值:成功返回0,失败返回错误代码
6.pipe函数示例
通过pipe在父子进程之间传递数据
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>int main()
{int fda[2];char buf[1];int curflag;if (pipe(fda)==-1){printf("error\n");}switch (fork()){case -1:printf("error\n");break;case 0:read(fda[0],buf,1);printf("%c\n",buf[0]);close(fda[1]);close(fda[0]);break;default:write(fda[1],"a",1);close(fda[0]);close(fda[1]);break;}
}
7.管道读写规则
如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生
当没有数据可读时,read调用就会阻塞,即进程暂停执行,一直等到有数据来到为止。
如果管道的另一端已经被关闭,也就是没有进程打开这个管道并向它写数据时,read调用就会阻塞
如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;
当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。
向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。
8.复制文件描述符dup
复制文件描述符可以有三种办法:dup、dup2 、fcntl
dup系统调用从头开始搜文件描述符数组,并且在找到第一个空闲文件描述符时完成它的复制。
#include <unistd.h>int dup(int oldfd);
执行dup 前后的效果
9.复制文件描述符 dup2
#include <unistd.h>int dup2(int oldfd, int newfd);
dup2系统调用复制操作之前,如果newfd已被打开,先关闭。
10.复制文件描述符fcntl
包含头文件<unistd.h><fcntl.h>
功能:实现对文件描述符的多种控制
原型:
int fcntl(int oldfd,F_DUPFD,int minifd)
参数
Minifd:从指定的minifd开始搜索空闲文件描述符,找到时,复制oldfd,返回值为新的文件描述符。
Oldfd:要被复制的文件描述符
说明:另外fcntl还有其他的作用。
11.dup示例
通过popen实现cat /etc/passwd | wc -l
#include <unistd.h>
#include <stdio.h>int main()
{int fda[2];if (pipe(fda)==-1){fprintf(stderr,"error creating pipe\n");exit(1);}switch (fork()){case -1:fprintf(stderr,"error forking child\n");exit(1);break;case 0: /* 在子女进程中运行ls */close(1); /* 关闭标准输出 */dup(fda[1]); /* 复制管道写端 */close(fda[1]); /* 关闭管道写端 */close(fda[0]); /* 关闭管道读端 */execlp("ls","ls",0); /* 执行ls命令 */fprintf(stderr,"error trying to exec ls\n");exit(1);break;default: /* 在父进程中运行wc */close(0); /* 关闭标准输入 */dup(fda[0]); /* 复制管道读端 */close(fda[0]); /* 关闭管道读端 */close(fda[1]); /* 关闭管道写端 */execlp("wc","wc","-w",0); /* 执行wc命令 */fprintf(stderr,"error trying to exec wc\n");exit(1);break;}
}
12.popen函数
作用:允许一个程序把另外一个程序当作一个新的进程来启动,并能对它发送数据或接收数据
FILE* popen(const char *command,const char *open_mode);
command:待运行程序的名字和相应的参数open_mode:必须是“r”或“w”如果操作失败,popen会返回一个空指针
每个popen调用都必须指定“r”或“w”,在popen的标准实现里不支持任何其他的选项
如果open_mode是“r”,调用者程序利用popen返回的那个“FILE*”类型的指针用一般的stdio库函数(比如fread)就可以读这个文件流
如果open_mode是“w”,调用者程序就可以使用fwrite向被调用命令发送数据
13.pclose函数
int pclose(FILE *stream_to_close);
pclose调用只有在popen启动的进程结束之后才能返回。如果在调用pclose的时候它仍然在运行,pclose将等待该进程的结束
14.popen示例
通过popen实现cat /etc/passwd | wc -l
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>int main()
{FILE *read_fp;FILE *write_fp;char buffer[BUFSIZ+1];int chars_read;memset(buffer,0,sizeof(buffer));read_fp=popen("cat /etc/passwd","r");if (read_fp!=NULL){chars_read=fread(buffer,sizeof(char),BUFSIZ,read_fp);pclose(read_fp);if (chars_read > 0){write_fp=popen("wc -l","w");if (write_fp!=NULL){fwrite(buffer,sizeof(char),strlen(buffer),write_fp);pclose(write_fp);exit(EXIT_SUCCESS);}}}exit(EXIT_FAILURE);
}
15.简单的客户-服务器例子
客户从标准输入(stdin)读取一个路径名,并把它写入IPC通道。服务器从该IPC通道读出这个路径名,并打开其文件来读。如果服务器能读入其中的内容并写入另外一个IPC中以作给客户端的反应,否则它就响应一个出错信息写入到另外一个IPC中。客户从IPC中读取消息并显示到标准输出(stdout)
16.客服服务器分析
管道是单数据是单向流动的,通过上面的图例我们知道客户和服务器程序之间数据流动是双向的
因此我们比必须在客户和服务器程序之间建立两个管道,两个管道之间数据流向是相反。
客户和服务器分别用父子进程来实现
我们用父进程实现客户端程序,读取由客户从标准屏幕中输入的文件路径
用子进程实现服务器程序,它通过在父进程创建的管道来读取文件路径信息,通过调用open函数来检查父进程传过来的文件信息是否合法。
如果文件是合法的就创建一个新的管道,数据从子进程流向父进程。通过read函数读取文件内容然后写入流向父进程的数据
17.命名管道: FIFO文件
管道应用的一个限制就是只能在相关的程序之间进行,这些程序是由一个共同的祖先进程启动的。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
18.创建一个命名管道
命名管道可以从命令行上创建,推荐的命令行方法是使用下面这个命令: $ mkfifo filename
命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);int mknod(const char *filename,mode_t mode|S_IFIFO,(dev_t) 0);
19.用mkfifo创建一个命名管道
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{int res = mkfifo(“/tmp/my_fifo”,0777);if(res==0) printf(“FIFO created\n”);exit(EXIT_SUCCESS);
}
20.访问一个FIFO文件
FIFO和通过pipe调用创建的管道不同,它是一个有名字的文件而表示一个打开的文件描述符。
在对它进行读或写操作之前必须先打开它
FIFO文件也要用open和close函数来打开或关闭,除了一些额外的功能外,整个操作过程与我们前面介绍的文件操作是一样的
传递给open调用的是一个FIFO文件的路径名,而不是一个正常文件的路径名
21.用open打开FIFO文件
在打开FIFO文件时需要注意一个问题:即程序不能以O_RDWR模式打开FIFO文件进行读写
如果确实需要在程序之间双向传递数据的话,我们可以同时使用一对FIFO或管道,一个方向配一个;还可以用先关闭再重新打开FIFO的办法明确地改变数据流的方向
22.命名管道的打开规则
如果当前打开操作是为读而打开FIFO时,若已经有相应进程为写而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为写而打开该FIFO(当前打开操作设置了阻塞标志,即只设置了O_RDONLY),反之,如果当前打开操作没有设置了非阻塞标志,即O_NONBLOCK,则返回成功
如果当前打开操作是为写而打开FIFO时,如果已经有相应进程为读而打开该FIFO,则当前打开操作将成功返回;否则,可能阻塞直到有相应进程为读而打开该FIFO(当前打开操作设置了阻塞标志);或者,返回ENXIO错误(当前打开操作没有设置阻塞标志)。
23.打开FIFO文件
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FIFO_NAME “/tmp/my_fifo”int main(int argc , char *argv[]){int res;int op_m = 0;if(argc < 2) {fprintf(stderr,”Usage: %s<some \ combination of O_RDONLY O_WRONLY or \ O_NONBLOCK>\n”,*arg);exit(EXIT_FAILURE);}argv++;
if(strncmp(*argv,”O_RDONLY”,8)==0)op_m|=O_RDONLY;
If(strncmp(*argv,”O_WRONLY”,8)==0)op_m|=O_WRONLY;
if(strncmp(*argv,”O_NONBLOCK”,10)==0)op_m |= O_NONBLOCK;
argv++;
if(*argv) {if(strncmp(*argv,”O_RDONLY”,8)==0)op_m|=O_RDONLY;If(strncmp(*argv,”O_WRONLY”,8)==0)op_m|=O_WRONLY;if(strncmp(*argv,”O_NONBLOCK”,10)==0)op_m |= O_NONBLOCK;
}
if(access(FIFO_NAME, F_OK) == -1) {res = mkfifo(FIFO_NAME,0777);if(res != 0) {fprintf(stderr,”Could not create fifo\ %s\n”,FIFO_NAME);exit(EXIT_FAILURE);}
}
printf(“Process %d opening FIFO\n”,getpid());
res = open(FIFO_NAME,op_m);
printf(“Process %d result %d\n”,getpid(),res);
sleep(5);
if(res != -1) close(res);
printf(“Process %d finished\n”,getpid());
exit(0);}
24.命名管道读写规则
如果有进程写打开FIFO,且当前FIFO内没有数据,则对于设置了阻塞标志的读操作来说,将一直阻塞。对于没有设置阻塞标志读操作来说则返回-1,当前errno值为EAGAIN,提醒以后再试。
对于设置了阻塞标志的读操作说,造成阻塞的原因有两种:当前FIFO内有数据,但有其它进程在读这些数据;另外就是FIFO内没有数据。解阻塞的原因则是FIFO中有新的数据写入,不论信写入数据量的大小,也不论读操作请求多少数据量。
如果没有进程写打开FIFO,则设置了阻塞标志的读操作会阻塞
对于没有设置阻塞标志的写操作:
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。在写满所有FIFO空闲缓冲区后,写操作返回
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。如果当前FIFO空闲缓冲区能够容纳请求写入的字节数,写完后成功返回;如果当前FIFO空闲缓冲区不能够容纳请求写入的字节数,则返回EAGAIN错误,提醒以后再写。
25.利用FIFO文件实现进程间通信
使用两个独立的程序,一个负责生产数据,即向FIFO文件中写入数据,另一个专门负责读数据,这两个程序都是使用阻塞模式的FIFO文件
26.生产数据程序
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#define FIFO_NAME “/tmp/my_fifo”
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024*1024*10)
int main()
{ int pipe_fd;int res;int op_mode = O_WRONLY;int bytes_sent = 0;char buffer[BUFFER_SIZE + 1];if(access(FIFO_NAME,F_OK) == -1) {res = mkfifo(FIFO_NAME, 0777);if(res != 0) exit(EXIT_FAILURE);}pipe_fd = open(FIFO_NAME,op_mode);if(pipe_fd != -1) {while(bytes_sent < TEN_MEG) {res = write(pipe_fd,buffer,BUFFER_SIZE);if(res == -1 ) exit(EXIT_FAILURE);bytes_sent += res;}close(pipe_fd);}else {exit(EXIT_FAILURE);}exit(0);
}
27.读数据程序
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <limits.h>
#define FIFO_NAME “/tmp/my_fifo”
#define BUFFER_SIZE PIPE_BUFint main()
{int pipe_fd;int res;int op_mode = O_RDONLY;char buffer[BUFFER_SIZE + 1] = {0};int bytes_read = 0;pipe_fd = open(FIFO_NAME,op_mode);
If(pipe_fd != -1) {do {res = read(pipe_fd,buffer,BUFFER_SIZE);bytes_read += res;} while(res > 0);close(pipe_fd);
}
else {exit(EXIT_FAILURE);
}
printf(“Process %d finished,%d bytesread\n”,getpid(),bytes_read);
exit(0);
}
C:消息队列
1.什么是消息队列
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
消息队列也有管道一样的不足,就是每个数据块的最大长度是有上限的,系统上全体队列的最大总长度也有一个上限
消息队列是消息的链表,存放在内核中并由消息队列标识符表示。内核为每个IPC对象维护了一个数据结构struct ipc_perm,用于标识消息队列,让进程知道当前操作的是哪个消息队列。每一个msqid_ds表示一个消息队列,并通过msqid_ds.msg_first、msg_last维护一个先进先出的msg链表队列,当发送一个消息到该消息队列时,把发送的消息构造成一个msg的结构对象,并添加到msqid_ds.msg_first、msg_last维护的链表队列
2.消息队列函数
包含头文件:<sys/msg.h><sys/types.h>和<sys/ipc.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);int msgget(key_t key, int msgflg);msgrcv(int msqid, struct msgbuf *msgp, size_t msgsz, long msg_typ, int msgflg);msgsnd(int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg);
3.msgget函数
作用:用来创建和访问一个消息队列
原型:
int msgget(key_t key, int msgflg);
key: 某个消息队列的名字msgflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的如果操作成功,msgget将返回一个非负整数,即该消息队列的标识码;如果失败,则返回“-1”
4.msgsnd函数
作用:把一条消息添加到消息队列里去
原型:
int msgsnd(int msgid,const void *msg_ptr,size_t msg_sz,int msgflg);
msgid: 由msgget函数返回的消息队列标识码msg_ptr:是一个指针,指针指向准备发送的消息,msg_sz:是msg_ptr指向的消息长度,这个长度不能保存消息类型的那个“long int”长整型计算在内msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情操作成功,返回“0”,如果失败,则返回“-1”
msgflg=IPC_NOWAIT表示队列满不等待,返回EAGAIN错误。
消息结构在两方面受到制约。首先,它必须小于系统规定的上限值;其次,它必须以一个“long int”长整数开始,接收者函数将利用这个长整数确定消息的类型
最好把消息结构定义为下面这个样子:
struct msgbuf {long mtype;char mtext[1];}
5.msgrcv函数
作用:是从一个消息队列里检索消息
int msgrcv(int msgid, void *msg_ptr,size_t msgsz,long int msgtype,int msgflg);
msgid: 由msgget函数返回的消息队列标识码msg_ptr:是一个指针,指针指向准备接收的消息,msgsz:是msg_ptr指向的消息长度,这个长度不能保存消息类型的那个“long int”长整型计算在内msgtype:它可以实现接收优先级的简单形式msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事操作成功,返回实际放到接收缓冲区里去的字符个数,如果失败,则返回“-1”
msgtype=0返回队列第一条信息msgtype>0返回队列第一条类型等于msgtype的消息msgtype<0返回队列第一条类型小于等于msgtype绝对值的消息msgflg=IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误。msgflg=MSG_NOERROR,消息大小超过msgsz时被截断msgtype>0且msgflg=MSC_EXCEPT,接收类型不等于msgtype的第一条消息。
6.msgctl函数
作用:消息队列的控制函数,与共享内存的控制函数很相似
int msgctl(int msqid,int command,strcut msqid_ds *buf);
msqid: 由msgget函数返回的消息队列标识码command:是将要采取的动作,(有三个可取值)如果操作成功,返回“0”;如果失败,则返回“-1”
command:将要采取的动作(有三个可取值),分别如下:
7.消息对列---接收者
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>struct my_msg_st {long int my_msg_type;char some_text[BUFSIZ];
};
int main() {int running = 1;int msgid;struct my_msg_st some_data;long int msg_to_receive = 0;msgid = msgget((key_t)1234,0666| IPC_CREAT);if(msgid == -1) {fprintf(stderr,”msgget failed with error:%d\n”,errno);exit(EXIT_FAILURE); };while(running) {if(msgrcv(msgid,(void*)&some_data,BUFSIZ,msg_to_receive,0)==-1) {fprintf(“stderr,”msgrecv failed,error:%d\n”,errno);exit(EXIT_FAILURE); }printf(“You wrote %s”,some_data.some_text);if(strncmp(some_data.some_text,”end”,3) == 0) { running = 0;}}if(msgctl(msgid,IPC_RMID,0) == -1) {fprintf(stderr,”msgctl failed\n”);exit(EXIT_FAILURE);}exit(EXIT_SUCCESS);
}
8.消息对列---发送者
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MAX_TEXT 512
struct my_msg_st {long int my_msg_type;char some_text[MAX_TEXT];
};
int main() {int running = 1;int msgid;struct my_msg_st some_data;char buffer[BUFSIZ];msgid = msgget((key_t)1234,0666|IPC_CREAT);if(msgid == -1) {fprintf(stderr,”msgget failed with error:%d\n”,errno);exit(EXIT_FAILURE);}while(running) {printf(“Enter some text: ”);fgets(buffer,BUFSIZ,stdin);some_data.my_msg_type = 1;strcpy(some_data.some_text,buffer);if(msgsnd(msgid,(void*)&some_data,MAX_TEXT,0) == -1) {fprintf(stderr,”msgsnd failed\n”);exit(EXIT_FAILURE);}if(strncmp(buffer,”end”,3) == 0) running = 0;}exit(0);
}
D:共享内存
1.什么是共享内存
共享内存允许两个不相关的进程去访问同一部分逻辑内存
如果需要在两个运行中的进程之间传输数据,共享内存将是一种效率极高的解决方案
2.共享内存概述
共享内存是由IPC为一个进程创建的一个特殊的地址范围,它将出现在进程的地址空间中。
其他进程可以把同一段共享内存段“连接到”它们自己的地址空间里去。
所有进程都可以访问共享内存地址,就好像它们是有malloc分配的一样
如果一个进程向这段共享内存写了数据,所做的改动会立刻被有权访问同一段共享内存的其他进程看到
3.共享内存函数
void *shmat(int shmid, const void *shmaddr, int shmflg);int shmdt(const void *shmaddr);int shmctl(int shmid, int cmd, struct shmid_ds *buf);int shmget(key_t key, int size, int shmflg);
4.shmget函数
作用:用来创建共享内存
原型:
int shmget(key_t key,size_t size, int shmflg);
key: 这个共享内存段的名字size: 需要共享的内存量shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的如果共享内存创建成功,shmget将返回一个非负整数,即该段共享内存的标识码;如果失败,则返回“-1”
5.shmat函数
作用:共享内存段刚被创建的时候,任何进程还都不能访问它,为了建立对这个共享内存段的访问渠道,必须由我们来把它连接到某个进程的地址空间,shmat函数就是用来完成这项工作的。
void* shmat(int shm_id,const void *shm_addr,int shmflg);
shm_id: shmget返回的共享内存标识shm_addr:把共享内存连接到当前进程去的时候准备放置它的那个地址shmflg是一组按位OR(或)在一起的标志。它的两个可能取值是SHM_RND和SHM_RDONLY•调用成功,返回一个指针,指针指向共享内存的第一个字节,如果失败,则返回“-1”
shmaddr为0,核心自动选择一个地址
shmaddr不为0且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为0且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
在fork() 后,子进程继承已连接的共享内存
在exec后,已连接的共享内存会自动脱离(detach)
在结束进程后,已连接的共享内存会自动脱离(detach)
6.shmdt函数
作用:把共享内存与当前进程脱离开
int shmdt(const void *shm_addr);
shm_addr: 由shmat返回的地址指针操作成功,返回“0”,失败则返回“-1”脱离共享内存并不等于删除它,只是当前进程不能再继续访问它而已
7.shmctl函数
作用:共享内存的控制函数
int shmctl(int shm_id,int command,struct shmid_ds *buf);
shm_id: 由shmget返回的共享内存标识码command:将要采取的动作(有三个可取值)buf:指向一个保存着共享内存的模式状态和访问权限的数据结构操作成功,返回0,失败则返回-1
command:将要采取的动作(有三个可取值),分别如下:
8.共享内存示例程序
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define TEXT_SZ 2048
Struct shared_use_st {int written_by_you;char some_test[TEXT_SZ];
};
int main(){int running = 1;void *shared_memory = (void*)0;struct shared_use_st *shared_stuff;int shmid;srand((unsigned int)getpid()); shmid=shmget((key_t)1234,sizeof(struct shared_use_t),0666|IPC_CREATE);if(shmid == -1){fprintf(stderr,”shmget failed\n”);exit(EXIT_FAILURE);}shared_memory = shmat(shmid,(void*)0,0);if(shared_memory == (void*)-1) {fprintf(stderr,”shmat failed\n”);exit(EXIT_FAILURE);}printf(“\nMemory attached at %X\n”,(int)shared_memory);shared_stuff = (struct shared_use_st*)shared_memory;shared_stuff ->written_by_you = 0;while(running) {if(shared_stuff->written_by_you) {printf(“You wrote : %s”,shared_stuff->some_text);sleep(rand() % 4);shared_stuff->written_by_you = 0;if(strncmp(shared_stuff->some_text,”end”,3) == 0) {running = 0;}}}if(shmdt(shared_memory) == -1) {fprintf(stderr,”shmdt failed\n”);exit(EXIT_FAILURE); }if(shmctl(shmid,IPC_RMID,0) == -1) exit(EXIT_FAILURE);exit(0);
}
E:网络基础 和 SOCKET
1.远程通信的基础
通信双方
双方共同约定和遵循的协议
2.异地进程通信
协议层为双方的主机上通信进程分配“端口”和缓冲区,以便异地进程间的通信
协议层:类似于物流公司
端口:类似于物流公司两地的加盟点
缓冲区:类似于加盟点的囤放货物平台
3.TCP/IP协议概述
TCP/IP是互联网的基础
OSI参考模型与TCP/IP参考模型
4.TCP/IP协议族
TCP/IP 实际上是一个一起工作的通信家族,为网际数据通信提供通路。为讨论方便可将
TCP/IP 协议组大体上分为三部分:
1.Internet 协议(IP)
2.传输控制协议(TCP)和用户数据报文协议(UDP)
3.处于TCP 和UDP 之上的一组协议专门开发的应用程序。它们包括:TELNET,文件传送协议(FTP),域名服务(DNS)和简单的邮件传送程序(SMTP)等许多协议。
5.应用层协议
Telnet文件传送协议(FTP 和TFTP)简单的文件传送协议(SMTP)域名服务(DNS)等协议
6.网络编程基础
socket概述
为了简化开发通信程序的工作,由Berkely学校开发了一套网络通信程序的API函数标准
socket标准被扩展成window socket和unix socket
linux中的网络编程通过socket接口实现。Socket既是一种特殊的IO,它也是一种文件描述符。一个完整的Socket 都有一个相关描述{协议,本地地址,本地端口,远程地址,远程端口};每一个Socket 有一个本地的唯一Socket 号,由操作系统分配。
7.SOCKET分类
流式套接字(SOCK_STREAM)
流式的套接字可以提供可靠的、面向连接的通讯流。它使用了TCP协议。TCP 保证了数据传输的正确性和顺序性。
数据报套接字(SOCK_DGRAM)
数据报套接字定义了一种无连接的服务,数据通过相互独立的报文进行传输,是无序的,并且不保证可靠,无差错。使用数据报协议UDP协议。
原始套接字。
原始套接字允许对低层协议如IP或ICMP直接访问,主要用于新的网络协议实现的测试等。
8.套接字地址结构
struct sockaddr{unsigned short sa_family; /* address族, AF_xxx */char sa_data[14]; /* 14 bytes的协议地址 */
};
sa_family 一般来说, IPV4使用“AF_INET”。
sa_data 包含了一些远程电脑的地址、端口和套接字的数目,它里面的数据是杂溶在一起的。
9.sockaddr_in地址结构
struct sockaddr_in {short int sin_family; /* Internet地址族 */unsigned short int sin_port; /* 端口号 */struct in_addr sin_addr; /* Internet地址 */unsigned char sin_zero[8]; /* 添0(和struct sockaddr一样大小)*/
};
这两个数据类型是等效的,可以相互转换,通常使用sockaddr_in更为方便
10.字节序列转换
因为每一个机器内部对变量的字节存储顺序不同(有的系统是高位在前,底位在后,而有的系统是底位在前,高位在后 ),而网络传输的数据大家是一定要统一顺序的。所以对与内部字节表示顺序和网络字节顺序不同的机器,就一定要对数据进行转换。
11.字节转换函数
htons()——“Host to Network Short”–主机字节顺序转换为网络字节顺序(对无符号短型进行操作2bytes)htonl()——“Host to Network Long”–主机字节顺序转换为网络字节顺序(对无符号长型进行操作4bytes)ntohs()——“Network to Host Short”–网络字节顺序转换为主机字节顺序(对无符号短型进行操作2bytes)ntohl()——“Network to Host Long ”–网络字节顺序转换为主机字节顺序(对无符号长型进行操作4bytes)
12.地址格式转换
linux提供将点分格式的地址转于长整型数之间的转换函数。
inet_addr()能够把一个用数字和点表示IP 地址的字符串转换成一个无符号长整型。
inet_ntoa()
inet_aton()
13.基本套接字调用
socket() bind() connect() listen() accept() send() recv()
sendto() shutdown() recvfrom() close() getsockopt()
setsockopt() getpeername() getsockname() gethostbyname()
gethostbyaddr() getprotobyname() fcntl()
14.基于流套接字的编程流程
15.accept()函数
#include <sys/types.h>#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能
accept 函数由 TCP 服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠。
参数sockfdsockfd是socket系统调用返回的服务器端socket描述符addr用来返回已连接的对端(客户端)的协议地址
返回值是表示已连接的套接字描述符,而第一个参数是服务器监听套接字描述符。
F:信号量
1.什么是信号量
Dijkstra提出的“信号量”概念是共发程序设计领域的一项重大进步
信号量是一种变量,它只能取正整数值,对这些正整数只能进行两种操作:等待和信号
用两种记号来表示信号量的这两种操作:
P(semaphore variable) 代表等待 -1
V(semaphore variable) 代表信号 +1
2.信号量的分类
最简单的信号量是一个只能取“0”和“1”值的变量,也就是人们常说的“二进制信号量”
可以取多种正整数值的信号量叫做“通用信号量”
3.pv操作的定义
假设我们有一个信号量变量sv,则pv操作的
定义如下
P(sv):如果sv的值大于零,就给它减去1;如果sv的值等于零,就挂起该进程的执行
V(sv): 如果有其他进程因等待sv变量而被挂起,就让它恢复执行;如果没有进程因等待sv变量而被挂起,就给它加1
4.pv操作工作情况
两个进程共享着sv信号量变量。如果其中之一执行了P(sv)操作,就等于它得到了信号量,也就能够进入关键代码部分了。
第二个进程将无法进入关键代码,因为当它尝试执行P(sv)操作的时候,它会被挂起等待一个进程离开关键代码并执行V(sv)操作释放这个信号量
5.信号量函数
每一个信号量函数都能对成组的通用信号量进行操作,自然也可以完成对最简单的二进制信号量的操作
还经常需要用到头文件<sys/types.h>和<sys/ipc.h>
int semctl(int sem_id,int sem_num,int command,...);
int semget(key_t key,int num_sems,int sem_flags);
int semop(int sem_id,struct sembuf * sops,size_t nsops);
6.semget函数
作用:创建一个新的信号量或者取得一个现有信 号量的键字
原型:
int semget (key_t key,int num_sems,int sem_flag);
key: 是一个整数值,不相关的进程将通过这个值去访问同一个 信号量num_sems:需要使用的信号量个数,它几乎总是取值为1sem_flags:是一组标志,其作用与open函数的各种标志很相似,它低端的九个位是该信号量的权限,其作用相当于文件的访问权限,可以与键值IPC_CREATE做按位的OR操作以创建一个新的信号量(IPC_CREAT|0766)成功时将返回一个正数值,它就是其他信号量函数要用到的那个标识码,如果失败,将返回-1
7.semop函数
作用:改变信号量的键值
int semop ( int sem_id,struct sembuf *sem_ops,size_t num_sem_ops,);
sem_id:是该信号量的标识码,也就是semget函数的返回值sem_ops:是个指向一个结构数值的指针Semop调用的一切动作都是一次性完成的,这是为了避免出现因使用了多个信号量而可能发生的竞争现象
sembuf结构体中的元素
struct sembuf{short sem_num;short sem_op;short sem_flg;};
sem_num是信号量的编号,如果你的工作不需要使用一组信号量,这个值一般就取为0。sem_op是信号量一次PV操作时加减的数值,一般只会用到两个值,一个是“-1”,也就是P操作,等待信号量变得可用;另一个是“+1”,也就是我们的V操作,发出信号量已经变得可用sem_flag通常被设置为SEM_UNDO.她将使操作系统跟踪当前进程对该信号量的修改情况
8.semctl函数
作用:允许我们直接控制信号量的信息
int semctl(int sem_id,int sem_num,int command,…);
sem_id: 是由semget函数返回的一个信号量标识码sem_num: 信号量的编号,如果在工作中需要使用到成组的信号量,就要用到这个编号;它一般取值为0,表示这是第一个也是唯一的信号量comman:将要采取的操作动作如果还有第四个参数,那它将是一个“union semun”复合结构删除 semctl(sem_id,0,IPC_RMID);
semctl函数里的command可以有许多不同的值,下面这两个是比较常用的:
SETVAL:用来把信号量初始化为一个已知的值,这个值在semun结构里是以val成员的面目传递的。
IPC_RMID:删除一个已经没有人继续使用的信号量标识码
9.创建信号量
//创建信号量int val;int sem_id=sem_creat((key_t)1234,5);//初始化信号量unsigned short sems[5]={0};sem_setall(sem_id,sems);sem_setval(sem_id,0,5);int i=0;for(i=0;i<5;i++){val=sem_getval(sem_id,i);printf("i:%d val:%d\n",i,val);}int sem_p(int sem_id,int semnum){struct sembuf sops;memset(&sops,0,sizeof(struct sembuf ));sops.sem_num=semnum;sops.sem_op=-1;sops.sem_flg=0;int ret=semop(sem_id, &sops,1);return ret;}int sem_v(int sem_id,int semnum){struct sembuf sops;memset(&sops,0,sizeof(struct sembuf ));sops.sem_num=semnum;sops.sem_op=1;sops.sem_flg=0;int ret=semop(sem_id, &sops,1);return ret;}
10.信号量语义扩展 & 案例分析
有一间汽车租赁公司共有10辆汽车,同时可以接受10个客户每个客户一辆车的订单。如第11个客户前来租赁,那么就必须等待前面10个客户中任意一个归还汽车后才能租赁,在这之前必须一直等待。
案例分析如下:
汽车租赁公司是个服务器程序,而客户是个客户端程序
服务器程序共掌握有10个资源,同时可以被10个客户端程序防问
利用之前学的信号量知识,我们初始化信号量时为信号量赋予10而不是1(不使用二进制信号量)
每个客户端获得资源之后用信号通知服务器,服务器接收到信号后把现有资源数量显示于屏幕中
进程间通信方式 超详细解析全站最全相关推荐
- Android技能树 — 网络小结(6)之 OkHttp超超超超超超超详细解析
前言: 本文也做了一次标题党,哈哈,其实写的还是很水,各位原谅我O(∩_∩)O. 介于自己的网络方面知识烂的一塌糊涂,所以准备写相关网络的文章,但是考虑全部写在一篇太长了,所以分开写,希望大家能仔细看 ...
- 单片机数字钟(调时,调时闪烁,万年历,年月日)超详细解析
2019/07/13 单片机数字钟(调时,调时闪烁,万年历,年月日)超详细解析 发表日期:2019/07/13 单片机开发板:巫妖王2.0, 使用同款开发板可直接上板测试 文档说明: 实现功能 : 一 ...
- 计算机网络之交换机的工作原理---超详细解析,谁都看得懂!!
在了解交换机的工作原理之前,我们先要了解几个概念. 一.相关概念 1.OSI七层模型是哪七层? 自上而下分别是: 应用层 表示层 会话层 传输层 网络层 数据链路层 物理层 交换机工作在数据链路层, ...
- dat关闭某进程_超详细解析!工程师必会的Linux进程间通信方式和原理
▍进程的概念 · 进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放.可以认为进程是一个程序的一次执行过程. ▍进程通信的概念 · 进 ...
- VUE 钩子函数超详细解析
点击上方蓝色字体关注我吧 一起学习,一起进步,做积极的人! 前言 Vue 实例在被创建时,会经过一系列的初始化过程,初始化过程中会运行一些函数,叫做生命周期钩子函数,通过运用钩子函数,用户在可以在Vu ...
- 【智能算法】粒子群算法(Particle Swarm Optimization)超详细解析+入门代码实例讲解...
喜欢的话可以扫码关注我们的公众号哦,更多精彩尽在微信公众号[程序猿声] 01 算法起源 粒子群优化算法(PSO)是一种进化计算技术(evolutionary computation),1995 年由E ...
- 超详细解析python爬虫爬取京东图片
超详细图片爬虫实战 实例讲解(京东商城手机图片爬取) 1.创建一个文件夹来存放你爬取的图片 2.第一部分代码分析 3.第二部分代码分析 完整的代码如下所示: 升级版代码: 爬取过程中首先你需要观察在手 ...
- 关于主从复制的超详细解析(全)
目录 前言 1. 主从复制 1.1 方式 2. Mysql的主从复制 2.1 一主一从 2.1.1 window和linux通讯 2.1.2 linux和linux的通讯 2.2 双主双从 3. Re ...
- 两万字深度讲解系统设计!超详细解析!面试复习必备!
Table of Contents generated with DocToc 三高 高并发 高性能 高可用 网站统计IP PV UV实现原理 如何进行系统拆分? 场景题:设计判断论文抄袭的系统 设计 ...
- C语言--getchar()函数超详细解析(多维度分析,小白一看就懂!!!)
目录 一.前言 二.什么是getchar()函数 三.getchar()函数的返回类型与机制 四.连续单个字符串 (代码演示) 五.getchar()函数其他用法,实战演练(重点) (1)按照题目写出 ...
最新文章
- Javascript中的函数是第一类对象(first-class object)
- NPAPI——实现非IE浏览器的类似ActiveX的本地程序(插件)调用
- java控制台输出百分比进度条示例
- JavaScript算法(实例二)9*9乘法表
- 《ArcGIS Runtime SDK for Android开发笔记》——数据制作篇:紧凑型切片制作(Server缓存切片)...
- 【JAVA】什么时候会发生空指针异常
- 就这一次看懂TraceView
- 关于Jmeter压力测试
- P2627 [USACO11OPEN]Mowing the Lawn G(单调队列优化dp)
- 如何评判一个企业是否需要实施erp系统?
- 2022年软件测试人员必读的经典书籍推荐(附电子版)
- SQL Server数据并发处理
- txt文件所有大写字母转小写代码
- Unity 颜色板|调色板|无级变色功能
- centos 静态编译MP4box
- GO基础---for循环
- Linux Ubuntu 命令行文件系统的创建,挂载,卸载
- [转载]永恒的经典——冰封十大经典战役寄语
- Dart list数组集合类型
- 中国总裁唐骏:说出微软的秘密