1、编辑 -> 预编译 -> 编译 -> 汇编 -> 链接
•1) 编辑: vi hello.c -> hello.c
•2) 预编译:gcc -E hello.c -o hello.i -> hello.i -+
•3) 编译: gcc -S hello.i -> hello.s | GCC
•4) 汇编: gcc -c hello.s -> hello.o | 工具链
•5) 链接: gcc hello.o -o hello -> hello -+
2、静态库
对目标文件进行归档
增量编译(对于单个.c文件进行单独编译)
合并目标文件成库 利于管理
1.单个编译生成目标文件
gcc -c x.c
2.归档
ar -r libxxxx.a xx.o …
ar -q
ar -d
ar -t
ar -x
使用静态库:
gcc -I 头文件所在路径 main.c -l库名 -L库所在路径
3、动态库
1.单个编译生成目标文件
gcc -fpic|-fPIC -c x.c
2.生成动态库文件
gcc -shared *.o -o libxxx.so
使用动态库
gcc -I 头文件所在路径 main.c -l库名 -L库所在路径
a.out 依然依赖于动态库 LD_LIBRARY_PATH
静态库和动态库的区别:
1.本质
使用静态库 静态库中的代码指令复制一份到可执行程序中 程序在执行时不再依赖于静态库
使用动态库 在可执行程序中嵌入了动态库中的相对地址 程序在执行时跳到动态库相对位置执行代码
静态库 生成的最终可执行程序 比较大 运行相对较快 编译过程较慢
动态库 生成的最终可执行程序 比较小 运行相对较慢 编译过程较快
4、C语言的错误处理
通过函数的返回值来判断
(1)通过无效值来表示错误
<0表示出错 >=0 表示正确执行的结果
通过errno来表示错误
全局变量 errno
当调用函数失败时,基本上都会对errno进行赋值
char *strerror(int errnum);//通过错误码来获得错误信息
依赖于全局的errno
void perror(const char *s);//直接输出错误信息 s是额外的显示的内容
printf("%m") 输出成功 或者 输出错误信息
errno是全局变量,只有当函数失败时才会设置该值
当函数成功执行时,并不会修改该errno的值
注意:不能通过 if(errno != 0) 来判断函数出错了
只能通过函数的返回值来判断函数是否出错了,如果函数出错了,可能通过errno来获取错误信息
5、内存管理
虚拟内存
1个进程有4G独立的虚拟内存
0-3G 用户内存
3-4G 内核内存
代码区->数据段->BSS段->堆区->堆栈缓冲区(共享库、共享内存)->栈区->命令行参数 环境表->内核区
6、环境表:
环境变量=环境变量值 形式的字符串
PATH=/home/ubuntu
以字符串形式存在的环境变量的首地址存储在一个 指针数组中 指针数组称为环境表
以NULL表示环境表结束
char *envs[n];
用char **environ = &envs[0]; 全局变量来保存 指针数组中第一个元素的地址
错误处理
函数的返回值来表示错误
1.返回无效的值表示错误
<0表示错误 >=0表示正确的值
NULL/0xffffffff(-1)来返回错误 返回其它指针表示正确
2.-1表示出错 0表示正确
3.永远正确
全局变量 int errno;
当系统调用出现错误时 该全局变量errno会赋值,函数调用成功并不会重新置0
但是不能通过errno!=0来判断函数调用是否失败
只能确定函数调用失败之后来获得错误信息
输出错误信息
perror
strerror()
%m
8、虚拟内存
动态内存
STL–>new/delete->malloc/calloc/realloc/free->sbrk/brk–>mmap/munmap->kmalloc–>get_free_page
所谓的申请和释放动态内存其实是建立或者取消虚拟内存和物理内存之间的映射关系
stdlib.h
malloc 每次至少申请33页的虚拟内存 当申请的内存没有作用完成之后都不会申请新的内存
1内存页 = 4KB = 4096字节
free 即使把所有的动态内存都释放依然会保留最初的33页的内存
malloc(size) 其实 申请了 size+12个字节的大小
12个字节 动态内存块的控制信息
char *p = malloc(1);
p 能够访问
p[1-12] 控制信息块 如果修改 后续必须产生段错误
p[13- (334096-1)] 可以访问 未分配都操作不会出现错误的区域 该区域会分配给下一次malloc申请
malloc/calloc/realloc/free 底层维护一个双向链表 底层由 sbrk/brk实现
sbrk/brk底层维护一个指针 这个指针指向动态内存的末尾的下一个地址
void *sbrk(intptr_t increment);
increment>0相当于申请内存
increment<0相当于释放内存
increment=0获取指针位置
返回调用sbrk/brk之前 维护的这个指针的 位置
int brk(void *ptr);
sbrk/brk 以内存页为单位申请和释放动态内存
自动扩充页和回收页
9、内存映射
mmap 建立映射
munmap 取消映射
void *mmap(void *addr,//一般取NULL 由系统指定虚拟内存的起始位置
size_t length,//虚拟内存的大小 4096对齐 一页
int port, //权限 PROT_EXEC PROT_READ PROT_WRITE
int flag, //标识 MAP_FIXED MAP_SHARED MAP_PRIVATE MAP_ANONYMOUS
int fd,
off_t offset);
10、文件系统
在linux下面 一切设备皆文件
open/read/write/ioctl/close
C语言标准库里 操作文件的函数
fopen fread fwrite fclose ftell fseek fprintf fscanf fputs fgets …
FILE * 文件指针 结构体指针
open 打开 或者 创建并打开文件
create 创建新文件
close 关闭文件
read 读文件
write 写文件
lseek 调整文件读写位置
fcntl 文件属性控制
unlink 删除硬链接
rmdir 删除空目录
remove 删除硬链接 或者 空目录
mmap( ,int fd,off_t offset);
文件描述符
1.非负整数
2.打开一个文件就会相对应产生一个文件描述符
3.通过open函数返回 被内核空间记录
4.每个进程会默认提供三个文件描述符
0 STDIN_FILENO
1 STDOUT_FILENO
2 STDERR_FILENO
stdin 标准输入
stdout 标准输出
stderr 标准错误输出
输出缓存区
输入缓存区
提升读写效率 降低了实时性
程序写 ->fwrite->缓存区(到达一定情况下)->write->磁盘文件
程序写 ->write->磁盘文件
write用户态
写入磁盘 内核态
每一次调用write都会进行用户态和内核态的切换 频繁地进行用户态和内核态场景的切换
time 命令 可以测试 程序在运行所需要时间
real 总执行时间
user 用户空间执行时间
sys 内核空间执行时间
real = user+sys
strace 命令 跟踪系统调用
size 命令
text 代码区大小
data 数据段大小
bss bss区大小
dec 上面三个总大小 十进制
hex 十六进制总大小
filename 文件名
I/O input output 输入/输出
ftell fseek --> lseek
int fcntl(int fd, int cmd, … /* arg */ );
文件锁 (对不同的进程才有意义):
读锁 F_RDLCK: 共享锁 可以上很多读锁
写锁 F_WRLCK: 独占锁 只允许上一把写锁 不能再上读锁 和 写锁
解锁 F_UNLCK
同一个进程可以上任意把任意锁
1.fork
•1) 创建一个子进程,失败返回-1
•2) 调用一次,返回两次
分别在父子进程中返回子进程的PID和0。
用返回值的不同,可以分别为父子进程编写不同的处理分支。
•3) 子进程是父进程的副本
子进程获得父进程数据段和堆栈段(包括I/O流缓冲区)的拷贝,但子进程共享父进程的代码段。
•4) 函数调用后父子进程各自继续运行
其先后顺序不确定, 某些实现可以保证子进程先被调度。
•5) 共享文件表
函数调用后,父进程的文件描述符表(进程级)也会被复制到子进程中,二者共享同一个文件表(内核级)。
•6) 总进程数或实际用户ID所拥有的进程数,超过系统限制,该函数将失败。
•7) 一个进程如果希望创建自己的副本并执行同一份代码,或希望与另一个程序并发地运行,都可以使用该函数。
•8) 孤儿进程与僵尸进程。
2.vfork
该函数的功能与fork基本相同,二者的区别:
•1) 调用vfork创建子进程时并不复制父进程的地址空间
子进程可以通过exec函数族,直接启动另一个进程替换自身,进而提高进程创建的效率。
•2) vfork调用之后,子进程先被调度。
不可靠信号(非实时信号)
•1) 那些建立在早期机制上的信号被称为“不可靠信号”,小于SIGRTMIN(34)的信号都是不可靠信号。
•2) 不支持排队,可能会丢失。同一个信号产生多次,进程可能只收到一次该信号。
•3) 进程每次处理完这些信号后,对相应信号的响应被自动恢复为默认动作,除非显示地通过signal函数重新设置一次信号处理程序。
可靠信号(实时信号)
•1) 位于[SIGRTMIN(34),SIGRTMAX(64)]区间的信号都是可靠信号。
•2) 支持排队,不会丢失。
•3) 无论可靠信号还是不可靠信号,都可以通过sigqueue/sigaction函数发送/安装,以获得比其早期版本kill/signal函数更可靠的使用效果。
何为进程间通信
进程间通信(Interprocess Communication, IPC),是指两个或多个进程之间进行数据交换的过程。
client
1.socket
2.struct sockaddr_in addr = {}; 所连接的服务器的地址信息
3.connect 连接到指定的服务器
4.read/write send/recv sendto/recvfrom
5.close
UDP
server
1.socket
2.bind
3.recvfrom sendto
4.close
client
1.socket
2.sendto recvfrom
3.close
•1) TCP (Transmission Control Protocol, 传输控制协议) 面向连接的服务。
•2) UDP (User Datagram Protocol, 用户数据报文协议) 面向无连接的服务。
iso/osi模型
应用层,表示层,会话层,传输层,网络层,数据链路层,物理层
tcp/ip协议
应用层,传输层,互联网层,网络接口层
一、基本概念
1、线程就是程序的执行路线,即进程内部的控制序列,或者说是进程的子任务。
2、线程,轻量级,不拥有自己独立的内存资源,共享进程的代码区、数据区、堆区(注意没有栈区)、环境变量和命令行参数、文件描述符、信号处理函数、当前目录、用户ID和组ID等资源。
3、线程拥有自己独立的栈,因此也有自己独立的局部变量。
4、一个进程可以同时拥有多个线程,即同时被系统调度的多条执行路线,但至少要有一个主线程。
二、基本特点
1、线程是进程的一个实体,可作为系统独立调度和分派的基本单位。
2、线程有不同的状态,系统提供了多种线程控制原语,如创建线程、销毁线程等等。
3、线程不拥有自己的资源,只拥有从属于进程的全部资源,所有的资源分配都是面向进程的。
4、一个进程中可以有多个线程并发地运行。它们可以执行相同的代码,也可以执行不同的代码。
5、同一个进程的多个线程都在同一个地址空间内活动,因此相对于进程,线程的系统开销小,任务切换快。
6、线程间的数据交换不需要依赖于类似IPC的特殊通信机制,简单而高效。
7、每个线程拥有自己独立的线程ID、寄存器信息、函数栈、错误码和信号掩码。
8、线程之间存在优先级的差异。
线程管理
1、早期厂商各自提供私有的线程库版本,接口和实现的差异非常大,不易于移植。
2、IEEE POSIX 1003.1c (1995)标准,定义了统一的线程编程接口,遵循该标准的线程实现被统称为POSIX线程,即pthread。
3、pthread包含一个头文件pthread.h,和一个接口库libpthread.so。
4、功能
•1) 线程管理:创建/销毁线程、分离/联合线程、设置/查询线程属性。
•2) 线程同步
A. 互斥量:创建/销毁互斥量、加锁/解锁互斥量、设置/查询互斥量属性。
B. 条件变量:创建/销毁条件变量、等待/触发条件变量、设置/查询条件变量属性。
四、线程函数
•1) restrict: C99引入的编译优化指示符,提高重复解引用同一个指针的效率。
•2) 在pthread.h头文件中声明的函数,通常以直接返回错误码的方式表示失败,而非以错误码设置errno并返回-1。
•3) main函数即主线程,main函数返回即主线程结束,主线程结束即进程结束,进程一但结束其所有的线程即结束。
•4) 应设法保证在线程过程函数执行期间,其参数所指向的目标持久有效。