【book】apue和unp

目录

unix 网络编程

unix 进程间通信

信号量:

条件变量

pipe

FIFO

共享内存

套接字

消息队列

RPC:

1.本地过程调用

2.远程过程调用-同一台主机两个进程中

3.远程过程调用-不同主机的两个进程中

常见RPC框架:

unix 环境编程

swap分区的概念和作用


unix 网络编程

1.从TCP套接字读取数字时,我们总是需要把read编写在某个循环中,当read返回0(表明对端关闭连接)或负值(表明发生错误)时终止循环。

2.Unix errno值:只要一个unix函数(例如某个套接字函数)中有错误发生,全局变量errno就会被置为一个指明该错误类型的正直,函数本身则通常返回-1.例如当errno值等于ETIMEDOUT时,可由err_sys查看errno变量值并输出“connection timed out”。

3.close函数会引发正常的tcp连接终止序列:每个方向上发送一个fin,每个fin又由各自的对端确认。

4.以太网的MTU大小:15000字节。即数据链路层最大报文长度。因此tcp报文的MSS常常设置为1460(1500-20-20),即减去TCP固定首部和IP固定首部长。达到尽量避免分片的目的。

    同时,tcp套接字的发送缓冲区和接收缓冲区应该为MSS的四倍左右,默认的大小为8k,刚好达到四倍。

   可以通过SO_RCVBUF/SO_SNDBUF来设置 范围是8k~60k。(setsockopt)

  至于为啥要四倍?这是由于TCP的快恢复算法,TCP的发送端使用3个重复的确认来检测某个分节是否丢失。因为在接收端发现某个分节丢失后,他将给新收到的每个分节发送一个重复的确认。   如果接收方的窗口大小不足以存放这样4个分节,那就不可能连发三个重复的确认,从而无法激活快恢复算法。

5.UDP是一个简单的传输层协议,应用进程往一个UDP套接字写入一个消息,该消息随后被封装到一个UDP数据报(即加上UDP报头),该UDP数据报进而又被封装到一个ip数据报,然后发送到目的地

