4.1 UNIX_C
UNIX C编程
一、开发环境
- C/C++/数据结构/算法:平台无关、算法逻辑
- UC/Win32/Android/IOS:平台相关、系统调用
- 嵌入式/驱动/移植:硬件相关、硬件接口
1.Unix操作系统
1.简介
- 美国AT&T公司,贝尔实验室,
- 1971年,
- 肯.汤普逊、[丹尼斯.里齐]。
- PDP-11,多用户,多任务,多处理器架构。
- 高安全性、高可靠性、高稳定性。
- 关键业务系统,商业应用。
- 移动终端、嵌入式设备。
2.三大版本派系
(1).System V
- AIX,IBM,银行
- Solaris,SUN->Oracle,电信
- HP-UX,惠普,教育,科研
- IRIX,图形工作站
(2).Berkley
- FreeBSD
- NetBSD
- OpenBSD
- Max OS X
(3).Linux
- Minix:迷你版UNIX系统
- Linux:GPL,免费开源,商用服务器(RedHat),桌面(Ubuntu),嵌入式(Android)
2.Linux操作系统
1.简介
- 类Unix操作系统,免费开源。
- 不同发行版本使用相同内核。
- 严格意义上讲,Linux仅指操作系统内核。
- 隶属于GNU(GNU Not Unix)工程。
- 发明人:Linus Torvalds。
- 标志:名为Tuxedo的小企鹅。
2.POSIX标准
- IEEE和ISO合作指定。
- Portable Operating System Interface for
- Computing Systems
- 统一系统编程接口规范
- 基于POSIX接口的开发是可移植的。
3.GPL
- 通用公共许可证
- 允许对某成果及其派生成果的重用、修改和复制,对所有人都是自由的,但不能声明做了原始工作。
4.版本
(1).早期版本:0.01,0.02,…,1.0
(2).旧计划:介于1.0和2.6之间,A.B.C
A:主版本号,内核大幅更新
B:次版本号,内核重大修改,奇数测试版,偶数稳定版
C:补丁序号,内核轻微修订
(3).2003年12月发布2.6.0以后:发布周期缩短,A.B.C-D.E
D:构建次数,反映极微小的更新
E:描述信息
rc/r - 候选版本,后面跟的数字表示第几个候选版本,数字越大越接近正式版
smp - 对称多处理器
pp - Red Hat测试版
EL - Red Hat正式企业版
mm - 针对最新技术的体验版本
fc - Fedora Core版本
$ cat /proc/version
5.主流Linux发行版本
- Ubuntu - 大众化
- Linux Mint - 时尚
- Fedora - RedHat的桌面版,应用丰富
- openSUSE - 外观华丽,欧洲国家受欢迎
- Debian - 开放,自由,免费
- Slackware - 简洁,朴素,专业化
- RedHat - 经典,支持丰富
3.GNU编译工具-GCC
支持多种编程语言
C、C++、Objective-C、Java、Fortran、Pascal、ADA
支持多种平台
Unix、Linux、Windows
构建(Build)过程
编辑->预编译->编译->汇编->链接
1)编辑:vi hello.c
2)预编译:gcc -E hello.c -o hello.i
3)编译:gcc -S hello.i
4)汇编:gcc -c hello.s
5)链接:gcc hello.o -lc -o hello
GCC工具链。
gcc版本
gcc -v
文件后缀
.h - C语言源代码头文件
.c - 预处理前的C语言源代码文件
- .i - 预处理后的C语言源代码文件
- .s - 汇编语言文件
- .o - 目标文件
- .a - 静态库文件
.so - 共享库(动态库)文件
编译单个源程序
gcc [选项参数] 文件
-c - 编译+汇编
- -o - 指定输出文件路径
- -E - 预编译
- -S - 编译,产生汇编文件
- -pedantic - 对程序中不符合标准C的代码给出 警告
- -Wall - 产生尽可能多的警告
- -Werror - 将警告作为错误
- -x - 指定源代码的语言
- -g - 生成调试信息
-O0/O1/O2/O3 - 优化等级,缺省O1,O0表示不做优化,某些系统上的gcc支持Os,相当于O2.5
编译多个源程序
gcc [选项参数] a.c b.c c.c d.c
makefile/make
- 头文件的作用
a)声明外部变量和函数。
extern int g_x;
int add (int, int);
b)定义宏、类型别名、自定义类型。
#define PAI 3.14159
typedef unsigned int UINT;
struct Student {
char name[128];
int age;
};
c)公共头文件
#include
#include
#include
d)头文件卫士,防止因钻石包含导致重定义
a.h
/ \
b.h c.h
\ /
d.c
gcc的-I选项:指定头文件的附加搜索路径。
include <…>
系统目录->-I指定的目录
通常的系统目录:
/usr/include
/usr/local/include
/usr/lib/gcc/i686-linux-gun/4.6/include
/usr/include/c++/4.6, C++编译器使用
预处理指令
#include
#define PAI 3.14
#undef PAI
#if
#ifdef PAI
#ifndef
#else
#elif
#endif
##
#
#error // 产生错误,结束并终止预编译过程
#warning // 产生警告,并不终止预编译过程
#line // 指定行号
#pragma // 为处理器提供额外信息
#pragma GCC dependency <文件> // 如果<文件>比当前文件新,产生警告
#pragma GCC poison <标识> // 如果程序中出现<标识>,直接报错
#pragma pack(1/2/4/8) // 对结构体型变量,按1/2/4/8字节做对齐和补齐
- 预定义宏
__BASE_FILE__ // 正在被编译的源文件名
__FILE__ // 所在的文件名
__LINE__ // 所在行号
__FUNCTION__ // 所在函数名
__func__ // 同__FUNCTION__
__DATE__ // 编译日期
__TIME__ // 编译时间
__INCLUDE_LEVEL__ // 包含层数,基0
__cplusplus // 是否C++编译器,是C++编译器此宏为1,不是C++编译器此宏无定义
- 环境变量
C_INCLUDE_PATH
CPATH //C头文件的附加搜索路径
CPLUS_INCLUDE_PATH //C++头文件的附加搜索路径
LIBRARY_PATH //链接时库文件搜索路径
LD_LIBRARY_PATH //运行时库文件搜索路径
4.库
为什么需要库?
二进制形式目标模块的包。
a.o \
b.o > abc.a ——库
c.o /
库的类型
(1).静态库:扩展名.a。库中所封装的二进制代码,在链接阶段被复制到调用模块中。
(2).共享库:扩展名.so。库中所封装的二进制代码,在链接阶段并不被复制到调用模块中,被嵌入到调用模块中的仅仅是被调用函数在共享库中的相对地址。在运行时,根据这个地址动态地执行共享库中的代码。
静态库
(1).编辑源程序:.c/.h
(2).编译成目标文件:gcc -c xxx.c -> xxx.o
(3).打包成静态库文件:ar -r libxxx.a xxx.o …
$ gcc -c calc.c
$ gcc -c show.c
$ ar -r libmath.a calc.o show.o
ar [选项] 静态库名 目标文件列表
-r:将目标文件插入到静态库中,若已存在
则更新
-q:将目标文件追加到静态库尾部
-d:从静态库中删除指定的目标文件
-t:列表显示静态库中的目标文件
-x:将静态库展开为目标文件
(4).调用静态库
$ gcc main.c libmath.a (直接法)
$ gcc main.c -lmath -L. (参数法)
$ export LIBRARY_PATH=$LIBRARY_PATH:.
$ gcc main.c -lmath (环境法)
为了使环境的设置持久化,把设置环境变量
的命令发到~/.bash_profile中。
共享库
(1).创建共享库
A. 编辑源程序:.c/.h
B. 编译成目标模块:
gcc -c -fpic xxx.c -> xxx.o
C. 链接成共享库:
gcc -shared xxx.o … -o libxxx.so
PIC,Position Independent Code,位置无关码。可执行程序加载共享库时,可将其映射到其地址空间的任何位置。
-fPIC - 大模式,代码量大,速度慢,所有平台都支持。
-fpic - 小模式,代码量小,速度快,仅一部分平台支持,如Linux。
(2).使用共享库
A. 静态加载
$ gcc main.c libmath.so
$ gcc main.c -lmath -L.
$ export LIBRARY_PATH=$LIBRARY_PATH:.
$ gcc main.c -lmath
运行时需要保证LD_LIBRARY_PATH环境变量中包含共享库所在的路径。
B. 动态加载
#include <dlfcn.h>
a.加载共享库
void* dlopen (
const char* filename,
int flag
);
filename:若只给共享库文件名,则通过
LD_LIBRARY_PATH环境变量搜索共享库。若给共享库路径,则按照路径加载,不使用环境变量。
flag:
RTLD_LAZY - 延迟加载,什么时候使用共享库再实际载入之。
RTLD_NOW - 立即加载。
成功返回共享库句柄,失败返回NULL。
b.获取函数地址
void* dlsym (
void* handle, // 共享库句柄
const char* symbol // 函数名
);
成功返回函数地址,失败返回NULL。
c.卸载共享库
int dlclose (
void* handle // 共享库句柄
);
成功返回0,失败返回非零。
d.获取错误信息
char* dlerror (void);
返回错误信息字符串,没有错误信息返回NULL。
gcc main.c -ldl
5.辅助工具
- nm:查看目标文件、可执行文件、静态库、共享库中符号列表。
- ldd:查看可执行程序或共享库的动态依赖。
- ldconfig:事先把共享库的路径信息写到
- /etc/ld.so.conf配置文件中,ldconfig根据该配置文件生成/etc/ld.so.cache缓冲文件,并将该缓冲文件读入内存,提高动态库的加载效率。系统启动时自动执行ldconfig,若修改了共享库配置,则需要手动执行该程序,更新缓冲。
- strip:通过删除符号表和调试信息,给目标文件、可执行文件、库文件减肥。
- objdump:对机器指令做反汇编。
6.错误处理
1.通过函数的返回值表示错误
(1). 返回合法值表示成功,返回非法值表示失败。
(2). 返回有效指针表示成功,返回空指针(NULL/0xFFFFFFFF)表示失败。
(3). 返回0表示成功,返回-1表示失败,如果有需要返回给调用者的数据,可以通过指针型参数向其输出。
(4). 如果一个函数永远不会失败,也没数据需要提供给调用者,可以没有返回值。
2.通过错误码获得函数失败的原因
#include <errno.h> // extern int errno;
(1). 通过errno全局变量获取出错原因。
(2). 将errno转换为一个字符串:
#include <string.h>
char* strerror (int errnum);
#include <stdio.h>
void perror (const char* s);
printf ("%m");
所有的错误码都非零,errno == 0表示无错误。
(3). errno在函数执行成功的情况下不会被修改,因此不能以errno非零作为发生错误的判断依据,除非在调用函数前人为将其复位为0。
(4). errno是一个全局变量,其值随时有可能发生变化,线程不安全。
7.环境变量
1.环境表
(1). 每个进程都会接收到一张环境表,是一个以NULL指针结尾的字符指针数组。
(2). 全局变量environ保存了环境表的首地址。
(3). main函数的第三个参数就是环境表的首地址。
2.环境变量函数
include
- 环境变量:\\=\
- getenv - 根据name获得value
- putenv - 以\\=\形式设置环境变量。如果name不存在,就添加,存在修改原来的value
- setenv - 根据name设置value,若name以存在,根据参数决定是否覆盖原value
- unsetenv - 删除环境变量
- clearen - 清空环境变量,environ == NULL
二、内存管理
1.进程映像
1.程序是保存在磁盘上的可执行文件,如:a.out、ls、gcc、qq.exe。
2.运行程序时,需要把磁盘上的可执行文件,加载到内存中,形成进程。
3.一个程序(文件)可以同时存在对个进程(内存)。
4.进程在内存空间中的布局就是进程映像。从低地址到高地址依次是:
代码区(text):可执行指令、字面值常量、具有常属性且初始化的全局和静态变量。只读。
数据区(data):不具常属性且初始化的全局和静态变量。
BSS区(bss):未初始化的全局和静态变量。
进程一加载此区即被清0。
堆区(heap):动态内存分配。
栈区(stack):非静态局部变量。
命令行参数和环境变量区
在堆区和栈区之间会留有一段空隙,一方面为堆和栈的增长预留空间,同时共享库、共享内存也会占用这个区域。
---- 命令行参数与环境变量 ----
环境变量:0xbfb1aebc
命令行参数:0xbfb1aeb4
-------------- 桟 -----------
常局部变量:0xbfb1adf8
前局部变量:0xbfb1adfc
后局部变量:0xbfb1ae00
-------------- 堆 ------------
后堆变量:0x97d2018
前堆变量:0x97d2008
------------- BSS ------------
未初始化全局变量:0x804a03c
未初始化静态变量:0x804a038
------------ 数据 ------------
初始化静态变量:0x804a028
初始化全局变量:0x804a024
------------ 代码 ------------
常静态变量:0x8048a50
字面值常量:0x80487b4
常全局变量:0x80487b0
函数:0x80484f4
PID = 3163
2.虚拟内存
每个进程都有各自独立的4G字节的虚拟地址空间。
用户程序中使用的都是虚拟地址空间中的地址,永远无法直接访问实际物理内存地址。
虚拟内存到物理内存的映射有操作系统动态维护。
虚拟内存一方面保护了操作系统的安全,另一方面允许应用程序使用比实际物理内存更大的地址空间。
4G的进程空间分为两部分,0~3G-1为用户空间,3G~4G-1为内核空间。
用户空间中代码不能直接访问内核空间中的代码和数据,但是可以通过系统调用进入内核态,间接地与内核交互。
对内存的越权访问,或访问未建立映射的虚拟内存,将会导致段错误。
int* p;
*p = 100;
--------------
int a;
int* p = &a;
*p = 100;
--------------
int* p = malloc (sizeof (int));
*p = 100;
用户空间对应进程,进程一切换,用户空间随即变化。内核空间由操作系统内核使用,不会随进程切换而变化。内核空间由内核根据独立且唯一的页表init_mm.pgd进行映射,而用户空间的页表则每个进程一份。
每个进程的内存空间完全独立,因此在不同进程之间交换虚拟地址毫无意义。
标准库的内存分配函数(malloc/calloc/realloc)需要用一套数据结构维护动态分配的内存,因此会分配比实际要求的内存多12个字节的内存,用户存储某些控制信息。该信息一旦被破坏,将导致后续操作,如free等,出现异常。
虚拟内存到物理内存的映射以页(4096字节)为单位。通过malloc函数首次分配内存,至少映射33页。即使通过free函数,释放掉全部动态分配的内存,最初的33页仍然保留,直到进程退出这33页才会真正被解除映射。
#include <unistd.h>
int getpagesize (void);
返回内存页的字节数。
char* pc = malloc (sizeof (char));
|
v<------------ 33页 ----------->|
------+-----+----------+--------------+----
|1字节| 控制信息 | |
------+-----+----------+--------------+----
^ ^ ^ ^ ^
段错误 OK 后续错误 不稳定 段错误
3.内存管理APIs
#include <unistd.h>
void* sbrk (
intptr_t increment // 内存增量(字节)
);
返回上次调用sbrk/brk函数后的末尾指针,失败返回-1。
increment取值:
0 - 获取末尾指针
>0 - 增加内存空间
<0 - 释放内存空间
内部维护一个指针,指向当前堆内存最后一个字节的下一个位置。
sbrk函数根据增量参数调整该指针的位置,同时返回该指针被调整之前的位置。
若发现页耗尽或空闲,则自动追加或解除页映射。
#include <unistd.h>
int brk (
void* end_data_segment // 内存块尾地址
);
成功返回0,失败返回-1。内部维护一个指针,指向当前堆内存最后一个字节的下一个位置。brk函数根据指针参数设置该指针的位置。若发现页耗尽或空闲,则自动追加或解除页映射。sbrk/brk底层维护一个指针位置,以页(4K)为单位映射和解映射内存,并根据参数调整所维护的指针位置。简便起见,用sbrk分配内存,用brk释放内存。
简化版malloc/free的实现。
#include <sys/mman.h>
void* mmap (
void* start, // 映射区首地址,
// NULL系统自动选择
size_t length, // 映射区长度(字节)
// 按页取整
int prot, // 映射权限
int flags, // 映射标志
int fd, // 文件描述符
off_t offset // 文件偏移量
);
成功返回映射区首地址,失败返回MAP_FAILED(-1)。
prot取值:
PROT_EXEC - 映射区可执行
PROT_READ - 映射区可读
PROT_WRITE - 映射区可写
PROT_NONE - 映射区不可访问
flags取值:
MAP_FIXED - 若在start上无法创建映射, 返回失败,无此标志则自动调整
MAP_SHARED - 对映射区的写操作直接反映到文件中
MAP_PRIVATE - 对映射区的写操作只反映到内存中,不进文件
MAP_ANONYMOUS - 内存映射,将虚拟地 址映射到物理内存中
MAP_DENYWRITE - 拒绝其它对文件的写入
MAP_LOCKED - 锁定映射区域
int munmap (
void* start, // 映射区首地址
size_t length // 映射区长度(字节)
// 按页取整
);
成功返回0,失败返回-1。
mmap/munmap:底层不维护任何东西,只是返回一个建立映射的虚拟地址。
brk/sbrk:底层维护一个指针,记录堆尾。
malloc/free:底层维护一个双向链表,存储内存块的控制信息。
总结:
Unix内存管理的函数:
malloc() free() - 标C函数
sbrk() brk() - Unix的系统函数
mmap() munmap() - Unix的系统函数
malloc申请内存时,如果是小块内存,一次映射33个内存页(物理内存),分配申请的数量(虚拟内存)。malloc()除了分配申请的内存之外,还需要额外的空间存储附加数据。 如果申请的是大块内存,一次映射比申请的稍多的内存页,分配申请的数量。
sbrk()和brk()是Linux的系统函数,本身具备分配和回收内存的能力。sbrk()分配内存简便,brk()回收内存方便。sbrk()、brk()没有额外的附加数据,也没有33页的映射。一次就是1页,底层维护一个位置,用位置的变化分配和回收内存。
mmap()和munmap()在内存分配/回收时提供了更多的选择,是一个可管理的内存分配方式。可以用第一个参数设定分配的首地址,也可以用第三个参数设定权限,第四个参数包括:
MAP_SHARED/MAP_PRIVATE 设定是否共享(只对映射文件有效)。
MAP_ANONYMOUS 设定映射物理内存
默认情况下,mmap()映射文件。
三、文件系统
系统调用:因为用户空间不能直接访问内核空间,想完成功能又必须得到内核的支持。因此,内核层提供了系统调用,做用户空间进入内核空间的桥梁。系统调用是 一系列的函数,包括各种系统的功能。以后我们接触的大多数都是系统调用。
文件操作:非常常用的函数,包括读写函数和 非读写函数。
在Linux系统中,几乎一切都是文件。目录、内存、各种硬件设备都可以看成文件。比如:/dev/tty 代表键盘和显示器。
echo hello 默认输出到显示器上
echo hello > a.txt 把输出改到a.txt中
echo hello > /dev/tty 把输出改到显示器中
cat /dev/tty 直接从键盘读数据
ctrl+C 退出
vi ../ 查看上层目录的内容
标C用FILE*(文件指针)代表一个打开的文件,UC用文件描述符代表一个打开的文件。文件描述符其实就是一个非负整数,文件描述符自身不存储任何文件信息,信息都存在 文件表中,文件描述符对应文件表。对应Linux来说,一个进程最多同时打开256个文件,描述符从0开始计算。0、1、2系统已经占用,程序员不能使用,代表标准输入、标准输出和标准错误。程序员的文件描述符从3开始。
文件读写函数:
open() - 打开一个文件,返回文件描述符
int open(char* filename,int flags,...)
参数:filename 是打开文件的路径(包括文件名)
flags 标识,主要有以下宏定义:
权限标识:O_RDONLY
O_WRONLY O_RDWR
权限标识必选其一
附加标识:O_APPEND 用追加的方式打开(从文件尾开始写,读文件一般不用)
创建标识:O_CREAT 存在就打开,不存在就新建
O_TRUNC 文件存在时清空所有数据(谨慎)
O_EXCL 不存在就新建,存在不打开,而是返回-1,代表出错。
第三个参数 ... 叫可变长参数,代表0-n个任意的参数,只有在新建文件时,才使用。传入新文件的权限。
注: 第三个参数是文件在硬盘上的权限(某些权限可能被系统屏蔽)。O_RDONLY等是文件描述符的权限。返回文件描述符,失败返回-1。多个选项 用 位或 | 连接。
- read()/write() - 读/写一个文件
int read(int fd,void* buf,size_t size)
int write(int fd,void* buf,size_t length)
参数:fd文件描述符,就是open()的返回值
buf是读/写的首地址,任意类型都可以
size是buf的大小(有可能读不满)
length是真实想要写入的字节数(满)
返回有三种:
正数 - 真实读到/写入的字节数
0 - 读到文件尾/什么都没写
-1 - 出现错误
注: read()返回0 通常用于循环读文件的退出。
vi编辑器用wq保存退出时,自动加一个结束符,可以被cat换行,但是用write()没有加。
- close() - 关闭文件
ioctl() - 是设备驱动程序中对设备的I/O通道进行管理的函数。
关于字符串的处理
C程序员定义字符串有三种:
“abc” 字面值,本身不是变量。
char buf[length]; 字符数组
char* st ; 字符指针
字符串以’\0’做结尾。数组可以看成常指针(不能改地址,只能初始化),某些时候和指针有区别(比如sizeof)。
具体操作见 string.c
time a.out可以查看a.out的运行时间
所有的标C函数都在用户层定义了输入/输出缓冲区,作用就是累计到一定量以后再进入内核读/写一次。UC函数都没有定义缓冲区,但可以由程序员自定义缓冲区提升效率。
文件读写的位置用偏移量记录,在文件表中存储了偏移量。函数lseek()可以随意移动偏移量。
int lseek(int fd,int offset,int whence)
参数:fd 就是文件描述符
offset 是偏移量
whence是偏移的开始位置
返回当前位置到文件头的偏移量,失败返回-1.
注:
whence + offset可以确定位置,whence包括:
SEEK_SET - 从头开始
SEEK_CUR - 从当前位置开始
SEEK_END - 从结尾开始
lseek()的返回值可以计算文件的大小。
文件操作的非读写函数
(1). dup() dup2() - 复制文件描述符,但不复制文件表
dup()由系统选定新描述符的值,dup2()由程序员指定新描述符的值,如果指定值已经被使用,先关闭再使用(不安全的隐患)。
内存用虚拟内存地址管理,硬盘上的文件/目录如何管理,用inode管理。i节点可以认为是文件在硬盘上的地址。ls -i可以查看文件/目录的i节点。
(2). 函数fcntl()实现很多的功能,由参数cmd决定,常见的应用:
可以复制文件描述符
可以设置/获取描述符的状态
可以设置文件锁
int fcntl(int fd,int cmd,...)
参数: fd 文件描述符
cmd 命令,可以设置fcntl()完成什么功能,常用功能:
F_DUPFD(long) - 复制文件描述符,传入第三个参数做新描述符的值。和dup2()的区别在于不会强行关闭已使用的描述符,而是寻找大于等于参数的最小未使用的值。
F_SETFL(long)/F_GETFL(void) - 设置/获取描述符的状态,比如权限。其中,设置时,只对O_APPEND有效,权限和创建标识都无效。获取时,只能取权限和O_APPEND,创建标识取不到。
F_SETLK/F_SETLKW/F_GETLK - 文件锁的操作
位与 运算用于 取某一位或者取某几位,比如取后8位:
int a;
取后8位: a & 0xFF
文件锁
当多个进程同时写一个文件时,有可能引发数据混乱,这个问题需要解决。解决方案包括:进程之间的同步 或 文件锁。文件锁就是当一个进程读写文件时,对其他进程进行读写的限制。结论:一个进程读,允许其他进程读,但不允许其他进程写。一个进程写,其他进程不能读也不能写。文件锁是一个读写锁,包括读锁和写锁。
读锁是一个共享锁,允许其他进程读(共享),不允许其他进程写(锁)。如果进程是读文件,就应该上读锁。
写锁是一个互斥锁,不允许其他进程读/写(互斥锁)。如果进程是写文件,就应该上写锁。
fcntl(fd,cmd,...)
当cmd为F_SETLK/F_SETLKW时,可以对文件上锁。
当使用文件锁时,第三个参数就是结构体flock指针。
struct flock{
short l_type;//锁的类型
short l_whence;//锁定起始点的参考位置
off_t l_start;//针对参考位置的偏移量
off_t l_len;//锁定的区间长度
pid_t l_pid;//只对F_GETLK有效,一般给-1
};
锁的类型包括:F_RDLCK(读锁) 、F_WRLCK(写锁)、F_UNLCK(释放锁)
l_whence和l_start联合决定了锁定的起始点。比如:l_whence选SEEK_SET,l_start为10,就是从头开始偏移10个字节以后开始锁。
进程结束自动释放文件锁,但最好还是程序员自己释放。
文件锁只是内存中的一个标识,不会真正锁定文件。fcntl(F_SETLK)不能锁定read()/write(),只能锁定其他进程的加锁行为fcntl(F_SETLK)。文件锁的正确用法是:在调用read()函数之前用fcntl()加读锁,能加上再读,读完以后释放读锁;在调用write()之前用fcntl()加写锁,能加上再写,写完以后释放写锁。
fcntl(fd,F_SETLK,&读锁);
read(fd,...);
fcntl(fd,F_SETLK,&释放锁);
或:
fcntl(fd,F_SETLK,&写锁);
write(fd,...);
fcntl(fd,F_SETLK,&释放锁);
但不管怎么加锁,类似vi的编辑器是无法锁定。
F_SETLK当锁加不上时,直接返回-1,而F_SETLKW当锁加不上时,会继续等待,等到能加上为止。
F_GETLK不是获得当前的锁,而是测试一下某个锁能不能加上,并不真正的加锁。(了解)
C语言中,参数可以有三种:
传入型参数 - 给函数传值,比如: add(int,int)
传出型参数 - 带回函数的结果,一般是指针类型
传入传出型参数 - 先传入一个值,再带出一个值
函数的返回值,可以用return直接返回,也可以用传出型参数返回。
stat()就是用传出型参数返回文件的信息。stat()可以取得文件的以下信息:
ls -il 的所有信息,其中最常用的是st_size。
st_mode需要拆分,文件类型和权限。
access()可以判定当前用户对文件的权限和文件是否存在。
int access(char* fname,int mode)
参数fname就是带路径的文件名
mode 就是判断什么,包括:
R_OK - 读权限
W_OK - 写权限
X_OK - 执行权限
F_OK - 文件是否存在
返回0代表有权限或者文件存在。
其他函数:
chmod() - 修改文件的权限
chmod(“a.txt”,0666)
truncate()/ftruncate() - 指定文件的大小
truncate(“a.txt”,100)
remove() - 删除文件/空目录
rename() - 文件改名
access() - 获取文件的权限/判断文件是否存在
umask() - 修改创建文件时,系统默认的权限屏蔽字。默认屏蔽其他用户的写权限 - 0002。umask()可以修改默认的权限屏蔽字(只针对新建文件)。
umask(mode_t)传入新的权限屏蔽字,返回之前的权限屏蔽字,用于处理之后的恢复。
mmap() 可以映射物理内存,但也可以映射文件,默认情况下 映射文件,映射物理内存需要加MAP_ANONYMOUS标识。
目录相关函数:
mkdir() - 新建一个目录
rmdir() - 删除一个空目录
chdir() - 切换当前目录 (cd)
getcwd() - 取当前目录(返回绝对路径) 双返回
char* s = getcwd(0,0);
读目录的函数:
- opendir() - 打开一个目录,返回目录流(指针)
- readdir() - 读目录的一个子项(子目录/子文件) 效果相当于 ls 目录
- closedir() - 关闭目录流(不写也可以)
使用递归的必要条件:
- 使用递归以后,问题简化而不是复杂
- 递归必须有 退出条件
- 使用递归要注意效率问题。
四、进程管理
程序就是代码编译连接的成品(a.out),程序是硬盘上的文件。进程就是运行在内存中的程序,一个程序可以启动多次,得到多个进程。CPU(中央处理器)只能直接操作内存,不能直接操作硬盘的。硬盘上的程序要想运行,先加载到内存中去,就变成了进程。有些时候也把进程叫程序。主流的操作系统都是多进程的,每个进程内部可以用多线程实现功能的并行(同时运行)。
进程相关命令(Unix版):
ps : 只能看到当前终端启动的程序
ps -aux : Linux专用查看所有进程的命令,Unix不直接支持。
ps -ef : 通用版
kill -9 进程ID : 杀进程(发信号)
常见的管道用法:
管道的作用就是用前面的输出作为后面的输入
ps -ef | wc - 统计行数、字节数等
ls -al | more - 分页显示(空格 回车 q)
Unix/Linux系统由 内核和SHELL,SHELL主要有: sh/bash(sh的升级版)/csh
whereis XXX 可以查看文件名XXX在哪里
父进程和子进程
如果a进程启动了b进程,a就是父进程,b就是子进程。Unix/Linux系统的启动次序是:系统启动0进程,0进程启动进程1/进程1和进程2,其他进程都由进程1/进程1和进程2启动。
进程的状态
每个进程都有自己的状态,主要包括:
S - 休眠状态,大多数进程处于休眠状态
s - 有子进程
R - 正在运行
Z - 僵尸进程(已经结束,但资源没有回收)
T - 暂停或被追踪
每个进程用进程ID(PID)做唯一标识,进程PID是系统管理。函数getpid()可以取得进程的PID。如果进程结束,PID是可以重复使用,但要延迟重用。PID唯一标识一个进程。
getpid() - 取当前进程的PID
getppid() - 取父进程的PID
getuid() - 取当前用户的ID。
创建子进程
fork() - 非常复杂的简单函数,通过复制父进程创建子进程。
vfork()+execl() - 不复制任何东西,创建一个全新的子进程。
进程PID用pid_t类型,是一个非负整数。pid_t fork() , 返回子进程的PID或0,出错 -1。
fork()是通过复制父进程的内存空间创建的子进程,除了代码区父子进程共享(只读),其他内存区域子进程都要复制。
fork()创建子进程之后,父子进程同时运行,但谁先运行不确定,谁先结束也不确定。
fork()在复制父进程的内存空间时,如果遇到文件描述符,复制描述符但不复制文件表。
fork()在复制父进程的内存空间时,也会复制输出/输入缓冲区。
fork()函数调用一次,返回两次。父进程返回一次,子进程也会返回一次。父进程返回子进程的PID,子进程返回0。
关于父进程的运行和资源回收:
父进程启动子进程后,父子进程同时运行。如果子进程先结束,会给父进程发信号,父进程负责回收子进程的资源。
父进程启动子进程后,父子进程同时运行。如果父进程先结束,子进程变成孤儿进程,认进程1(init)做新的父进程,init进程也叫 孤儿院。
父进程启动子进程后,父子进程同时运行。如果子进程没有给父进程发信号就结束,或者父进程没有及时处理信号,此时子进程就变成僵尸进程。
fork()之前的代码父进程执行一次,fork()之后的代码父子进程分别执行一次,fork()将返回两次。
刷新输出缓冲区的条件:
遇到换行 \n
缓冲区满了
程序结束了
fflush()函数人工刷新
进程结束的方式分为正常结束和非正常结束。
正常结束包括:
主函数中执行了return
执行exit()
_Exit()和_exit()
所有线程都结束
非正常结束:
信号打断进程(ctrl+c、kill -9)
最后一个线程被取消
exit() 与 _Exit()/_exit()的区别:
_Exit()/_exit()基本无区别,都是立即退出进程。
exit()不是立即退出,甚至可以先执行在 atexit()中注册函数后再退出。
函数wait()/waitpid()可以让父进程等待子进程结束,并取得子进程的退出状态和退出码(return/exit(值))。
pid_t wait(int* status)
wait() 函数 让父进程等待任意一个子进程的结束,并返回结束子进程的PID,把结束子进程的退出状态和退出码存入status中。如果没有子进程结束,会阻塞父进程,直到有子进程结束为止。包括僵尸子进程,因此wait()也叫殓尸工。
宏函数WIFEXITED(status)判断是否正常结束,而WEXITSTATUS(status) 可以获得退出码。
waitpid() 可以设置等待的方式和等待的子进程。
pid_t waitpid(pid_t pid,
int* status,
int options)
参数:pid可以指定等待哪个/哪些子进程
status用法和wait一样
options可以设置非阻塞的等待(不等待) options为0,没有子进程结束继续等待为WNOHANG,没有子进程结束不等待,直接返回0.
pid的值:
-1 : 等待任意子进程,和wait()一样
>0 : 等待子进程的ID=pid(特指)
0 : 等待本组子进程(与父进程相同进程组)
<-1: 等待进程组为|pid|的所有子进程
注:-1 和 >0 常用,后面两个了解即可。
返回有三种可能:
结束子进程的pid
-1 代表出错
0 只有在options为WNOHANG时可能返回,代表没有子进程结束,也没有出错。
fork() 父子进程是使用相同的代码区,如果 需要父子进程代码区不同的话,可以使用 vfork()+execl()。vfork() 创建新的子进程,execl()负责提供子进程的代码和数据(程序)。execl()函数是用新的程序替换原有的程序。vfork() 从语法上和fork()完全一样,区别在于vfork()不复制任何父进程的资源。vfork()会抢父进程的资源,导致父进程阻塞。父进程解除阻塞的条件:
子进程结束时,归还父进程的资源(无并行)。
子进程调用exec系列函数(execl等),也归还父进程资源。
注意:
vfork()确保子进程先运行(父进程没资源),调用execl()之后父子进程同时运行。
vfork()创建的子进程必须用exit()退出。
- execl()可以用一个新程序替换旧程序,但不新建任何的进程。如果新的程序正常启动,旧程序不再继续运行;如果新的程序启动失败,旧程序继续运行。execl(程序所在的路径,命令,选项,命令参数,NULL), 启动失败返回-1.
五、信号处理
信号(signal)
信号是Unix/Linux系统中 软件中断的最常用方式
中断:
中断就是中止当前正在执行的代码,转而执行其他代码。
中断分为软件中断和硬件中断。
中断就是中止当前正在执行的代码,转而执行其他代码。
常见的信号:
ctrl+c
段错误
总线错误
整数除以0
kill -9 发信号9
子进程结束,给父进程发信号
信号本质就是一个非负整数,Unix和Linux在信号上有区别,Unix是48个,Linux是64个,但中间不保证连续。
每个信号都有一个 宏名称,编程时尽量使用宏名称而不是信号的值。不同的系统中,同一个宏名称对应的值可能不同。宏名称 以 SIG开头。比如:SIGINT 就是 信号2的宏名称
查看信号都有哪些,可以使用kill命令。发送信号也可以使用kill命令。
kill -l : 查看所有信号
kill -整数 : 发送信号
信号分为可靠信号和不可靠信号,1-31 都是 不可靠信号,34-64都是可靠信号。不可靠信号 不支持排队,因此如果有多个 相同的不可靠信号同时到来时,可能出现信号丢失。可靠信号支持排队,因此不会丢失。
信号的处理方式:
(1) 默认处理 - 系统对每个信号都有默认处理方式,默认处理大多数都是 退出进程。
(2) 忽略信号 - 不做任何的处理,就像没有信号一样。
(3) 自定义信号处理函数 - 信号的处理方式改为执行我们自己定义的函数。
注:
信号9 不能忽略,也不能自定义处理函数。
当前用户只能给当前用户的进程发信号,不能给其他用户的进程发信号。 root可以给所有进程发信号。
信号0 没有特殊的意义,用于 测试是否有发信号的权限。 kill -0 3333(测试对3333进程是否有发送信号的权限)
Unix系统提供了设置信号的处理方式的函数,signal()、sigaction()。
(void(*f)(int)) signal(int signum,void(*f)(int))
参数 signum就是被设置处理方式的信号
第二个参数是函数指针,支持三种值:
SIG_IGN - 代表忽略该信号
SIG_DFL - 代表信号到来执行默认处理方式
自定义的函数 - 代表信号到来执行自定义函数
返回之前的信号处理方式,如果出错返回 SIG_ERR.
自定义信号处理方式的步骤:
(1) 写一个处理函数,格式 void fa(int){ }
(2) 调用signal(int signum,fa)注册处理函数。
如果父进程改变了信号的处理方式,子进程如何?
fork()创建的子进程,与父进程的处理方式一致。
vfork()+execl()创建的子进程,父进程忽略的,子进程也忽略;父进程默认的,子进程也默认;父进程自定义处理函数,子进程改为默认。
killall可以删除所有同名的进程,比如:killall a.out 就会删除所有的a.out进程
信号的发送:
(1) 用键盘发送信号(部分)
ctrl+c -> 信号2
ctrl+\ -> 信号3
ctrl+z -> 信号20
(2) 硬件故障/或者程序出错(部分)
段错误、总线错误、整数除0
(3) kill命令发送信号(全部)
kill -信号 进程PID
(4) 信号发送函数(全部)
kill()、raise()、alarm()、sigqueue()等
int kill(pid_t pid,int signum)
参数 pid 就是发送哪个/哪些进程,使用方式和waitpid(pid)一样。
signum就是发送哪个信号
成功返回0,失败返回-1.
发送信号时,一般pid 为正数,也就是发给特定进程。
alarm()函数 - 不是真正意义的信号发送函数,而是过一段时间(秒数)发送特定的信号。
sleep() - 让程序休眠一段时间(秒数),但可能被非 忽略的信号打断。
usleep() - 让程序休眠一段时间(微秒).
信号集:
多个信号可以存入信号集,类型sigset_t,可以看成一个超大型整数。 long long int - C语言的64位整数。
信号集的函数:
(1) 增加信号和删除信号(分单独和全部)
(2) 查询信号
sigaddset() - 增加一个信号(二进制位 置1)
sigdelset() - 删除一个信号(二进制位 置0)
sigemptyset() - 全部删除信号
sigfillset() - 填满全部信号
sigismember() - 查询有没有某个信号
信号屏蔽:信号不确定什么时间会来,因此有可能在非常重要的场合(执行关键代码)信号到来,此时可能产生重大的错误。程序员无法阻止信号的到来,但是可以屏蔽信号,就是信号可以到来但暂时不做处理,等关键代码执行完毕,解除信号屏蔽后再做处理。信号9 屏蔽无效。
信号屏蔽/解除函数 sigprocmask()
int sigprocmask(int how,sigset_t* set,sigset_t* old)
参数:how就是信号屏蔽的方式,包括:
SIG_BLOCK - 相当于旧的屏蔽+新的屏蔽
A B C + C D E -> A B C D E
SIG_UNBLOCK - 相当于旧的屏蔽 - 新的屏蔽
A B C - C D E -> A B
SIG_SETMASK - 就是无视旧的,直接替换成新的屏蔽。
set 就是新的权限屏蔽字
old是一个传出参数,可以传出旧的权限屏蔽字,用于恢复之前的屏蔽
注: 信号屏蔽之后一定要解除屏蔽。
一般情况下,how都采用SIG_SETMASK。
函数sigpending()可以判断在信号屏蔽期间,有没有信号来过。功能就是把信号屏蔽期间来过的信号放入信号集。
sigaction()也是一个信号处理方式的注册函数,是signal()的增强版,sigaction()可以拿到更多的信号相关信息,甚至可以在发送信号的时候附带其他的数据。sigaction()中,信号的处理函数支持两种格式: signal()的格式和更复杂的格式。
信号的应用之计时器。
每个进程在Linux中都有三种计时器,真实计时器、虚拟计时器和实用计时器。其中真实计时器是产生SIGALRM工作。计时器可以用setitimer()进行设置。
int setitimer(int which, const struct itimerval *value, struct itimer val *ovalue);
参数which选择哪种计时器,一般都是真实计时器。
struct itimerval 设置计时器的开始时间和间隔时间。
六、进程通信
1.进程见通信
进程间通信 - IPC:
Unix/Linux系统基于多进程,进程和进程之间经常做数据的交互,这种技术叫进程间通信。
常见的IPC:
1 文件
2 信号
3 管道
4 共享内存
5 消息队列
6 信号量集
7 网络编程(socket)
…
XSI IPC 之 共享内存、消息队列。XSI IPC 包括共享内存、消息队列和信号量集,遵循相同的规范。
标准(规范) 、产品 和 项目
标准是行业准则,任何相关软件都必须遵守。标准是 行业共同协商的成果。做标准的公司最幸福的。
产品就是遵循标准的软件,产品更注重质量,不是为个别客户服务的。比较轻松,不用特别赶时间
项目是针对 特定客户的定制,客户的影响力非常大,时间一般比较紧张。比较累,而且需要年轻化
XSI IPC的通用规范:(三种都可以用)
(1) 所有的IPC结构都有一个内部的ID做唯一标识
(2) 内部ID的获取 需要借助 外部的key,类型key_t。
(3) key的获取有三种方式:
a 使用宏 IPC_PRIVATE做key,但这种方式外部无法获取,因此基本不用。
b 使用ftok()提供一个key。
c 在头文件中统一定义所有的key。
(4) 用key获取内部id的函数都是 xxxget(),比如: shmget() 、msgget()
(5) 每种IPC结构都提供了一个 xxxctl()函数,这个函数的功能至少包括:
查询、修改和删除。
其中有一个cmd参数,值:
IPC_STAT - 查询
IPC_SET - 修改
IPC_RMID - 按ID删除IPC结构
(6) 所有IPC结构都是内核管理,不使用时需要手工删除。
(7) IPC结构的相关命令:
ipcs 查询当前的IPC结构
ipcrm 删除当前的IPC结构(用id删除)
选项: -a 所有IPC结构
-m 共享内存
-q 消息队列(更常用)
-s 信号量集
2.管道
其中,管道是最古老的的IPC之一,目前较少使用。共享内存、消息队列和信号量集 遵循相同的规范,因此编码上有很多的共同点,并且这三个统称为XSI IPC。网络编程以前用于IPC,现在更多的用于网络。
管道(pipe) - 就是用管道文件做交互媒介的IPC。管道文件是一种特殊的文件,ls时 文件类型是p。
mkfifo命令/函数 管道文件名 就可以创建管道文件。touch命令和open() 都无法创建管道文件。
管道文件只是交互的媒介,不存储任何的数据;只有在有读进程 有写进程时 才能畅通,否则阻塞。
管道有两种用法: 有名管道和无名管道。
有名管道可以用于所有进程之间的交互,而无名管道只能用于fork()创建的父子进程之间的交互。
有名管道就是由程序员创建管道文件进行IPC。无名管道就是系统创建和维护管道文件进行IPC。
有名管道的用法:
(1) 用mkfifo命令/函数 创建管道文件。
(2) 像读写普通文件一样操作管道文件。
(3) 如果不再使用管道文件,可以删除。
管道实际上是一种固定大小的缓冲区,管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。它类似于通信中半双工信道的进程通信机制,一个管道可以实现双向 的数据传输,而同一个时刻只能最多有一个方向的传输,不能两个方向同时进行。管道的容 量大小通常为内存上的一页,它的大小并不是受磁盘容量大小的限制。当管道满时,进程在写管道会被阻塞,而当管道空时,进程读管道会被阻塞。
3.共享内存
以 一块共享的物理内存做媒介。通常情况下,两个进程无法直接映射相同的内存。共享内存是效率最高的IPC。.
共享内存的实现:
(1) 内核先拿出一块物理内存,内核 负责管理。
(2) 允许所有进程对这块内存进行映射。
(3) 这样两个不同的进程 就可以 映射到 相同的物理内存上,从而实现信息的交互。
共享内存编程步骤:
(1) 获取 key,方式ftok()或头文件定义。
key_t ftok(char* pathname,int projectid)
参数:pathname是一个真实存在的可访问的路径
projectid是项目编号,低8位有效(1-255)
返回key。
ftok()如果给定路径有效,不会出错。会按照路径和项目ID生成一个key。相同的路径+相同的项目ID生成 相同的key。
(2) 使用shmget()函数创建/获取内部ID。
int shmget(key_t key,size_t size,int flag)
参数:key 就是第一步返回值,外部的key
size就是共享内存的大小
flag在获取时用0,在新建时用:
IPC_CREAT|0666 (权限)
成功返回共享内存的ID,失败返回-1.
(3) 使用shmat()挂接共享内存(映射)。
void* shmat(int shmid,0,0) 可以挂接
(4) 可以像正常操作一样使用共享内存。
(5) 使用shmdt() 脱接共享内存(解除映射)。
int shmdt(void* addr)
adrr是shmat()的返回值,首地址(虚拟)。
(6) 如果确定已经不再使用,可以使用shmctl()删除共享内存。
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
参数: shmid 共享内存标识符
cmd
IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中
IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内
IPC_RMID:删除这片共享内存
buf 共享内存管理结构体。具体说明参见共享内存内核结构定义部分
shmctl() 可以查询、修改和删除共享内存。 查询时,会把共享内存的信息放入第三个参数 修改时,只有用户id、组id和权限可以修改。 删除时,第三个参数给0 即可。 删除共享内存时,挂接数必须为0才能真正删除,否则删除只是做一个删除标记,等挂接数为0时才真正删除。 第三个参数是结构体指针:struct shmid_ds
4.消息队列
共享内存虽然速度最快,但当多个进程同时写数据时,会发生互相覆盖,导致数据混乱。消息队列就可以解决多个进程同时写数据的问题。
消息队列就是 存放消息的队列。队列是线性的数据结构,先入先出(FIFO)。一般情况下,队列有满有空。数据先封入消息中,然后再把消息存入队列。
消息队列的编程步骤:
(1) 得到外部的key,函数ftok()。
(2) 用key创建/获取一个队列(消息队列),函数msgget()。
msgget() msgctl()与共享内存的函数相似。
int msgsnd(int msgid,void* msgp,size_t size,int flag)
参数 msgid就是消息队列的ID
msgp就是消息的首地址,其中消息分为有类型消息和无类型消息,更规范的是 有类型消息。有类型消息就是一个结构:
struct Msg{ //结构的命令可以自定义
long mtype; //消息的类型,必须大于0
... //数据区域,可以任意写
};
将来在接收消息时,可以按照类型有选择的接收消息。
size参数是 数据区域的大小,不包括mtype(有些时候包括了也行)。
flag 就是选项,可以是0 代表阻塞(队列满了等待),也可以是IPC_NOWAIT 代表非阻塞(队列满了直接返回错误)。
成功返回0,失败返回-1.
(3) 把数据/消息 存入队列 或 从队列中取出。函数 msgsnd()存入/msgrcv()取出
int msgrcv(int msgid,void* msgp,size_t size,long mtype,int flag)
前三个参数和msgsnd()一样。
参数flag和msgsnd()也一样。
参数mtype可以让接收者有选择的接收消息,值可能是:
0 - 接收任意类型的消息(第一个,先入先出)
>0 - 接收 类型为mtype的特定消息
<0 - 接收类型 小于等于 mtype绝对值的消息,从小到大接收。
成功返回接收到的数据大小,失败返回-1.
(4) 如果不再使用消息队列,可以msgctl()删除。
5.信号量集
信号量是一个计数器,用于控制访问共享资源的最大并行进程数。信号量集就是信号量的数组。
信号量的工作方式:设定一个初始计数,每来一个进程计数-1,每完成一个进程计数+1,计数到0不允许进程访问,直到大于0为止。
信号量集的编程步骤:
(1) 获得key。
(2) 用semget()获取信号量集的ID。
(3) 用semctl()设置信号量的初始计数。
semctl()设置初始值的代码:
semctl(semid,index,SETVAL,count)
其中,semid是信号量集的ID,index是信号量在信号量集中的下标,SETVAL是宏,count就是该信号量的初始计数。
(4) 用semop()进行加1或减1的操作。
int semop(int semid, struct sembuf semoparray[],size_t nops);
参数semoparray是一个指针,它指向一个信号量操作数组,信号量操作由sembuf结构表示:
struct sembuf{
unsigned short sem_num;//操作信号量的下标
short sem_op; //对信号量操作方式。 -1 和 1
short sem_flg; //0 等待 IPC_NOWAIT不等
};
(5) 如果不再使用,用semctl()删除。
七、网络通信
网络编程(非常重要),网络常识 - 比如: 协议、IP地址、端口等。
网络编程的步骤和函数
基于TCP/UDP的开发
OSI 七层模型: 物理层、数据链路层、网络层、传输层、会话层、表现层、应用层
协议就是 计算机信息交互时的规范。常见的协议: http协议 - 超文本传输协议
ftp协议 - 文件传输协议
tcp协议 - 传输控制协议 (传输层)
udp协议 - 用户数据报协议(传输层)
ip 协议 - 网络层协议
…
协议簇就是多个协议的集合,协议簇一般都是以核心协议命名。也有非官方的写成协议族。
IP地址就是网络中计算机的唯一标识,本质是一个整数。IP地址早期都是32位的,叫IPV4。后来推出了IPV6(128位)。主流还是IPV4。IP地址有两种描述方式: 点分十进制和十六进制。
点分十进制就是每8位做一个整数(0-255),分4段,中间用点. 隔开。
十六进制就是把32位二进制直接写成 8位十六进制。
计算机更倾向十六进制,而人更习惯点分十进制。
IP地址其实绑定的是网卡,每个网卡在出厂时都有一个唯一的物理地址(MAC地址),IP地址其实是找到网卡的物理地址,找到网卡从而找到计算机。
IP地址分为A/B/C/D 四类。系统预留了127.0.0.1,做本机的IP地址。
关于网络的一些基本命令:
ipconfig - windows dos命令,查看IP地址
ifconfig - Unix/Linux 查看IP地址。
ping IP地址 - 测试IP地址是否可以访问。
子网掩码 就是用来 判断IP是否同一网段。
IP地址:166.111.160.1
166.111.161.45
子网掩码:255.255.254.0
做位与运算:
166.111.160.1 - 所有偶数 最后一位0,奇数1
255.255.254.0
\-------------------
166.111.160.0
166.111.161.45
255.255.254.0
\-------------------
166.111.160.0 ---> 结果相同,同一网段
IP地址可以让我们找到计算机,但没有找到对应的进程。在网络中,端口代表计算机内部的一个进程。有IP地址+端口号 就可以网络通信。ip地址+端口号的网络编程就是socket编程。
端口号也是一个整数short,0到65535。其中:0-1023不要用,系统占用其中很多端口。48000以后也不要用,不稳定,系统随时可能征用。有些软件会强占一些端口,这些端口不要使用。Oracle数据库 占1521端口、8080端口等。常用端口:
Http端口 80
ftp端口 21
telnet端口 23
字节顺序: 整数是4个字节,有些计算机从低位字节到高位字节存储,有些机器从高位字节到低位字节存储。本机的字节顺序无法确定,但网络的字节顺序是固定的。编程用网络字节顺序传输,到本地以后再网络转本地格式。
Unix/Linux网络编程的实现,有固定套路并且有一些不方便的函数、结构。socket编程 (插座、套接字)。socket通信包括 一对一 和一对多。先研究一对一(一对多的模式也一样)。socket编程 早期用来做进程间通信(IPC),现在主体是网络,编程的代码差不多。
本地通信(IPC)
服务器端的编程步骤:
(1) 创建一个socket,使用socket()
int socket(int domain,int type,int protocol)
参数:domain叫域,用于选择协议簇
AF_UNIX/AF_LOCAL/AF_FILE : 本地通信IPC
AF_INET : 网络通信
AF_INET6: IPV6的网络通信
其中,AF换成PF效果一样。
type 选择通信的类型(选协议)
SOCK_STREAM : 数据流(TCP协议)
SOCK_DGRAM : 数据报(UDP协议)
protocol 本来应该选择协议,但实际上没什么用,协议已经被前2个参数决定,给0即可。
成功返回 socket描述符,类似文件描述符。失败返回-1.
注:读写函数 可以操作socket描述符。
(2)准备通信地址(IPC是文件,网络是IP/端口),系统提供了三种通信地址,就是三个结构体。
(1) struct sockaddr本身不存数据,做函数的参数。
(2) struct sockaddr_un 存本地通信的通信地址
(3) struct sockaddr_in 存网络通信的通信地址
#include <sys/un.h>(本地)
struct sockaddr_un{
int sun_family; //协议簇,与socket()一致
char sun_path[];//socket文件的路径
};
#include <netinet/in.h>(网络)
struct sockaddr_in{
int sin_family; //协议簇,与socket()一致
short sin_port; //端口号
struct in_addr sin_addr; // IP地址
};
(3) 绑定socket描述符和通信地址
bind(int sockfd,struct sockaddr* addr,int length)
length是通信地址的sizeof
(4) 通信(read()、write())
(5) 关闭socket描述符(close())
客户端的编程步骤和服务器端编程步骤一样,除了第三步把bind换成connect(),但函数的参数不用改变。bind() 是服务器绑定通信地址,开放端口。connect()是客户端连接服务器,通信地址要使用服务器的。 本地通信 媒介是 socket文件,类型s。
在使用网络编程时,IP地址和端口号都需要做一些处理。IP地址需要做点分十进制和十六进制转换,使用函数 inet_addr();端口号需要本机格式和网络格式之间的转换,使用函数htons()。
有两种socket描述符,其中一种负责 等待客户端的连接,当有客户端连接时,启动一个新的描述符负责信息交互。
TCP协议是一个基于连接(有连接)的协议,全程保持客户端和服务器的连接。会重发一切的错误数据,因此TCP可以保证数据的完整和有效。缺点就是当客户端超级多的时候,效率非常低。
UDP协议是一个不基于连接(无连接)的协议,发送数据时连接一下,发送完了就断开,而且不考虑是否接收到了。UDP效率比TCP高,UDP不保证数据的有效和完整。QQ/MSN 都是采用UDP协议。
TCP一对多的编程步骤:
服务器端:
1 socket(),得到第一类的socket描述符。 2 准备通信地址 struct sockaddr_in 3 绑定 bind(),开发断开。 4 监听客户端的连接,函数listen()。
int listen(int sockfd,int backlog)
设置当多个客户端同时 连接时,需要把多余的客户端存入队列,backlog就是队列的最大长度。
5 等待客户端的连接,函数accept(),返回新的socket描述符,用于信息交互。(无客户端连接会阻塞).
int accept(int sockfd,struct sockaddr* addr, socklen_t* len)
参数 sockfd就是socket描述符,第一步的返回
addr是一个结构体指针,存客户端的通信地址
len是传入传出参数,先传入addr的长度,再传出接收到的客户端通信地址的真实长度。
返回 新的socket描述符,失败返回 -1.
6 用第五步返回描述符进行读写操作。 7 close()关闭两个描述符。
客户端:
1 socket(),得到第一类的socket描述符。 2 准备通信地址 struct sockaddr_in 3 连接 connect()。
4 描述符进行读写操作read() write()
5 关闭close()描述符。
UDP编程: TCP和UDP的区分主要在于socket()第二个参数,如果是SOCK_STREAM就是TCP,SOCK_DGRAM就是UDP。UDP分 发送方和接收方。UDP发送数据很少使用write(),使用sendto()。接收数据可以使用两个函数:read()/recvfrom()。区别就是read()函数不知道数据的来源,而recvfrom()可以获取数据的发送者信息。
ssize_t sendto(int sockfd,void* data,size_t length,int flags, struct sockaddr* addr,socklen_t size)
参数:前三个 参数与write()一样,flags一般给0即可,addr就是第二步的通信地址的指针,size就是sizeof(addr)。
成功返回发送的字节数,失败返回-1.
注:sendto()好像write()和connect()的合体。
ssize_t recvfrom(int sockfd,void* data,size_t length,int flags, struct sockaddr* addr,socklen_t* size)
参数:前三个和read()一样,flags给0即可,addr是用于存储发送者通信地址的指针,size是一个传入传出参数,把addr的sizeof传入,再传出真正获取到的通信地址的大小。(后两个参数和accept()一样)
成功返回接收到的字节数,失败返回 -1.
注:recvfrom像 read() 和 accept()的合体。
网络编程总结:
TCP的编程步骤:
Server端:
1 socket()
2 准备通信地址 struct sockaddr_in
3 绑定 bind()
4 listen()
5 accept(),返回一个用于交互的新的描述符
6 读写 read() write()
7 关闭close()
Client端:
1 socket()
2 准备通信地址 struct sockaddr_in
3 连接 connect()
4 读写 read() write()
5 关闭close()
UDP的编程步骤:
Server端:
1 socket()
2 准备通信地址,struct sockaddr_in
3 绑定 bind()
4 发送或接收 sendto() recvfrom() read()
5 关闭close()
Client端:
1 socket()
2 准备通信地址,struct sockaddr_in
3 发送或接收 sendto() recvfrom() read()
4 关闭close()
八、线程管理
线程(thread) - 做应用,有网络必有线程。主流的操作系统都是支持多进程的,每个进程的内部可以启动多线程完成代码的并行;每个线程的内部可以无限启动多线程。线程是轻量级的代码并行,不需要额外创建过多的内存空间,而是共享所在进程的内存空间。线程只需要额外建一个独立的栈即可。多线程之间互相独立,又互相影响。 多线程可以大幅提升代码的效率。程序的运行必须拥有CPU和内存,内存可分, CPU不可分,如何实现并行。大多数的操作系统都是采用CPU时间片实现CPU的在多线程之间的轮换。CPU时间片是极短的一段CPU的执行时间,拥有CPU时间片的线程有机会运行。比如:人的感官是需要时间的,比如视觉0.1秒。就是100毫秒。假定CPU时间片是1毫秒,有4个线程。每个线程先分一个时间片,也就是1毫秒的CPU运行时间。每个线程只能运行1毫秒,时间片的运行时间到了以后就只能看其他线程运行,直到所有线程的时间片都运行完毕,再重新分配。针对时间点的并行是不存在的,针对时间段的并行就是我们通常说的代码并行。每个进程都有一个主线程,就是main()函数,主线程结束,进程也结束,同时导致所有线程都结束。
线程的编程:
Unix/Linux的线程相关函数都在pthread.h中,代码都在libpthread.so中。线程相关的函数/结构都以pthread_ 开头。比如创建线程函数:
(1). pthread_create();
int pthread_create( pthread_t* id,
pthread_attr_t* attr,
void* (*fa)(void*),
void* arg )
pthread_create()是一个四针函数,参数id就是用于存储线程ID的;
attr是线程的属性,一般给0即可(默认属性);
fa是一个函数指针,写线程执行的代码;
arg是传给fa的参数。fa+arg指定了线程要执行的代码。
返回值: 成功返回0,失败返回错误码,想看错误信息需要用strerror()做转换。
每个线程启动以后,只能执行一个函数,主线程执行的是main(),其他线程执行自定义的一个函数。这个函数以并行的方式运行。线程之间的代码乱序执行,每个线程的内部代码都是顺序执行。每个线程都会返回自己的错误码,而不是使用errno。
(2). pthread_join();
pthread_join()函数可以让一个线程等待另外一个线程的结束,并取得线程的返回值。如果在线程a中调用了pthread_join(b,0),线程a就会等待线程b的结束,等线程b结束以后a才能继续运行。线程传参时,一定要注意保证地址的有效性,尤其是堆内存。支持直接传递int。
函数的返回
(1). 能返回局部变量,但不能返回指向局部变量的指针。
(2). static的局部变量的地址可以返回(全局区)。
(3). 数组理论上可以做返回值,但返回值类型不能写数组。最好用指针。
int[] get() 错.
void fa(int* pi){ *pi = 200;}
int main(){
int x;
fa(&x); -> pi = &x ->*pi = x = 200;
}
void fa(int** pi){ *pi = 地址1;}
int main(){
int* px;
fa(&px); ->pi = &px ->*pi = px = 地址1;
}
线程的状态:
线程应该处于以下两种状态:
分离状态:就是线程一旦结束,不用管其他线程,直接回收资源。函数pthread_detach()设置线程分离状态。
join状态:如果线程用pthread_join(),就处于join()状态,就是线程结束时暂不回收资源,到pthread_join()函数结束时再回收资源。
注:没有分离也没有join()的线程资源回收是没有保障的。分离状态的线程再调用pthread_join()没有效果
线程的退出
正常退出:
在线程的函数中执行了return语句。
执行了pthread_exit(void*)函数
非正常退出:
自身出现错误
被其他线程终止/取消
exit()和pthread_exit(void*)的区别?
exit() 是结束进程,所有线程全结束
pthread_exit()是结束线程,其他线程继续运行
参数void* 和 return 一样,都是 用于返回值。
取消线程的函数: pthread_cancel()
九、线程同步
多线程之间是共享进程的资源,因此有可能出现共享数据的冲突,解决方案就是把并行访问改为串行访问,这种技术叫线程同步。线程同步的技术包括: 互斥量、信号量、条件变量。
互斥量又叫互斥锁,是线程在设计时官方的同步技术,编程步骤如下:
(1) 声明互斥量
pthread_mutex_t lock; //变量名不一定叫lock
(2) 初始化互斥量
pthread_mutex_init(&lock); 或在声明的同时赋值: pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
(3) 加锁/上锁 pthread_mutex_lock(&lock);
(4) 执行共享数据的访问代码
(5) 解锁 pthread_mutex_unlock(&lock);
(6) 释放锁的资源
pthread_mutex_destroy(&lock);
信号量是一个计数器,用于控制访问共享资源的最大的并行 进程/线程的数量。
信号量的工作原理:先设置最大值最初始计数,每上来一个计数减1,每退出一个就加1,到0就不允许新进程/线程访问,除非计数又回到大于0。 信号量不属于线程的范围,不在pthread.h中,只是一个线程计数辅助。头文件 semaphore.h 信号量如果初始计数为1,效果等同于互斥量。
信号量的编程步骤:
(1) 声明信号量 sem_t sem;
(2) 初始化信号量的原始计数 sem_init()
sem_init(&sem,0,count)
第一个参数就是信号量的地址
第二个参数必须是0,0代表线程的计数,非0代表进程的计数(Linux系统没有提供进程计数功能)。
第三个参数就是 计数的初始值(最大计数)。
(3) 计数减1 sem_wait(&sem);
(4) 正常使用
(5) 计数加1 sem_post(&sem);
(6) 释放信号量资源 sem_destroy(&sem);
使用线程同步技术,小心避免死锁。
pthread_mutex_t lock1,lock2;
线程a:
lock(&lock1);
...
lock(&lock2); //等待线程b unlock(&lock2)
...
unlock(&lock2);
unlock(&lock1);
线程b:
lock(&lock2);
...
lock(&lock1);//等待线程a unlock(&lock1)
...
unlock(&lock1);
unlock(&lock2);
结果就是看起来都问题,但执行 a和b互相锁定,死锁。
打赏
License
本作品由Simon(http://www.uusystem.com)创作,采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。 欢迎转载,但任何转载必须保留完整文章,在显要地方显示此声明以及原文链接。如您有任何疑问或者授权方面的协商,请邮件:postmaster@uusystem.com。