1. 编译的四个步骤:
- 预处理
gcc -E test.c -o test.i
- 编译:将高级语言编译成汇编语言
gcc -S test.c -o test.s
- 汇编:将汇编语言编译成二进制的目标代码
gcc -c test.c -o test.o
- 链接:包含各函数的入口,得到可执行代码
gcc -o test test.c
2. gdb调试
(1) 在编译时,需要加上调试信息-g
参数;
l:list列出各行编号;
n:next下一行执行;
c:continue下一个断点;
b 行号:在相应的行号设置断点;
p 变量名:打印这个变量的值。
3. 系统限制
- 本身平台的限制:32位平台or64位平台
- 数据类型的限制:
/usr/include/limits.h
- 系统本身的限制:系统的资源是有限的,不可能无限制的申请资源
命令行:ulimit
来修改和获取
编程时:getrlimit
函数获取,setrlimit
来设置系统的限制,可用man getrlimit
查看
4. 命令行参数
getopt
:获取短选项
getlongopt
:获取长选项
以下全局变量要配合getopt
函数:
extern char* optarg;
extern int optind;
extern int optopt;
extern int opterr;
5. 程序结构与进程的结构
一个可执行程序包含三个部分:
- 代码段(text):主要存放指令,操作以及只读的(常量)数据
- 数据段(data):全局或者静态的已经初始化的变量
- BBS段(bbs):全局或者静态的未初始化的变量
执行一个程序(存放在外存中),系统如何为其中申请的内存空间?
进程:Linux操纵系统最小资源管理单元,一个程序运行,必然首先为其创建一个(或多个)进程,进程是有生命周期的,一个进程是执行的程序段,资源有哪些?
因为在程序的执行时,会动态的申请空间,执行子函数,因此Linux对一个进程的管理采用以下方式:
在执行程序时,系统首先在内核空间中创建一个进程,为这个进程申请一个PCB(进程控制块tast_struct),用于管理整个进程的所有资源。其中mm_struct成员用来管理与当前进程相关的所有内存资源。(内存资源包含(1)代码段,数据段,BBS段。这三个段直接从磁盘拷贝到当前内存空间,大小相等。(2)动态空间:堆,栈空间,mmap段[映射其他的库相关的信息])
在Linux系统中查看进程信息:
搜索指定进程的相关信息
有关ps aux|grep a.out
详细信息移步链接,其中第二列的数字表示进程的pid。
查看进程映射文件maps信息
起始地址和终止地址并不是真正的物理地址,而是虚拟地址空间,为什么需要虚拟地址?是出于堆资源的保护,对系统来说,内存资源是宝贵的,而一个程序执行并不需要立即将所有的资源全部加载到内存中,而实际上棵采用写时申请的方式。
好处:(1)保护系统:用户程序非法访问造成内核的崩溃,因此发生段错误,使执行这个非法访问的进程自动退出。(2)节约资源:采用内存映射的方法,一个程序执行时并不是立即将所有的代码和空间映射到内存空间,而是使用时用缺页的方法真正申请内存空间,一是提高效率,二是节约内存空间。
在32为平台中,一个进程拥有自己的4G的虚拟空间,与其他进程无关,因此进程与进程之间是独立的。
6. 进程空间的地址申请
(1) 代码段,数据段,BSS段,这三个部分直接从磁盘拷贝到内存。起始地址在当前的32位平台linux下为0x0804a000地址。
(2) 堆。动态变化。malloc系列申请。
(3) mmap映射文件(普通文件,也可以是其他类型的文件)
(4) 栈段。
(5)高地址1G空间共内核映射处理的。用户不能直接处理。
堆和栈的起始地址默认是随机参数的,其目的是避免安全漏洞
堆:可以在堆中申请空间的起始地址(通过brk
和sbrk
来改变)
int brk(void *addr);
:指定下次申请堆空间的起始地址为addr
void *sbrk(intptr_t increment);
:为当前的地址位置后移increment字节,如果为0,返回当前值
系统执行一个进程,到底怎么加载这些空间?——使用strace工具查看
堆空间的起始地址是随机的,可以设置不随机,大小也可以设置固定大小
(1)代码段、数据段、BSS段的地址已经在编译链接时固定
(2)在BSS段结束与堆起始地址有间隙,这个大小时随机值**
(3) brk函数仅仅是调整在堆中申请空间的起始值
(4)系统默认为每个进程分配的堆空间的大小是固定的。使用sbrk(0)
得到的是我们堆空间的结束值。第一次是使用malloc
申请资源返回的地址接近堆空间的起始值。使用brk(addr)
改变的是新申请数据的堆空间起始值
(5)在真正编程时,很少用brk/sbrk
,使用malloc
函数来新申请堆空间,效率更高。部分时间使用mmap
来映射mmap
区。
栈:栈从高地址向低地址增长,栈的起始地址也是随机的。栈中主要存放的是局部变量,新调用的子函数时函数的参数及返回值,由OS自动管理。
7. 编程中内存的申请与释放
- 代码段:由只读数据(const字符串常量)和代码组成。这些内容的空间在编译链接时申请且指定存储地址。
- 数据段和BSS段:定义的全局的或者静态的变量。已经初始化的在数据段,未初始化的在BSS段中。这些内容的空间在编译链接时申请且指定存储地址。
以上空间的申请在运行之时就加载到内存中,直到程序的结束,都不在发生变化(除exceX函数替换外)。因此这些变量申请的空间生存周期就是整个程序,如果在相应的作用域中,可一直访问,也可以根据这个变量的地址访问。
通过符号访问一个变量,要在他的作用域中,但是,对于以上三个段中的数据,只要知道地址,其实可以通过地址间接访问这个空间,且在整个程序执行期间都可以访问。在编程中,以上空间的申请就是定义相应的变量或者常量。
堆。动态申请,在C中用
malloc/delete
等,C++中用malloc/delete
。
堆的起始位置由brk
函数来指定。但具体编程中,一般不会自己使用brk
函数,而是使用malloc
库函数。内核对内存的管理是页式管理,因此在malloc
申请空间时,使用链式结构来管理已经申请的堆空间。
void *calloc(size_t nmemb, size_t size);
:
void *malloc(size_t size);
:申请指定大小的内存空间,虚拟地址空间
void free(void *ptr);
:释放内存空间
void *realloc(void *ptr, size_t size);
:调整已经申请的堆中的数据空间
注意事项:
(1)ptr_new = realloc(ptr, 200)
在真正实现上,如果ptr后有足够的空间,则直接扩展;如没有足够的空间,会重新查找一个(>=200)大小空间,将原来的数据复制到新的空间中,然后释放原来的空间。如果查找不到足够的空间,则返回null。因此不能直接写ptr= realloc(ptr, 200)
,这样如果申请失败,会丢失原来的ptr
(2)free(ptr)
时,需要加一句ptr = NULL
,来避免后面的双重释放。
(3)申请和释放一定需要匹配,避免不必要的内存泄露栈。动态申请。栈空间在加载程序,创建进程时就申请了一个范围,栈是由OS来主要管理。
需要考虑两个问题:为什么不能返回局部变量的地址?局部变量的生命周期是怎样的?
对于未申请的堆空间地址访问,一般会出现段错误;
对于已经释放的函数栈空间的访问,一般不会出现段错误,但是是非法访问,是不允许的;
在编程中,对任何空间的访问一定要保证这个空间已经申请且在控制范围内。
对任何空间地址值的操作仅仅是数据的操作,没有任何的问题。mmap的库以及相关文件。将一个文件内容的全部或部分映射到虚拟地址空间中,操作这段内存空间会同步到磁盘文件的操作,效率比较高。
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
// 参数意义:
void *mmap(
void *addr, // 映射到哪个虚拟地址,一般为NULL,让系统选择;如果自己设置,最好是页的整数倍,getpagesize
size_t length, // 大小
int prot,
int flags,
int fd, // 加载的文件对应打开的文件描述符
off_t offset // 偏移量,从文件哪个位置开始
);
prot:
PROT_EXEC Pages may be executed.
PROT_READ Pages may be read.
PROT_WRITE Pages may be written.
PROT_NONE Pages may not be accessed.
flags:
MAP_SHARED Share this mapping. Updates to the mapping are visible to other processes that map this file,
and are carried through to the underlying file. The file may not actually be updated until
msync(2) or munmap() is called.
MAP_PRIVATE
Create a private copy-on-write mapping. Updates to the mapping are not visible to other pro‐
cesses mapping the same file, and are not carried through to the underlying file. It is
unspecified whether changes made to the file after the mmap() call are visible in the mapped
region.
8. 常见的内存错误以及valgrind使用
- 代码段:这部分为只读数据。因此对这个一部分的数据,试图写只读数据,编译的时候会报waring。
- 数据段/BSS段:未初始化直接访问,即使没有显示初始化,仍然会初始化为0。
- 栈数据空间:为局部变量,未初始化会给随机值;栈溢出,在栈中申请过大的局部变量。
- 堆空间:内存泄漏,申请未释放;申请后,双重释放
- 对于所有的地址空间:
(1)野指针问题,访问未初始化指针,
(2)越界访问
(3)非法的越界文芳
(4)空间不再控制范围仍然去访问空间
使用工具:来检测常见的内存错误,valgrind工具。
gcc -o valgrind_example01 valgrind_example01.c -g
valgrind --tool=memcheck --show-reachable=yes --read-var-info=yes --verbose --time-stamp=yes --leak-check=full--log-file=mycode.log ./valgrind_example01
less mycode.log
如果要使用图形化的工具,要安装QT,这个工具名字叫valkyrie
9.Posix磁盘文件内容管理—文件描述符
-
Linux操作系统内核态与用户态:
用户态与内核态
磁盘文件也是由OS来管理,因此,要访问磁盘文件需要系统调用 -
怎么让一个程序(进程)与要操作的文件建立联系?
要去访问磁盘的文件,必须通过系统调用来返回一个与该文件相关的ID,这个ID就是文件描述符(file descriptor)
具体过程:
Linux操作系统提供了open
系统调用,任何的进程要去访问一个文件,首先使用open
打开这个文件,系统将返回一个编号,即与这个文件相关联的文件描述符。
(1)用户程序从用户空间向内核提交了打开申请;
(2)操作系统会在内核中去检查请求是否合法,如果合法,在内核中申请这个打开文件相关的信息(读写位置,在磁盘中的位置......全用struct files来存储)。并添加到当前的PCB块中的打开文件列表数组中。对应的这个数组下标即为文件描述符值。
(3)将这个文件描述符返回给用户空间,用户空间接下来对这个文件进行读写就通过这个编号值。
系统默认为每个进程打开三个文件:
文件描述符0(标准输入,键盘STDIN_FILENO), 1(标准输出,显示器STDOUT_FILENO), 2(标准错误输出,显示器STDERR_FILENO)
10.Posix磁盘文件内容管理—系统调用函数
- 建立与断开联系
open/close
:即进程与磁盘建立/断开联系
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
参数说明:
pathname:打开文件路径
flags:打开方式标志
O_RDONLY :只读
O_WRONLY:只写
O_RDWR:读写
O_APPEND:追加
mode:文件权限
新创建一个文件的真正权限是mode & ~umask,
这个文件打开成功,将返回一个新的文件描述符值,
后面针对这个文件的操作就是用这个文件描述符。
// 示例代码
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
int fd;
fd = open(argv[1], O_RDONLY|O_CREAT, 0644);
if( -1 == fd )
{
perror("open");
exit(EXIT_FAILURE);
}
printf("fd: %d\n", fd);
close(fd);
return 0;
}
- 读写文件内容
read/write
,即将磁盘中的内容读到内存/将内存中的内容写入磁盘
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
// 从fd所指向的文件中读取count字节到buf为首地址的内存空间中
ssize_t write(int fd, const void *buf, size_t count);
// 往fd所指向的文件中写入count字节,这个内容存放在buf为首地址的内存空间中
- 文件位置的修改
lseek
:对当前文件的读写位置进行定位,可以在文件中添加空洞。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
DESCRIPTION
The lseek() function repositions the offset of the open file associated with the file descriptor fd to
the argument offset according to the directive whence as follows:
SEEK_SET // 文件开头
The offset is set to offset bytes.
SEEK_CUR // 文件当前位置
The offset is set to its current location plus offset bytes.
SEEK_END // 文件结尾
The offset is set to the size of the file plus offset bytes.
The lseek() function allows the file offset to be set beyond the end of the file (but this does not
change the size of the file). If data is later written at this point, subsequent reads of the data in
the gap (a "hole") return null bytes ('\0') until data is actually written into the gap.
也可以用这个函数来实现文件大小的获取。把文件的读写位置设置为文件结束,因为这个函数返回当前读写位置距离文件头的偏移字节数。
11.Posix磁盘文件内容管理—文件描述符复制与锁定
- 复制功能,用处:在重定向应用中
cat test>test.txt
dup/dup2/fcntl
复制后,两个文件描述符都指向同一个文件表项,即使用这两个文件描述符中的一个就会影响读写相关的信息。
#include <unistd.h>
int dup(int oldfd); //复制oldfd这个文件描述符,返回在调用此函数前最小未使用的文件描述符值。
close(0);
dup(3);
这个代码可以实现输入的重定向。
默认从0读数据,现在0指向的表项被复制为3,
这样从0读实际上就是从3指向的文件读,
也就实现的输入的重定向。
int dup2(int oldfd, int newfd); // 把oldfd复制为newfd,如果newfd已经对应一个打开的文件,则先关闭它。
dup2(3,0);
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
cmd为:F_DUPFD时为复制文件描述符
- 锁定功能:并发的环境下,除了使用并发的工具来保护共享文件外,也可以使用文件锁。
flock/fcntl
#include <sys/file.h>
int flock(int fd, int operation);
参数: operation
LOCK_SH Place a shared lock. More than one process may hold a shared lock for a given file at a given time.
LOCK_EX Place an exclusive lock. Only one process may hold an exclusive lock for a given file at a given time.
LOCK_UN Remove an existing lock held by this process.
强调一下:这里指的是锁定文件描述符,锁定的是整个文件表项,防止被其他的进程访问,锁整个文件。
如果要锁定某一部分,可以用fcntl
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
cmd:
F_GETLK, F_SETLK and F_SETLKW are used to acquire, release, and test for the existence of record locks
(also known as file-segment or file-region locks). The third argument, lock, is a pointer to a structure
that has at least the following fields (in unspecified order).
struct flock {
...
short l_type; /* Type of lock: F_RDLCK,
F_WRLCK, F_UNLCK */
short l_whence; /* How to interpret l_start:
SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock
(F_GETLK only) */
...
};
属性控制、权限状态、拥有者等
使用fcntl
提高效率, 同步磁盘
可以使用mmap函数来某个打开的文件映射到虚拟地址空间,以后操作这个虚拟地址空间就类似于操作这个文件。
实现大于2G的文件的拷贝操作。如果文件大于2G,偏移超过int_32类型的限制,为了实现大文件的拷贝,必须使用宏。方法如下
(1)在所有的头文件包含前加上
#define _LARGEFILE_SOURCE
#define _FILE_OFFSET_BITS 64
(2)在编译时,加上宏
gcc -D _LARGEFILE_SOURCE -D _FILE_OFFSET_BITS =64
12文件流与目录流管理—ANSI文件流,文件描述符与缓冲区类型
问题一:每执行一次读写,都要执行系统调用,系统调用会导致CPU的切换,比较耗时,频繁的读写会导致耗时较多。
问题二:使用系统调用时,可移植性问题。
为了提高效率,ANSI C提出了文件流的概念,即在用户空间的文件描述符的基础上进行封装,添加缓冲区和读写位置信息,使功能更强。
流的一个重要提高效率的办法是使用缓冲区,将多次的系统调用合并成一次系统调用,节约执行的时间。
流和缓冲区:
全缓冲:只有当数据量达到某个限制后(不同的平台有差异,通常为4096),或者主动的要求刷新缓冲区(如close()/fflush
),才真正执行一定的系统调用操作。常见的哪些流是全缓冲:文件流,即以fopen
打开的文件。
行缓冲:只有当数据量达到某个限制后(不同的平台有差异,通常为128或1024),或者遇到换行,或者主动的要求刷新缓冲区,才真正执行一定的系统调用操作。常见的哪些流是行缓冲:终端命令行。
无缓冲:需要实时显示信息的,例如标准的错误输出,如fprintf(stderr)
默认的三个打开的文件对应的流:
/* Standard streams. */
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
- 如果要修改缓冲的类型或者自己指定对应缓冲区的位置,可以使用
setbuf/setvbuf
:
#include <stdio.h>
void setbuf(FILE *stream, char *buf);
void setbuffer(FILE *stream, char *buf, size_t size);
void setlinebuf(FILE *stream);
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
参数mode:
_IONBF unbuffered
_IOLBF line buffered
_IOFBF fully buffered
13文件流与目录流管理—ANSI文件流操作函数详解
- 文件流的操作:编程中用到的函数是ANSI库函数,之前用到的
open/read/write/close
为系统调用函数 - 打开/关闭
open/close
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
参数说明:
path:打开文件路径
mode:打开方式
r:只读,对应系统文件中的open->O_RDONLY
w:只写,open->O_WRONLY|O_CREAT|O_TRUNC
a:追加,open->O_APPEND|O_CREAT|O_WRONLY
X+:可读可写,open->RDWR
FILE *fdopen(int fd, const char *mode);
将一个文件描述符封装成一个流
FILE *freopen(const char *path, const char *mode, FILE *stream);
将原来的stream重新封装成另外mode的流
使用完成后close这个流对象,期间会刷新流的缓冲区
- 读/写
int fgetc(FILE *stream);
char *fgets(char *s, int size, FILE *stream);
int getc(FILE *stream);
int getchar(void);
char *gets(char *s);
int ungetc(int c, FILE *stream);
的