6.当一个unix进程无论是自愿的(调用exit或者从main函数返回)还是非自愿的(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何tcp连接上也发出一个fin(与调用close一样,但是之后进程就没了)。导致对端进行第三次挥手前会收到RST。若对一个已收到RST的fd再write,则会收到对方内核发来的SIGPIPE信号。

    产生RST的三个条件是:目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器程序(也称端口未打开);TCP想要取消一个已有连接(异常终止而不是正常发送FIN终止);TCP连接收到一个根本不存在的连接上的分节。

    但是,当一个客户端SYN到达,而服务器的listen监听队列满了,TCP会直接忽略该分节,而不会发送RST。

  异常终止一个连接对应用程序来说有两个优点:

   (1)丢弃任何待发数据并立即发送复位报文段;

   (2)R S T的接收方会区分另一端执行的是异常关闭还是正常关闭。

    应用程序使用的A P I必须提供产生异常关闭而不是正常关闭的手段

几种会发送RST的情况:

1.当程序调用close关闭socket的时候,如果发送缓冲区中仍然有数据的话,协议栈会发送RST包代替FIN包给客户端.

2.在一个已经关闭的socket上收到数据

3.端口未打开(监听)

 

7.四次挥手time-out作用:

  • 可靠的实现TCP连接终止
  • 允许老的重复分节在网络中消逝

    可靠终止指的是:若客户端的第四次挥手ACK丢失了,服务器将会重新发送其第三次挥手FIN,此时客户端收到了,再次发送第四次挥手ACK。  若是没有time-out,则服务器重发FIN时会收到客户端机器的一个RST分节,该分节会被服务器解释成一个错误。

   分节消逝指:若关闭了此连接后,下一次在相同的IP和端口之间建立了一个新连接,若网络中还滞留老连接的一些分节,将造成混乱。故关闭连接时等待2MSL,让老连接的分节都消逝。

8.客户端创建完socket后无需bind端口,其会话通常使用短期存活的临时端口。

9.unix每个文件或者套接字都有一个引用计数(在文件表项中维护)。例如当服务端accept连接产生一个新的fd后,调用fork后,此fd的引用计数就为2.随后父进程close此fd,而子进程仍可以对此fd进行read或write。只有当子进程也close后,此fd引用计数变为0,才会清理和资源释放。

10.每次调用select函数时,我们都要用FD_SET指定所关心的描述符值(一位一位),该函数返回时,结果将指示哪些描述符已就绪。我们可以用FD_ISSET宏来测试fd_set数据类型中的描述符,其中任何未就绪的描述符对应的位返回时均清为0.为此,每次重新调用select都要重新置为关心的描述符。

11.IO复用应该配套非阻塞IO使用,否则可能出现服务器被阻塞在对一个客户端的read上无法返回,就延误了对其他fd的处理。

12.端口复用 socket reuseaddr :打开端口复用:目的是在服务器挂掉后能够立即重启,监听跟刚才同一个端口,否则会显示端口正在被监听,一般要等2分钟才能再次监听(time_wait) 。 情况2:accept新连接后创建了一个子进程来处理客户端请求,此时监听进程停止了,重启时,由于子进程正在处理客户端请求(这个连接占用了端口),若没开启SO_REUSEADDR将会导致bind失败,监听进程无法启动。

13.

wait()

pid_t wait(int *status)

父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。

如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 -1,同时 errno 被置为 ECHILD。参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL。

waitpid()

pid_t waitpid(pid_t pid, int *status, int options)

作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。

pid 参数指示一个子进程的 ID,表示只关心这个子进程退出的 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。

 

14:write,read,writen,readn

write函数

    ssize_t write(int fd,const void *buf,size_t nbytes);

    write函数将buf中的nbytes字节内容写入到文件描述符中,成功返回写的字节数,失败返回-1.并设置errno变量。在网络程序中,当我们向套接字文件描述舒服写数据时有两种可能:

    1、write的返回值大于0,表示写了部分数据或者是全部的数据,这样用一个while循环不断的写入数据,但是循环过程中的buf参数和nbytes参数是我们自己来更新的,也就是说,网络编程中写函数是不负责将全部数据写完之后再返回的,说不定中途就返回了!

    2、返回值小于0,此时出错了,需要根据错误类型进行相应的处理。

    如果错误是EINTR表示在写的时候出现了中断错误,如果是EPIPE表示网络连接出现了问题。

因此为预防万一,我们一般使用writen来写:

writen

ssize_t writen(int fd, const void *vptr, size_t n){
    size_t nleft;
    ssize_t nwritten;
    const char* ptr;
    
    ptr = vptr;
    nleft = n;
    while(nleft > 0){
        if((nwritten = write(fd, ptr, nleft)) <= 0){
            if(nwritten < 0 && errno == EINTER)
                nwritten = 0;    //continue
            else if (errno == EAGAIN)    //如果是在IO复用的    =-1,写缓冲区满了,下次wait再写
            {                            //使用场景下
                return nwritten;          //应该加上
            }                            //这四行
            else
                return (-1);
        }
        nleft -= nwritten;
        ptr += nwritten;
    }
    return n;
}

ssize_t read(int fd,void *buf,size_t nbyte)

    read函数是负责从fd中读取内容,当读取成功时,read返回实际读取到的字节数,如果返回值是0,表示已经读取到文件的结束了,小于0表示是读取错误。如果错误是EINTR表示在写的时候出现了中断错误,如果是EPIPE表示网络连接出现了问题。

在socket中服务器与客户端进行通信,当其中一方调用close(即这一方会发送一个FIN)关闭套接字之后,另一方read()会返回一个0。

 

 

readn:

ssize_t      /* Read "n" bytes from a descriptor. */
readn(int fd, void *vptr, size_t n)
{
    size_t nleft;
    ssize_t nread;
    char *ptr;
    
    ptr = vptr;
    nleft = n;
 
    while (nleft > 0) {
        if ( (nread = read(fd, ptr, nleft)) < 0) {
            if (errno == EINTR)
                nread = 0;  /* and call read() again */
            else if (errno == EAGAIN)    //如果是在IO复用的    =-1且数据还没读完,下次wait再读
            {                            //使用场景下
                return n-nleft;          //应该加上
            }                            //这四行
            else
                return(-1);
        } else if (nread == 0)
            break;    /* EOF */
 
        nleft -= nread;
        ptr += nread;
    }
    
    return(n - nleft);  /* return >= 0 */
}

size_t: 为了增强程序的可移植性,便有了size_t,它是为了方便系统之间的移植而定义的,不同的系统上,定义size_t可能不一样。在32位系统上 定义为 unsigned int 也就是说在32位系统上是32位无符号整形。在64位系统上定义为 unsigned long 也就是说在64位系统上是64位无符号整形。

ssize_t是signed size_t,是标准C库中定义的,应为unsigned int。定义为typedef int ssize_t。而ssize_t:这个数据类型用来表示可以被执行读写操作的数据块的大小.它和size_t类似,但必需是signed.意即:它表示的是sign size_t类型的。

 

 

unix 进程间通信

线程通信:

信号量:

信号量与mutex、cond不同:

  • 信号量的PV操作可以是不同线程来做的,而mutex加锁解锁只能是同一个线程。
  • 信号量的每次post(+1)操作总会被记住,而当向一个条件变量发送信号时(pthread_cond_signal),若没有线程等待在此条件变量上(pthread_cond_wait),则此信号会消失(被忽略了)。
  • pthread_cond_wait做三件事:解开mutex(进入while前获得的锁)、等在cond上、被唤醒时又锁上mutex,再做业务操作,做完解锁。

g++ -o test semaphore.cpp -pthread

线程指定函数应该是这种形式,注意要带个参数:

void* produce(void* arg)

#include<iostream>
#include<semaphore.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>

using namespace std;

#define NUM 5
int queue[NUM];

int producer_num = 1;
int consumer_num = 1;
sem_t blank_num,product_num;

void* produce(void* arg){
	int i=0;
	
	while(1){
		sem_wait(&blank_num);	//等待空格子
		queue[i]=rand()%100 +1;
		sem_post(&product_num);
		i=(i+1)%NUM;
	}
}

void* consume(void* arg){
	int i=0;
	
	while(1){
		sem_wait(&product_num);	//等待产品
		queue[i]=0;			//消费产品
		cout<< "i consume 1 product" << endl;
		sem_post(&blank_num);
		i=(i+1)%NUM;
	}
}

int main(){

	sem_init(&blank_num,0,NUM);  //初始空格NUM个
	sem_init(&product_num,0,0);	//初始产品0个

	pthread_t t_producers[producer_num];
	pthread_t t_consumers[consumer_num];
	
	for(int i=0;i<producer_num;i++){
		t_producers[i]=0;
		pthread_create(&t_producers[i],NULL,produce,NULL);
	}
	
	 for(int i=0;i<consumer_num;i++){
		t_consumers[i]=0;
		pthread_create(&t_consumers[i],NULL,consume,NULL);
	}
	 
	 for(int i=0;i<producer_num;i++){
		pthread_join(t_producers[i],NULL);
	}
	
	 for(int i=0;i<consumer_num;i++){
		pthread_join(t_consumers[i],NULL);
	}
	 
	 sem_destroy(&blank_num);    //用完信号量要回收
	 sem_destroy(&product_num);
	 
	return 0;
}

 

条件变量

需要配合mutex来实现,用于线程之间的通信

#include<iostream>
#include<pthread.h>

using namespace std;

pthread_mutex_t mutex;
int k=0;
pthread_cond_t cond =  PTHREAD_COND_INITIALIZER;	

void *func1(void *arg){
    for(int i = 0 ; i<10; i++){
	pthread_mutex_lock(&mutex);
            while(k%3!=0){
		pthread_cond_wait(&cond,&mutex);
            }
		cout<<"A";
                k++;
            pthread_mutex_unlock(&mutex);
            pthread_cond_broadcast(&cond);
	
    }
}

void *func2(void *arg){
    for(int i = 0 ; i<10; i++){
	 pthread_mutex_lock(&mutex);
            while(k%3!=1){
		pthread_cond_wait(&cond,&mutex);
            }
		cout<<"B";
                k++;
            pthread_mutex_unlock(&mutex);
            pthread_cond_broadcast(&cond);
	
    }
}

void *func3(void *arg){
    for(int i = 0 ; i<10; i++){
	pthread_mutex_lock(&mutex);
            while(k%3!=2){
		pthread_cond_wait(&cond,&mutex);
            }
		cout<<"C";
                k++;
            pthread_mutex_unlock(&mutex);
            pthread_cond_broadcast(&cond);
	
    }
}

int main(){
	
    pthread_mutex_init(&mutex, NULL);
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    
    pthread_create(&tid1,NULL,func1,NULL);
    pthread_create(&tid2,NULL,func2,NULL);
    pthread_create(&tid3,NULL,func3,NULL);

 //   cout<<"pthread create success!"<<endl;
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);

    pthread_mutex_destroy(&mutex);
    cout<<endl;
    return 0;
}

