操作系统为每一个进程维护着一个虚拟的地址空间,这个地址空间的大小通常取决于系统的地址线数目,比如在32位系统中,虚拟地址空间的返回就是0×00000000~0xFFFFFFFF,大小共4G。通常操作系统会划分出一部分来专门供内核使用,而不允许用户进程直接访问。Linux内核占用4G中高地址的1G,即0XC0000000~0XFFFFFFFF,windows内核通常占用高地址的2G空间,但也可配置成1G。进程的代码、数据以及共享库等资源终究是要放在物理内存中才能被访问的,操作系统在建立用户进程时,会为其建立各自独立的虚拟地址空间,然后将各自的数据段、代码段、BSS段等映射到这个地址空间,并为其初始化堆、栈等必须的资源。另外,操作系统还将虚拟空间和物理空间都划分成大小相等的页,把进程数据所在虚拟地址空间的各个虚拟页面映射到其真正被加载的物理页面,这种映射是全相联方式的,即任何一个虚页可以被映射到任何一个实页。
上PIC课讲到信号监测时,老师做过这样一个比喻:说同学们,10分钟后咱们到教室外面集合,对于“查询式”同学,他会不停地数数,直到600,“中断式“会定一个闹表,当然啦,我可以该干嘛干嘛,大家都走了我再走,多省资源啊哈哈!
话说今儿晚上在运动常跑步,遇到一查询式的哥们儿。我跑第一圈从他身边经过,冲我问:
- 几点啦?
- ……八点半……
- 三十几?
- ……三十四……
- 四十四?四十四还是三十四?
- …………我无语……继续跑……
又问:
-哎哥们儿,几点了?
-三十五!
若干圈之后,
-嗨美女,几点了?
-没带表!
第八圈,
-A,几点了?
-9点。
-哦……
啊然后这哥们儿从跑道边栏杆上摘下一个包,把手机掏出来,走出运动场……
这里面没有用到C库,也没有main函数,为了把这个程序编译成可执行文件,需要指定程序的入口。编译指令:
1 2 3 4 5 6 7 8 9 10 | $ gcc -c nomain.c $ ld -e nomain nomain.o -o nomain $ ./nomain $ echo $? 42 $ ls -l nomain -rwxr-xr-x 1 ivan ivan 618 2009-09-02 22:11 nomain $ strip nomain $ ls -l nomain -rwxr-xr-x 1 ivan ivan 356 2009-09-02 22:15 nomain |
解释一下,ld是linux下的一个链接器,-e选项用来指定程序的入口。编译后可执行文件的大小为618字节(一个动态链接的HelloWorld需要9KB,静态链接将近600K),strip命令可以”剥去”可执行文件中的调试信息,可进一步减小文件的大小,另外在链接时通过其他选项还可以将可执行文件中保存的编译器和系统版本信息也一并去掉……
1 2 3 4 5 6 7 | $ gcc test.cpp -otest $ ./test& $ 0x804a024 $ ./test& $ 0x804a024 $ ./test& $ 0x804a024 |
对可执行文件test,不同的执行实例产生的输出为什么是一样的呢?为什么呢?难道一直以来我对虚拟地址空间的理解都是错误的?另外,如果int a是局部变量,输出就不同。
1 2 3 4 5 6 7 | #include <stdio.h> int a[10240] = {1}; //~ 显式初始化a[0]为1的全局数组,编译后,可执行文件的大小为49.0K int main() { return 0; } |
局部对象是在运行时栈上分配的,编译后不占用磁盘空间。未显式初始化或者初始化为0的全局数组,编译器会做相应的记录,以便在程序装入时分配空间。做显式初始化的全局数组,由于初始化的元素个数及元素值不便记录,编译器便在编译时就分配了空间,因此需要占用磁盘空间。
程序输出:
o.o.o.o.o.o.o.o..o.o.oo.o.o.o.o.o.o.o.o.21
为什么呢?
加上代码中注释掉的互斥锁1、2、3后,输出没有大的变化,只是global变成了20。再注释掉$1、#2,输出结果就正常了。
管道是Linux支持的 IPC形式之一,具有以下特点:
- 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
- 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
- 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
- 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
实现了动态加载标准库中的数学函数库,输入函数名及参数,返回计算结果。
1 2 3 4 5 6 7 8 9 10 11 | /*
* #include <dlfcn.h>
* void *dlopen( const char *file, int mode );
* void *dlsym( void *restrict handle, const char *restrict name );
* char *dlerror();
* char *dlclose( void *handle );
* dlopen 使对象文件可被程序访问
* dlsym 获取执行了 dlopen 函数的对象文件中的符号的地址
* dlerror 返回上一次出现错误的字符串错误
* dlclose 关闭目标文件
*/ |
1 | void (*signal (int sigNum, void (*sigHandler)(int))) (int); |
乍一看这个函数原型就被唬住了,跟个指针似的。仔细分析一下,
*signal (int sigNum, void (*sigHandler)(int))
部分里面
(int sigNum, void (*sigHandler)(int))
优先级高于signal前面的*,所以这是个函数,*只是返回值的一部分,即返回的应该是一个指针。这个指针式什么类型的取决于
(*signal (int sigNum, void (*sigHandler)(int)))
外面的部分,前面是void,后面是(int),可见signal函数返回的是一个函数指针,其函数原型为void f(int)。返回的这个指针与signal的第二个参数的类型是一样的。于是乎,这个bt的函数原型声明还可以这样来定义:
1 2 | typedef void sigHandler(int); sigHandler *signal(int, sigHandler *); |
抑或是
1 2 | typedef void (*psigHandler)(int); psigHandler signal(int, psigHandler); |
这种声明看起来真累!
0x4000a963 <_dl_runtime_resolve+3>: movl 0x10(%esp,1),%edx 0x4000a967 <_dl_runtime_resolve+7>: movl 0xc(%esp,1),%eax 0x4000a96b <_dl_runtime_resolve+11>: call 0x4000a740
指令xchgl %eax,(%esp,1)将printf的地址放入栈顶。最精彩的一条指令当属ret $0×8,它将栈顶元素即printf的地址弹出至程序计数器PC,作为下一条将执行的指令地址,同时,清除堆栈中的0×10和0×8049560。此时堆栈中的情形,就如同直接调用了printf函数,似乎什么都没发生过。
此外
不知道我说清楚了没有,感觉说的很乱,文字也很乱。:-)
