文章目录
虚拟地址与物理地址
本文旨在通过虚拟地址计算物理地址,理解环境实际运行机制以及写时复制技术
/proc/pid/page_map
Linux文件目录中的/proc记录着当前进程的信息,称其为虚拟文件系统。在/proc下有一个链接目录名为self,这意味着哪一个进程打开了它,self中存储的信息就是所链接进程的。self中有一个名为pagemap的文件,专门用来记录所链接进程的物理页号信息。这样通过/proc/pid/pagemap文件,允许一个用户态的进程查看到每个虚拟页映射到的物理页
/proc/pid/pagemap中的每一项都包含了一个64位的值,这个值内容如下所示。每一项的映射方式不同于真正的虚拟地址映射,其文件中遵循独立的对应关系,即虚拟地址相对于0x0经过的页面数是对应项在文件中的偏移量
这两段话目前还是比较难理解
- /proc/pid/pagemap. This file lets a userspace process find out which
physical frame each virtual page is mapped to. It contains one 64-bit
value for each virtual page, containing the following data (from
fs/proc/task_mmu.c, above pagemap_read):
- Bits 0-54 page frame number (PFN) if present//present为1时,bit0-54表示物理页号
- Bits 0-4 swap type if swapped
- Bits 5-54 swap offset if swapped
- Bit 55 pte is soft-dirty (see Documentation/vm/soft-dirty.txt)
- Bit 56 page exclusively mapped (since 4.2)
- Bits 57-60 zero
- Bit 61 page is file-page or shared-anon (since 3.5)
- Bit 62 page swapped
- Bit 63 page present//如果为1,表示当前物理页在内存中;为0,表示当前物理页不在内存中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
在计算物理地址时,只需要找到虚拟地址的对应项,再通过对应项中的bit63判断此物理页是否在内存中,若在内存中则对应项中的物理页号加上偏移地址,就能得到物理地址
通过程序获取物理地址并验证写时拷贝技术
计算过程
1、首先获取一个变量的地址;(逻辑地址:段内偏移量)
2、获取系统设定的页面大小getpagesize()
3、计算虚拟地址相对于0x0经过的页面数
unsigned long v_pageIndex = vaddr/pageSize;
4、计算在/proc/pid/page_map文件中的偏移
unsigned long page_offset=v_pageIndex*sizeof(uint64_t);
5、计算虚拟地址的页内偏移量
unsigned long page_offset = vaddr%pageSize;
6、以只读方式打开/proc/pid/page_map
7、将游标移动到相应位置,即对应项的起始地址
lseek(fd,v_offset,SEEK_SET)
8、取8字节对应项
read(fd,&item,sizeof(uint64_t);
9、计算物理页号
uint64_t phy_pageIndex = (((uint64_t)1<<55)-1)&item;
10、计算物理地址
*paddr = (pht_pageIndex*pageSize)+page_offset;
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>
// 计算虚拟地址对应的地址,传入虚拟地址vaddr,通过paddr传出物理地址
void mem_addr(unsigned long vaddr, unsigned long *paddr)
{
int pageSize = getpagesize();调用此函数获取系统设定的页面大小
unsigned long v_pageIndex = vaddr / pageSize;//计算此虚拟地址相对于0x0的经过的页面数
unsigned long v_offset = v_pageIndex * sizeof(uint64_t);//计算在/proc/pid/page_map文件中的偏移量
unsigned long page_offset = vaddr % pageSize;//计算虚拟地址在页面中的偏移量
uint64_t item = 0;//存储对应项的值
int fd = open("/proc/self/pagemap", O_RDONLY);//。。以只读方式打开/proc/pid/page_map
if(fd == -1)//判断是否打开失败
{
printf("open /proc/self/pagemap error\n");//
return;//
}
if(lseek(fd, v_offset, SEEK_SET) == -1)//将游标移动到相应位置,即对应项的起始地址且判断是否移动失败
{
printf("sleek error\n");//
return;//
}
if(read(fd, &item, sizeof(uint64_t)) != sizeof(uint64_t))//读取对应项的值,并存入item中,且判断读取数据位数是否正确
{
printf("read item error\n");//
return;//
}
if((((uint64_t)1 << 63) & item) == 0)//判断present是否为0
{
printf("page present is 0\n");//
return ;//
}
uint64_t phy_pageIndex = (((uint64_t)1 << 55) - 1) & item;//计算物理页号,即取item的bit0-54
printf("phypageIndex:%d\n pageSize:%d\n pageoffset:%d\n",phy_pageIndex,pageSize,page_offset);
*paddr = (phy_pageIndex * pageSize) + page_offset;//再加上页内偏移量就得到了物理地址
}
int a = 100;//全局常量
int main()
{
int b = 100;//局部变量
static c = 100;//局部静态变量
const int d = 100;//局部常量
char *str = "Hello World!";//
unsigned long phy = 0;//物理地址
char *p = (char*)malloc(100);//动态内存
int pid = fork();//创建子进程
if(pid == 0)
{
sleep(10);
a=98;
printf("子进程中修改动态内存\n");
p[0] = '1';//子进程中修改动态内存
mem_addr((unsigned long)&a, &phy);//
printf("pid = %d, virtual addr = %x , physical addr = %x\n", getpid(), &a, phy);//
}
else
{
mem_addr((unsigned long)&a, &phy);//
printf("pid = %d, virtual addr = %x , physical addr = %x\n", getpid(), &a, phy);//
}
sleep(100);//
free(p);//
waitpid();//
printf("父进程安全退出\n");
return 0;//
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-33787By5-1618144263365)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618122377036.png)]
测试效果没有达到预期
解决方法
with a gap in time
访问物理内存
mknod /dev/dram c 85 0
./fileview /dev/dram
mknod
手动创建设备文件
mknod DEVNAME {b|c} MAJOR MINOR
1,DEVNAME是要创建的设备文件名,如果想将设备文件放在一个特定的文件夹下,就需要先用mkdir在dev目录下新建一个目录;
2, b和c 分别表示块设备和字符设备:
b表示系统从块设备中读取数据的时候,直接从内存的buffer中读取数据,而不经过磁盘;
c表示字符设备文件与设备传送数据的时候是以字符的形式传送,一次传送一个字符,比如打印机、终端都是以字符的形式传送数据;
3,MAJOR和MINOR分别表示主设备号和次设备号:
为了管理设备,系统为每个设备分配一个编号,一个设备号由主设备号和次设备号组成。主设备号标示某一种类的设备,次设备号用来区分同一类型的设备。linux操作系统中为设备文件编号分配了32位无符号整数,其中前12位是主设备号,后20位为次设备号,所以在向系统申请设备文件时主设备号不好超过4095,次设备号不好超过2^20 -1。
wait
通过分析各个回收方法理解进程之间的关系
父进程一旦调用了wait就立即阻塞自己,由wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,
如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.
如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态.
参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:
pid = wait(NULL);
如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。
如果参数status的值不是NULL,wait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:
————————————————
版权声明:本文为CSDN博主「DNFK」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wyhh_0101/article/details/83933308
WIFEXITED
这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非0值
请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数–指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。)
WEXITSTATUS
WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status) 就会返回5;如果子进程调用exit(7),WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说, WIFEXITED返回0,这个值就毫无意义。
————————————————
版权声明:本文为CSDN博主「DNFK」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wyhh_0101/article/details/83933308
waitpid
从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。下面我们就来详细介绍一下这两个参数:
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样。
pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
————————————————
版权声明:本文为CSDN博主「DNFK」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wyhh_0101/article/details/83933308
返回值和错误
waitpid的返回值比wait稍微复杂一些,一共有3种情况:
1、当正常返回的时候,waitpid返回收集到的子进程的进程ID;
2、如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
3、如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;
————————————————
版权声明:本文为CSDN博主「DNFK」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wyhh_0101/article/details/83933308
举例监视进程
目的:监视守护进程
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/stat.h>
void Daemon(void)
{
pid_t pid = -1;
int ret = 01;
int fd = -1;
int status = 0;
pid = fork();
if(-1 == pid)
{
perror("fork");
exit(1);
}
else if(pid >0)
{
//父进程退出
exit(1);
}
//创建会话
ret = setsid();
//当进程是会话的领头进程时setsid调用失败并返回(-1)
//调用setsid函数的进程成为新的会话的领头进程,并与其父进程的绘画组和进程组脱离
//由于会话对控制终端的独占性,进程同时与控制终端脱离
if(-1 == ret)
{
perror("setsid");
exit(1);
}
//设置权限掩码
//
umask(0);
//改变当前进程工作目录
//printf("当前目录:%s\n",get_current_dir_name());
char buf[1024];
memset(buf,0x00,sizeof(buf));
printf("当前进程目录:%s\n",getwd(buf));
//重定向文件描述符
//守护进程起循环
while(1)
{
pid = fork();
if(-1 == pid)
{
perror("fork");
exit(1);
}
else if(pid > 0)
{
//父进程监视异常
//如果异常会回收资源
wait(&status);
//但不会继续向下执行
//并发的子进程会跳出执行下一次循环使资源重新分配
if(WIFEXITED(status))
{
//子进程正常终止,父进程也退出
//正常结束
exit(0);
}
}
else if(pid == 0)
{
//父进程阻塞
//子进程跳出循环去执行任务
break;
}
else
{
perror("fork");
exit(1);
}
}
}
int main(int argc,char **argv)
{
if((2 == argc) &&(0 == strcmp("daemon",argv[1])))
{
Daemon();
}
else
{
printf("请输入两个参数\n");
exit(1);
}
while(1)
{
sleep(2);
printf("hello world\n");
}
return 0;
}
测试效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sNBi9oyy-1618144263368)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618131481453.png)]
未开启守护进程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-94mV0k7d-1618144263370)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618131516675.png)]
开启守护进程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0xBw0dj5-1618144263372)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618132328198.png)]
pid查看
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ssNituaB-1618144263374)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618132361088.png)]
关闭终端
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gGOLo3C8-1618144263375)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618132432921.png)]
将后台任务恢复到前台
查看后台运行任务
杀掉子进程
kill -9 11049
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o8B4RPYa-1618144263376)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618133073342.png)]
描述执行情况
守护方法中增加一个循环
开一个进程负责阻塞wait
通过WIFEXITED(status)
监视是否正常退出,
如果返回真,则正常退出
如果返回假,则子进程异常结束(比如接收一个kill 信号),再次执行循环fork一个子进程去执行任务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w8G4qGKR-1618144263377)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618133440221.png)]
杀掉父进程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uLq2mt5v-1618144263377)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618133491720.png)]
makefile实例
#获取所有的源文件
SRCS:=$(wildcard *.c*)
#获取所有的目标文件
OBJS:=$(patsubst %.c*, %.o, $(SRCS))
#指定编译器
CC:= g++
#指定目标
TARGET:=write
$(TARGET):$(OBJS)
$(CC) $^ -L/u01/app/oracle/product/11.2.0/db_1/lib -I/u01/app/oracle/product/11.2.0/db_1/rdbms/public -o $@ -lprotobuf -locci -lclntsh -lssl -lcrypto -lpthread
%.o:%.c*
$(CC) -c -std=c++11 $< -o $@ -lprotobuf -locci -lclntsh
.PHONY:clean
clean:
rm -rf $(OBJS) $(TARGET)
理解setsid
#include <sys/file.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/file.h>
int main (int argc, char ** argv)
{
if ( fork() > 0 ) {
printf ( "parent begin\n" ) ;
sleep(10);
printf ( "parent exit\n" ) ;
exit ( 0 ) ;
}
printf ( "child begin\n" ) ;
sleep(100);
printf ( "child exit\n" ) ;
return 0 ;
}
————————————————
版权声明:本文为CSDN博主「sweetfather」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sweetfather/article/details/79457261
测试结果
错误
c:23:2: error: stray ‘\302’ in program
分析
od -c *.c
这个错误一般是源代码中含有一些隐藏的非ascii字符。你把东西copy到文本编辑器中,再copy回来试试。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UozOfyi2-1618144263378)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129119457.png)]
打开另一个终端
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xRga0GSo-1618144263379)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129158583.png)]
关闭运行test的窗口,父子进程都不在
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DgMq7af4-1618144263379)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129226187.png)]
子进程开始调用setsid
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wVahipfs-1618144263380)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129559733.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pl9ZqLOb-1618144263381)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129633493.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-okjnclhg-1618144263381)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129664830.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fnLP5dXl-1618144263382)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618129689980.png)]
testSid分为两个进程组
父进程退出,子进程继续存在
说明:setsid后子进程不受终端影响,终端退出,不影响子进程
umask
linux中的 umask 函数主要用于:在创建新文件或目录时 屏蔽掉新文件或目录不应有的访问允许权限。文件的访问允许权限共有9种,分别是:r w x r w x r w x(它们分别代表:用户读 用户写 用户执行 组读 组写 组执行 其它读 其它写 其它执行)。
其实这个函数的作用,就是设置允许当前进程创建文件或者目录最大可操作的权限,比如这里设置为0,它的意思就是0取反再创建文件时权限相与,也就是:(~0) & mode 等于八进制的值0777 & mode了,这样就是给后面的代码调用函数mkdir给出最大的权限,避免了创建目录或文件的权限不确定性。
————————————————
版权声明:本文为CSDN博主「sweetfather」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/sweetfather/article/details/79457261
pmap命令
举例
进程1
pmap -d 1
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z2aPUZHq-1618144263383)(C:\Users\11066\AppData\Roaming\Typora\typora-user-images\1618142968796.png)]
最后一行的值
mapped 表示该进程映射的虚拟地址空间大小,也就是该进程预先分配的虚拟内存大小,即ps出的vsz
writeable/private 表示进程所占用的私有地址空间大小,也就是该进程实际使用的内存大小
shared 表示进程和其他进程共享的内存大小
扩展格式
pmap -x 1
循环显示pid的设备格式的最后一行
间隔2秒
while true;
pmap -d pid|tail -1;sleep 2;done
分析cat maps
第一行:从r-xp可知其权限为只读、可执行,该段内存地址对应于执行文件的代码段,程序的代码段需加载到内存中才可以执行。由于其只读,不会被修改,所以在整个系统内共享。
第二行:从rw-p可知其权限为可读写,不可执行,该段内存地址对应于执行文件的数据段,存放执行文件所用到的全局变量、静态变量。
第三行:从rwxp可知其权限是可读写,可执行,地址空间向上增长,而且不对应文件,是堆段,进程使用malloc申请的内存放在堆段。每个进程只有一个堆段,不论是主进程,还是不同的线程申请的内存,都反映到到进程的堆段。堆段向上增长,最大可以增长到1GB的位置,即0x40000000,如果大于1GB,glibc将采用mmap的方式,为堆申请一块内存。
第四行:是程序连接的共享库的内存地址。
第五行:是以mmap方式映射的虚拟地址空间。
第六、七行:是线程的栈区地址段,每个线程的栈大小都是16K。
第八行:是进程的栈区。关于栈段,每个线程都有一个,如果进程中有多个线程,则包含多个栈段。
系统总内存的统计
四、对调试内存泄露类问题的一些启示
当进程申请内存时,实际上是glibc中内置的内存管理器接收了该请求,随着进程申请内存的增加,内存管理器会通过系统调用陷入内核,从而为进程分配更多的内存。
针对堆段的管理,内核提供了两个系统调用brk和mmap,brk用于更改堆顶地址,而mmap则为进程分配一块虚拟地址空间。
当进程向glibc申请内存时,如果申请内存的数量大于一个阀值的时候,glibc会采用mmap为进程分配一块虚拟地址空间,而不是采用brk来扩展堆顶的指针。缺省情况下,此阀值是128K,可以通过函数来修改此值。
修改阈值:
#include <malloc.h>
intmallopt(int param,int value);
如果在实际的调试过程中,怀疑某处发生了内存泄露,可以查看该进程的maps表,看进程的堆段或者mmap段的虚拟地址空间是否持续增加,如果是,说明很可能发生了内存泄露,如果mmap段虚拟地址空间持续增加,还可以看到各个段的虚拟地址空间的大小,从而可以确定是申请了多大的内存,对调试内存泄露类问题可以起到很好的定位作用。
程的栈区。关于栈段,每个线程都有一个,如果进程中有多个线程,则包含多个栈段。
### 系统总内存的统计
### 四、对调试内存泄露类问题的一些启示
当进程申请内存时,实际上是glibc中内置的内存管理器接收了该请求,随着进程申请内存的增加,内存管理器会通过系统调用陷入内核,从而为进程分配更多的内存。
针对堆段的管理,内核提供了两个系统调用brk和mmap,brk用于更改堆顶地址,而mmap则为进程分配一块虚拟地址空间。
当进程向glibc申请内存时,如果申请内存的数量大于一个阀值的时候,glibc会采用mmap为进程分配一块虚拟地址空间,而不是采用brk来扩展堆顶的指针。缺省情况下,此阀值是128K,可以通过函数来修改此值。
修改阈值:
#include <malloc.h>
intmallopt(int param,int value);
如果在实际的调试过程中,怀疑某处发生了内存泄露,可以查看该进程的maps表,看进程的堆段或者mmap段的虚拟地址空间是否持续增加,如果是,说明很可能发生了内存泄露,如果mmap段虚拟地址空间持续增加,还可以看到各个段的虚拟地址空间的大小,从而可以确定是申请了多大的内存,对调试内存泄露类问题可以起到很好的定位作用。