今天碰到一个问题:互斥锁mutex能用于两个进程之间的通信吗?

答案是肯定的:

互斥锁位于共享内存区,将用于不同进程之间的上锁,设置互斥锁的属性为PTHREAD_PROCESS_SHARED,并初始化互斥锁。

static pthread_mutex_t *mptr;
 
void my_lock_init(char *pathname)
{
    int fd;
    pthread_mutexattr_t mattr;
    fd=open("/dev/zero", O_RDWR, 0);
    mptr=mmap(0, sizeof(pthread_mutex_t), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    close(fd);
    pthread_mutexattr_init(&mattr);
    pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(mptr, &mattr);
}
 
//上锁
void my_lock_wait()
{
    pthread_mutex_unlock(mptr);
}
 
//解锁
void my_lock_release()
{
    pthread_mutex_unlock(mptr);
}

 

 

进程通信:

pipe

                                       

匿名管道,只能用于有亲缘关系的进程之间进行通信

碰到一个问题:read的返回值始终为1,不是应该返回读到的字节数吗?

while(ret=read(pipe_fd[0],message,1024) > 0)
                    cout<<"parent read "<< ret <<" byte message: " << message <<endl;

原因:运算符优先级的问题。

改为

while((ret=read(pipe_fd[0],message,1024)) > 0)

运行正确

#include <iostream>
#include <unistd.h>
//#include<pthread.h>
#include <stdlib.h>
#include <string>
#include <errno.h>
#include <stdio.h>
#include <sys/wait.h>

using namespace std;

int main(){
	
	pid_t pid;
	int pipe_fd[2];
	if(pipe(pipe_fd)<0){
		perror("pipe failed");
		exit(-1);
	}
	
	pid = fork();
	if(pid<0){
		perror("fork failed");
		exit(-1);
	}
	else if(pid == 0){	//子进程
		close(pipe_fd[0]);
		string s;
		cout<<"in child process, please input a string "<<endl;
		cin>>s;
		cout<<"child is going to send " << s.size() << " byte message"<<endl;
		if(write(pipe_fd[1],s.c_str(),s.size()) < 0){
			perror("write error"); exit(-1);
		}
		exit(-1);
	}
	else{				//父进程
		close(pipe_fd[1]);
		cout<<"parent is going to read pipe"<<endl;
		char message[1024];
		ssize_t ret = 0;
		//if(ret=read(pipe_fd[0],message,1024) < 0){
		//	perror("read error"); exit(-1);
		//}
		while((ret=read(pipe_fd[0],message,1024)) > 0)
		    cout<<"parent read "<< ret <<" byte message: " << message <<endl;
		waitpid(pid,NULL,0);
	}
	return 0;
}

 

 

 

FIFO

命名管道,可用于无亲缘关系的进程间通信

运行程序时,可以看见确实是创建了一个文件,记得用完要     unlink(FIFO_fd);//删除管道文件

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
#include <errno.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

using namespace std;

#define FIFO_fd "/data/chenjinan/fifo.1"

int main(){
	
	int readfd,writefd;
	
	if(mkfifo(FIFO_fd, S_IFIFO|0666) < 0 && (errno != EEXIST)){
		perror("make fifo failed");
		exit(-1);
	}
	
	int pid = fork();
	if(pid<0){
		perror("fork failed");
		exit(-1);
	}
	else if(pid == 0){	//子进程
		writefd = open(FIFO_fd,O_WRONLY);
		string s;
		cout<<"please input a string "<<endl;
		cin>>s;
		if((write(writefd,s.c_str(),s.size())) < 0){
			perror("write error"); exit(-1);
		}
		exit(-1);
	}
	else{				//父进程
		readfd = open(FIFO_fd,O_RDONLY);
		cout<<"parent is going to read pipe"<<endl;
		char message[1024];
		ssize_t ret=0;
		if((ret=read(readfd,message,1024)) < 0){
			perror("read error"); exit(-1);
		}
		cout<<"parent read "<<ret<<" byte message: " << message <<endl;
		waitpid(pid,NULL,0);
	}
        unlink(FIFO_fd);//删除管道文件
	return 0;
}

                                              

 

 

共享内存


在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。

共享内存的通信原理示意图:

对于上图我的理解是:当两个进程通过页表将虚拟地址映射到物理地址时,在物理地址中有一块共同的内存区,即共享内存,这块内存可以被两个进程同时看到。这样当一个进程进行写操作,另一个进程读操作就可以实现进程间通信。但是,我们要确保一个进程在写的时候不能被读,因此我们使用信号量来实现同步与互斥。

对于一个共享内存,实现采用的是引用计数的原理,当进程脱离共享存储区后,计数器减一,挂加成功时,计数器加一,只有当计数器变为零时,才能被删除。当进程终止时,它所附加的共享存储区都会自动脱离。
 

此段原文:https://blog.csdn.net/qq_33611327/article/details/81738195#commentBox  ,写得真的很好:

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。如下图所示:

          

由上图可以看出,进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。

  • mmap内存映射原理

mmap内存映射的实现过程,总的来说可以分为三个阶段:

(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

1、进程在用户空间调用库函数mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址

3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化

4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中

(二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。

6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用户空间库函数。

7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。

8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。

(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。

10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。

11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。

12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

  • mmap和常规文件操作的区别

我们首先简单的回顾一下常规文件系统操作(调用read/fread等类函数)中,函数的调用过程:

1、进程发起读文件请求。

2、内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的inode。

3、inode在address_space上查找要请求的文件页是否已经缓存在页缓存中。如果存在,则直接返回这片文件页的内容。

4、如果不存在,则通过inode定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页面过程,进而将页缓存中的数据发给用户进程。

总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。见图:

read操作文件:(write也需要两次拷贝)

mmap操作文件:

  • 内核怎样保证各个进程寻址到同一个共享内存区域的内存页面?

1、 page cache及swap cache中页面的区分:一个被访问文件的物理页面都驻留在page cache或swap cache中,一个页面的所有信息由struct page来描述。struct page中有一个域为指针mapping ,它指向一个struct address_space类型结构。page cache或swap cache中的所有页面就是根据address_space结构以及一个偏移量来区分的。

2、文件与 address_space结构的对应:一个具体的文件在打开后,内核会在内存中为之建立一个struct inode结构,其中的i_mapping域指向一个address_space结构。这样,一个文件就对应一个address_space结构,一个 address_space与一个偏移量能够确定一个page cache 或swap cache中的一个页面。因此,当要寻址某个数据时,很容易根据给定的文件及数据在文件内的偏移量而找到相应的页面。

3、进程调用mmap()时,只是在进程空间内新增了一块相应大小的缓冲区,并设置了相应的访问标识,但并没有建立进程空间到物理页面的映射。因此,第一次访问该空间时,会引发一个缺页异常。

4、 对于共享内存映射情况,缺页异常处理程序首先在swap cache中寻找目标页(符合address_space以及偏移量的物理页),如果找到,则直接返回地址;如果没有找到,则判断该页是否在交换区 (swap area),如果在,则执行一个换入操作;如果上述两种情况都不满足,处理程序将分配新的物理页面,并把它插入到page cache中。进程最终将更新进程页表。 
注:对于映射普通文件情况(非共享映射),缺页异常处理程序首先会在page cache中根据address_space以及数据偏移量寻找相应的页面。如果没有找到,则说明文件数据还没有读入内存,处理程序会从磁盘读入相应的页 面,并返回相应地址,同时,进程页表也会更新。

5、所有进程在映射同一个共享内存区域时,情况都一样,在建立线性地址与物理地址之间的映射之后,不论进程各自的返回地址(实际上各进程返回的地址为各进程的虚拟地址,虚拟地址自然就会不一样,而它们映射的物理地址是相同的)如何,实际访问的必然是同一个共享内存区域对应的物理页面。 
注:一个共享内存区域可以看作是特殊文件系统shm中的一个文件,shm的安装点在交换区上。

 

  • mmap相关函数

函数原型

void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset);

返回说明

成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],error被设为以下的某个值:

返回错误类型

参数

start:映射区的开始地址

length:映射区的长度

prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起

flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体

fd:有效的文件描述词。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1

offset:被映射对象内容的起点

相关函数

int munmap( void * addr, size_t len ) 

成功执行时,munmap()返回0。失败时,munmap返回-1,error返回标志和mmap一致;

该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小;

当映射关系解除后,对原来映射地址的访问将导致段错误发生。 

 

int msync( void *addr, size_t len, int flags )

一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。

可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致

 

  • mmap使用细节

1、使用mmap需要注意的一个关键点是,mmap映射区域大小必须是物理页大小(page_size)的整倍数(32位系统中通常是4k字节)。原因是,内存的最小粒度是页,而进程虚拟地址空间和内存的映射也是以页为单位。为了匹配内存的操作,mmap从磁盘到虚拟地址空间的映射也必须是页。

2、内核可以跟踪被内存映射的底层对象(文件)的大小,进程可以合法的访问在当前文件大小以内又在内存映射区以内的那些字节。也就是说,如果文件的大小一直在扩张,只要在映射区域范围内的数据,进程都可以合法得到,这和映射建立时文件的大小无关。

3、映射建立之后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本身,和文件句柄无关。同时可用于进程间通信的有效地址空间不完全受限于被映射文件的大小,因为是按页映射。

https://blog.csdn.net/joejames/article/details/37958017

使用例子:可以看出read/write与mmap的效率差距

#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <fcntl.h>
#include <sys/mman.h>

using namespace std;
#define MAX 1

int main()
{
        int i=0;
        int count=0, fd=0;
        struct timeval tv1, tv2;
        int *array = (int *)malloc( sizeof(int)*MAX );
         
        /*read*/
         
        gettimeofday( &tv1, NULL );
        fd = open( "mmap_test.txt", O_RDWR );
        int num = read( fd, (void *)array, sizeof(int)*MAX );
        if( sizeof(int)*MAX != num )
        {
                cout<< "Reading data failed..." << endl;
                return -1;
        }
        for( i=0; i<MAX; ++i )
                ++array[i];
        num = write( fd, (void *)array, sizeof(int)*MAX );
        if( sizeof(int)*MAX != num )
        {
                cout << "Writing data failed..." << endl;
                return -1;
        }
        free( array );
        close( fd );
        gettimeofday( &tv2, NULL );
        cout<< "Time of read/write: " << tv2.tv_usec-tv1.tv_usec<<endl;
         
        /*mmap*/
         
        gettimeofday( &tv1, NULL );
        fd = open( "mmap_test.txt", O_RDWR );
        array = (int *)mmap( NULL, sizeof(int)*MAX, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0 );
        for( i=0; i<MAX; ++i )
                ++array[i];
        munmap( array, sizeof(int)*MAX );
        msync( array, sizeof(int)*MAX, MS_SYNC );//写回磁盘
        //free( array );
        close( fd );
        gettimeofday( &tv2, NULL );
        cout<< "Time of mmap: " <<  tv2.tv_usec-tv1.tv_usec<<endl;
         
        return 0;
}    

 

 

 

套接字

选择协议的时候   AF_LOCAL  

首先是 socket 的创建。同样使用 socket() 这个函数。

本地套接字的地址结构体 sockaddr_un 的后缀是 _un,表示 Unix,而不是原来的 sockaddr_in(Internet)。我们来看一下这个Unix域套接字的地址结构体中包含哪些内容:

 

Unix 本地套接字的地址结构体中包含两个成员,其中 sun_family 表示协议族,填 AF_LOCAL 或 AF_UNIX 即可;sun_path 表示一个路径名。 
  从这里面可以很明显得看出 Unix 域套接字与原来的 网络套接字的区别,Unix 域中用于标识客户和服务器的协议地址是普通文件系统中的路径名,而这个文件就称为套接字文件。 
  这里要强调一下的是,Unix 本地套接字关联的这个路径名应该是一个绝对路径名,而不是一个相对路径名。为什么呢?因为解析相对路径依赖于调用者的当前工作目录,也就是说,要是服务器绑定了一个相对路径名,那么客户端也得在与服务端相同的目录中才能成功调用connect(连接)或者sendto(发送)这样一些函数。显然,这样就会导致程序出现异常情况,所以建议大家最好使用一个绝对路径名。 
 

例子:

服务端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

#define UNIXSTR_PATH "/tmp/unix.str"    //要指定一个路径
#define LISTENQ 5
#define BUFFER_SIZE 256

int main(void)
{
    int listenfd, connfd;
    socklen_t len;
    struct sockaddr_un servaddr, cliaddr;

    if(-1 == (listenfd = socket(AF_LOCAL, SOCK_STREAM, 0)))    //协议选择
    {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    unlink(UNIXSTR_PATH);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;    //协议指定
    strcpy(servaddr.sun_path, UNIXSTR_PATH);    //路径指定
    if(-1 == bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)))
    {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    listen(listenfd, LISTENQ);

    len = sizeof(cliaddr);

    if(-1 == (connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len)))
    {
        perror("accept");
        exit(EXIT_FAILURE);
    }

    char buf[BUFFER_SIZE];

    while(1)
    {
        bzero(buf, sizeof(buf));
        if(read(connfd, buf, BUFFER_SIZE) == 0) break;
        printf("Receive: %s", buf);
    }

    close(listenfd);
    close(connfd);
    unlink(UNIXSTR_PATH);    //当不再需要这个 Unix 域套接字时,应删除路径名对应的文件

    return 0;
}

客户端:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

#define UNIXSTR_PATH "/tmp/unix.str"
#define LISTENQ 5
#define BUFFER_SIZE 256

int main(void)
{
    int sockfd;
    struct sockaddr_un servaddr;

    sockfd = socket(AF_LOCAL, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sun_family = AF_LOCAL;
    strcpy(servaddr.sun_path, UNIXSTR_PATH);

    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    char buf[BUFFER_SIZE];

    while(1)
    {
        bzero(buf, sizeof(BUFFER_SIZE));
        printf(">> ");
        if(fgets(buf, BUFFER_SIZE, stdin) == NULL)
        {
            break;
        }
        write(sockfd, buf, strlen(buf));
    }

    close(sockfd);

    return 0;
}

看看这个文件:

rudy@ubuntu12:/tmp$ ls -l unix.str 
srwxrwxr-x 1 rudy rudy 0 10月 26 11:58 unix.str

显然,文件类型为“s”,代表套接字文件,也就是 S_IFSOCK 类型。

 

消息队列

比如kafka

 

RPC:

当讨论客户-服务端情形和过程调用时,存在着三种不同类型的过程调用:

 

1.本地过程调用

就是被调用的过程(函数)与调用过程处于同一个进程中,通过压栈出栈进行调用。

int func(int a, int b)
{
        int c = 7;
		c = a + b;
		return c;
}
 
int main()
{
		int x = 10;
		int y = 20;
        int z = 0;
		z = func(x, y);
 
        return 0; 
}

相关寄存器

ebp:存储当前栈帧的基地址
esp:存储当前栈帧的栈顶地址
eip:存储程序计数器值
eax:存储函数返回值

入栈过程 (参数、被调局部变量、原函数下一条指令地址、被调函数第一条指令地址)

1、将调用者函数的ebp入栈
2、将调用者函数的栈顶指针esp赋值给被调用函数的ebp
3、按从右到左的顺序将被调用函数的参数入栈
4、按声明的顺序将被调用函数的局部变量入栈
5、将调用函数的下一个指令地址作为返回地址入栈
6、将被调用函数的第一条指令地址赋值给eip寄存器
7、开始执行被调用函数指令

ebp寄存器处于一个非常重要的位置,该寄存器中存放的地址可以作为基准,向栈底方向可以获取返回地址,传入参数值,向栈顶方向可以获取函数的局部变量。而esp所指向的内存中又存放着上一层函数调用的ebp值。

出栈过程

1、将函数返回值存入eax寄存器中

2、执行leave指令

1、将ebp的值赋给esp
2、将esp所指向的栈顶(上一层函数调用的ebp值)赋值给ebp,同时esp增加4。

3、执行ret指令

1、将esp所指向的栈顶(返回地址)赋值给eip,同时esp增加4。
2、修改了程序计数器eip,因此跳转到返回地址处继续执行。

 

2.远程过程调用-同一台主机两个进程中

    通常称调用者为客户,被调用者为服务器。主要是通过门(door)进行:一个进程调用同一台主机上另一个进程中的某个过程(函数)。通过给本进程内的某个过程创建一个门,一个进程(服务器)就能使得该过程能为其他进程(客户)所调用。我们也可以认为门是一种特殊类型的IPC,因为客户和服务器之间以函数参数和返回值形式交换信息。

客户端程序:未测试

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
#include <errno.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/door.h>

using namespace std;

#define DOOR "/data/chenjinan/door.rpc"

int main(){
	
	
	int fd;
	long ival,oval;
	door_arg_t arg;
	
	fd = open(DOOR,O_RDWR);    //获得fd
	
	ival = 1000000;
	arg.data_ptr = (char*) &ival;
	arg.data_size = sizeof(long);	//输入参数
	arg.desc_ptr = NULL;
	arg.desc_num = 0;	//处理描述符的传递
	arg.rbuf = (char*) &oval;
	arg.rsize = sizeof(long);	//输出结果
	
	door_call(fd, &arg);    //调用
	
	cout<<"result is "<< oval << endl;
	
	return 0;
}

服务器端程序:

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
#include <errno.h>
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/door.h>

using namespace std;

#define DOOR "/data/chenjinan/door.rpc"

void servproc(void* cookie, char* dataptr, size_t datasize, door_desc_t* descptr, size_t ndesc)
{
	long arg,result;
	
	arg = *((long *) dataptr);
	
	result = arg * arg;
	door_return((char*)&result, sizeof(result), NULL,0);
}

int main(){
	
	int fd;

	fd = door_create(servproc,NULL,0);    //创建door
	
	close(open(DOOR, O_CREAT | O_RDWR));
	fattach(fd,DOOR);    //attach一个路径,该路径是客户标识这个门的手段
	
	for(;;)
		pause();	//所有工作交给servproc去做。
	
	return 0;
}

 

 

3.远程过程调用-不同主机的两个进程中

 

sun RPC:

用sun RPC代替门,先给出RPC说明书文件:square.x

应该有个注册中心,服务端在注册中心中注册服务,客户端在注册中心获取服务地址进行调用

 

struct square_in{
	long arg1;
};

struct square_out{
	long res1;
};

program SQUARE_PROG{
	version SQUARE_VERS{
		square_out SQUAREPROC(square_in) = 1;   /* procedure number = 1 */
	}  = 1;		/* version number */
}   = 0x31230000;	/* program number */

定义了两个结构:一个用于参数,一个用于结果

 

客户端程序:

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include "square.h"
using namespace std;

#define host = "127.0.0.1"    //可以是主机名或者IP地址

int main(){
	
	CLIENT *cl;
	square_in in;        
	square_out *outp;        //协议
	
	long q = 100000;
	
	cl = clnt_create(host, SQUARE_PROG, SQUARE_VERS, "tcp");    //获得客户句柄:主机,程序名,版本号,协议选择
	in.arg1 = q;
	
	if((outp = squareproc_1(&in,cl)) == NULL){    //调用远程过程
		err_quit("%s", slnt_sperror(cl,host));
	}
	
	cout<<"result is "<< outp->res1 << endl;
	return 0;
}

服务端程序:

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "square.h"
using namespace std;

square_out* squareproc_1_svc(square_in *inp, struct svc_req * rqstp){
	static square_out out;
	
	out.res1 = inp->arg1 * inp->arg1;
	return (&out);
}

这里服务器端只编写服务器过程,服务器程序的main函数有rpc_gen程序自动生成。

在square.x说明书文件中,我们成远程过程为SQUAREPROC,但在客户程序中,我们称它为squareproc_1.这儿的约定是:.x文件中的名字转换成小写字母形式,添上一个下划线后跟以版本号。

具体流程:

 

 

常见RPC框架:

dubbo:阿里开源的一款高性能RPC框架,在国内应用广泛,期间停止维护过一段时间,如今又开始了更新,并且捐献给Apache基金会。

thrift:可伸缩的跨语言服务的RPC软件框架,最早由Facebook开发,2007年捐献给了Apache基金会管理,现在是Apache的顶级项目;

brpc:百度开源的RPC框架,brpc是百度内最常使用的工业级RPC框架, 有1,000,000+个实例(不包含client)和上千种多种服务, 在百度内叫做"baidu-rpc". 目前只开源C++版本。

 

常见调用流程:

  • 1.服务端向注册服务中心绑定自己的地址
  • 2.客户端通过注册服务中心获取目标地址
  • 3.客户端调用本地stub对象上的方法,与调用本地方法一致
  • 4.本地stub对象将调用信息打包(序列化),通过网络发送给服务端
  • 5.服务端对象接收到网络请求,对调用信息进行解包(反序列化)
  • 6.服务端真正服务调用发起调用,并将返回结果打包(序列化)发送给客户端
  • 7.客户端解包(反序列化)结果,获取服务调用结果

一个rpc框架需要实现的几个功能:

注册中心

序列化反序列化

网络传输

详细描述如下:

  • Call ID映射。我们怎么告诉远程机器我们要调用Multiply,而不是Add或者FooBar呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用Multiply,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中,函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个 {函数 <--> Call ID} 的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call ID,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
  • 序列化和反序列化。客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
  • 网络传输。远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。Java的Netty也属于这层的东西。

 

 

unix 环境编程

1.fork:由于fork之后子进程经常跟随着exec(注),所以现在的很多实现并不执行一个父进程的数据段、栈和堆的完全副本。作为替代,使用了写时复制技术。这些区域由父子进程共享,而且内核将他们的访问权限改变为只读。如果父子进程中的任一个视图修改这些区域,则内核只为修改区域的那快内存制作一个副本,通常是虚拟存储系统中的“一页”。

注:当一个进程调用一种exec函数时(共7个),该进程执行的程序完全替换成为新程序,而新程序则从其main函数开始执行。因为调用exec并不创建新进程,所以前后的进程ID不变。exec只是用磁盘上的一个新程序替换了当前进程的正文段、数据段、堆段和栈段。

vfork:vfork和fork一样都会创建一个子进程,但是他并不将父进程的地址空间完全复制到子进程中,因为子进程会立即调用exec(或exit),于是也就不会引用该地址空间。不过在子进程调用exec之前,它在父进程的空间中运行。这种做法在某些情况下提高了效率,但若子进程修改数据、进行函数调用、或者没有调用exec或exit就返回都可能会带来未知的结果。

         还有一个区别:vfork保证子进程先运行,在他调用exec或exit之后父进程才可能被调度运行(否则父进程一直处于休眠)。

 

2.僵尸进程和孤儿进程

我们知道在unix/linux中,正常情况下,子进程是通过父进程创建的,子进程在创建新的进程。子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。 当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态(回收)。

孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

unix提供了一种机制可以保证只要父进程想知道子进程结束时的状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。直到父进程通过wait / waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

孤儿进程是没有父进程的进程,孤儿进程这个重任就落到了init进程身上,init进程就好像是一个民政局,专门负责处理孤儿进程的善后工作。每当出现一个孤儿进程的时候,内核就把孤儿进程的父进程设置为init,而init进程会循环地wait()它的已经退出的子进程。这样,当一个孤儿进程凄凉地结束了其生命周期的时候,init进程就会代表党和政府出面处理它的一切善后工作。因此孤儿进程并不会有什么危害。

 

僵尸进程的解决方法:通过信号机制

  1. 子进程退出时向父进程发送SIGCLD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程。
  2. 要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 进程所收养,这样 init 进程就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。(这里就有一个技巧:在fork得到子进程后,在子进程中再fork,然后退出子进程,这样孙进程的父进程就会变成init,就不会成为僵尸进程)

 

swap分区的概念和作用

    我们在安装系统的时候已经建立了 swap 分区。swap 分区通常被称为交换分区,这是一块特殊的硬盘空间,即当实际内存不够用的时候,操作系统会从内存中取出一部分暂时不用的数据,放在交换分区中,从而为当前运行的程序腾出足够的内存空间。

    https://www.cnblogs.com/gethinwang/p/10511047.html

    当用户提交程序,然后产生进程在机器上运行。机器会判断当前物理内存是否还有空闲允许进程调入内存运行,如果有则直接调入内存进行;如果没有,则会根据优先级选择一个进程挂起,把该进程交换到swap中等待,然后把新的进程调入到内存中运行。根据这种换入和换出,实现了内存的循环利用,让用户感觉不到内存的限制。从这也可以看出swap扮演了一个非常重要的角色,就是暂存被换出的进程。

 从上可以看出,当物理内存使用完或者达到一定比例之后,我们可以使用swap做临时的内存使用。当物理内存和swap都被使用完那么就会出错,如:out of memory。

  对于使用多大比例内存之后开始使用swap,在系统配置文件中可以通过调整参数进行修改。

1 [root@localhost ~]# cat /proc/sys/vm/swappiness

2 60

该参数范围为0-100。0就是最大限度使用内存,尽量不使用swap;100是积极使用swap。

 Swap分区的数量对性能也有很大的影响。因为swap毕竟还是以磁盘来伪装成内存,交换的操作是磁盘IO的操作而不是内存的ioad与store操作。如果有多个swap交换区,每个swap会有一定的优先级,该优先级也可以调整。swap空间的分配会以轮流的方式操作于所有的swap,这样会大大均衡IO的负载,加快swap交换的速度。

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值