**********************************************************************************

和linux相关的一些信息
1.GUN计划:GUN的主旨在于发展一个类似Unix,并且为自由软件的操作系统,GUN系统。
2.POSIX标准:是由IEEE和ISO/IEC开发的一簇标准,该标准基于现有的Unix实践和经验,描述了操作系统的调用服务接口,用于保证编制的应用程序可以子啊源代码一级上在多种操作系统上移植运行。
3.GPL通用公共许可证
GUN通用公共许可证(GPL)一个法定的版权声明,但是允许对某项成果以及由他派生的其余成果的重用,修改和复制对所有人都是自由的。

 

UNIX操作系统,是美国AT&T公司于1971年运行的操作系统。具有多用户/多任务的特点,支持多种处理器架构。

标准:(规范),产品和项目,标准是行业标准,所有的行业产品都必须遵守
。标准一般都是行业集体指定,也有行业老大制定。产品是以质量为核心的软件。不需要过多的考虑某些特殊团体的需求,只需要做好产品本身就可以。
项目是针对特定用户的软件,受特定客户的限制。

Linux是类UNIX系统,GUN解决了界面的问题,POSIX规范解决了和Unix的兼容问题,GPL解决了版权问题,加上本身开源免费,因此发展非常迅速,各种版本也层出不穷。


编译器GCC
编程基本上使用高级语言(C,C++,java),高级语言计算机无法直接执行,先变成汇编语言,然后汇编语言转成机器指令,计算机才能执行。
gcc可以用来编译连接代码,生成可执行文件/动态库

gcc的用法:

gcc -E:把指令(以#开头)进行预处理,转成纯C语法。
gcc -S:生成汇编文件,生成.s文件
gcc -c:只编译不连接,生成.o文件
gcc -o:改变目标文件名
gcc 编译+连接,生成a.out文件
gcc -Wall 显示更多的警告,规范代码
gcc -I 路径 寻找头文件


在使用GCC编译程序时,编译过程可以细分为4个阶段:1.预处理,针对所有指令(以#开头的),把指令换成纯C语法 2.编译,狭义上的编译,主要工作是完成一些错误上的检测 3.汇编,把高级语言转换成汇编语言 4.链接,把相关的各种资源都连接进来。通常情况下所说的编译包括1,2,3三个。也就是广义的编译。

如果想把预处理的结果保存起来,可以用:gcc -E hello.c -ohello.i这样会生成一个hello.i文件。选项的位置比较灵活,可以放在前面或后面(个别情况例外)

C程序员会写.c文件(源文件)和.h(头文件),各种声明和定义都写在头文件中,各种实现和赋值都是写在.c文件中。比如:全局变量的声明可以写在头文件中,函数的原型声明写在头文件中,结构的声明,宏的定义都写在头文件中,函数的原型声明写在头文件中,结构的声明,宏的定义都写在头文件中;变量的赋值必须在源文件中,函数的实现(代码)必须在源文件中。
注:头文件中的内容迟早会被源文件所包含和使用,因此写在头文件中的内容都可以直接写在源文件中,头文件的意义在于可以被多个源文件同时使用。

 

C语言的指令
以#开头的统称为指令,常用的指令包括:
1.#include 主要用于包含头文件,可以使用头文件中定义的变量,函数和结构,联合等
2.#define #undef 定义或者取消宏变量,宏函数
3.#if #elif #else #endif
#ifndef #ifdef #endif 用来做条件编译,在编译期间选择分支,而C程序中的if else 是在运行期间选择分支,编译时都会进行编译

#error 产生一个错误
#warning 产生一个警告
#pragma 额外提供一些功能
#line 指定行号,意义不大

#pragma GCC dependency 文件名a:关联两个文件的时间,如果当前文件比a文件新,就没事;如果a文件比当前新,就会产生一个警告。
#pragma GCC poison 单词:把单词声明为毒药,禁止使用,否则产生错误。
#pragma pack(n):以n的倍数作为对齐和补齐的字节数,n如果大于4,按4计算,
对齐就是每个成员必须前面留出sizeof(自己)的整数倍,超过4个字节的按4计算,补齐就是整个结构的大小应该是最大成员的整数倍,超过4字节的按4字节算。对齐和补齐会提升效率,但浪费了空间。如果不想浪费空间,可以使用#pragma pack(1)解决。


关于头文件
代码可以写到头文件(.h)和源文件(.c)中。
各种声明写在头文件中,各种实现写在源文件中。变量声明,函数的原型声明,结构体/联合的声明都写在头文件中;
变量的赋值,函数的代码都写在源文件中。

定位头文件,系统头文件系统搞定;程序员自定义的头文件,有三种方法可以找到:
1.用” “把头文件扩起来,用相对路径定位目录,比如”./a/add.h“
2.gcc -I 后面跟头文件所在的目录,用<>也能找到。
3.用环境变量CPATH设置头文件所在的目录,用<>也能找到。环境变量的设置,在bash下,用 export CPATH=. 即可设置, .代表当前目录。


*******************************************************************************************


操作系统是由内核和shell组成,内核完成的核心的功能。shell是内核和用户之间的接口。Unix系统常见的shell包括:
1.sh---最早的shell
2.bash--sh的增强版,多了很多额外的支持。
csh--按照C程序员的习惯写的shell
sh和bash中,普通用户命令提示符是$;csh中,普通用户的命令提示符是%;对于root用户,命令提示符是#。


每种操作系统都有环境变量:CPU负责运算,执行各种指令,但CPU不负责数据的存储,数据的存储是由内存或硬盘上的文件负责。CPU可以直接访问内存,但不能直接访问硬盘上的文件,因此硬盘上的文件必须先进入内存(加载)后才能被程序使用。
内存中的数据 一旦关机就会消失(通常意义上的),而硬盘上文件中的数据会永久存在。
操作系统有许多的辅助数据需要存在内存之中,其中有一部分就是环境变量。比如:PATH就是系统路径的辅助数据。一般来说可执行文件都是要带路径才能启动的。而定义在PATH中的路径系统自动能找到,因此可以省略路径。

对C程序员来说,环境变量包括:
CPATH-用来定位头文件的。
LIBRARY_PATH 用于连接时定位库文件的。
LD_LIBRARY_PATH用于运行期间定位共享库文件的

 

配置环境变量时,要保证以前的不变,然后在加入新的。
配置环境变量可以用代码和命令实现,命令的实现方式:
在bash下,用命令:export PATH=$PATH:. ,这样就把 . 加入到了环境变量中。
$PATH代表以前的环境变量PATH的值
echo $PATH 打印输出PATH的值
. 代表当前目录 :是路径分隔符 $PATH代表以前的值
在命令行敲 export PATH =.:$PATH 只对当前窗口有效,如果想持久有效,需要把命令写到启动文件中。

bash中,启动文件可以选登陆目录下(~)下,.bashrc(还有其他的启动命令),

把export PATH=.:$PATH 写入.bashrc 就持久有效了。


Unix/Linux的库文件
大型项目中,如果每个函数都在不同的.o文件中,项目管理是灾难。库文件可以解决这个问题。把相关的.o文件打包到一个库文件中,代码从库文件中获取。商业开发中,每个程序员都是提供一个或几个头文件+库文件
库文件有两种,静态库和共享库(动态库),静态库是代码的合集,使用方法是直接复制代码到目标文件中;共享库是代码的合集,使用方法是复制代码在共享库中的地址到目标文件中。
静态库的优点:速度稍快,在运行时不需要库文件(独立性),缺点是占用空间大,不利于修改和维护。
共享库的优点是占用空间小,修改和维护方便。缺点是速度稍慢,在运行时需要库文件同时存在。
商业开发中,以共享库为主流。


创建和使用库的步骤:
提供库文件的程序员必须同时提供头文件。
静态库:(后缀是.a文件)
创建静态库的步骤
1.写源程序,保存退出。
2.用gcc -c 编译,生成.o文件。 gcc -c add.c
3.ar -r命令创建静态库文件
ar -r libmyku.a add.o
静态库文件名有规范,前面必须是lib开头,后缀必须是.a,中间写上库名。库和库文件名是不一样的,库名不带前缀和后缀。

调用静态库的步骤:
1.写源程序,保存退出。
2.用gcc -c 编译,生成.o文件。 gcc -c test.c
3.连接静态库文件和test.o,方式有三种:

a.直接连接法 gcc test.c libmyku.a
b.双L连接法 gcc test.c -l库名 -L库路径
c.单L连接法 先配置环境变量LIBRARY_PATH,把库路径写入,用gcc test.c -l库名 即可。


*********共享库(.so)的创建和使用
创建共享库的步骤
1.写源程序,保存退出。
2.用gcc -c 编译,生成.o文件。 gcc -c -fpic add.c
3.gcc 命令创建共享库库文件
gcc -shared add.o -olibmyku.so
调用的步骤和方法与静态库完全相同。
但是需要注意:共享库在运行时,必须先配置环境变量LD_LIBRARY_PATH
。使用命令export LD_LIBRARY_PATH=. 。配置路径。
**********使用ldd a.out命令即可察看这个执行文件需要的库的情况

系统提供了一些直接操作共享库的函数,包括:(了解)
dlopen() ---打开一个共享库
dlsym() ---从一个打开的共享库中打开一个函数,返回函数指针
dlclose() ---关闭打开的共享库文件
dlerroe() ---判断是否出错

 

C程序员的错误处理
程序员都要写错误处理的代码,错误处理是软件的一个组成部分。后期的语言都是使用异常机制(exception)处理错误,C语言没有使用异常机制。
C程序员用函数的返回值代表是否出错,错误处理的代码需要单独编写。

错误处理的方式有四种:
1.如果函数的返回值是int,并且返回的数据不可能是负数,可以用返回-1代表出错,其他正常。
2.如果函数的返回值是int,并且返回的数据有可能是负数,用返回-1代表出错,用返回0代表正确,数据用传入指针带回来。
3.如果函数的返回值是指针,返回NULL代表出错。
4.如果函数不可能出错或不需要考虑出错的情况,返回数据正常返回即可。一般void代表不考虑出错,数据可以用返回值或指针返回。
注:以上四种情况只是C程序员的常规处理方案没可以不按这个方案进行错误处理。

 


。。。。。。。。。。。。。

C官方提供了一个变量和3个函数用于错误的显示和处理,C语言存储的都是错误的编号,如果想知道究竟是什么错误,必须察看编号对应的错误信息。错误编号存在一个外部全局变量errno中,错误编号转错误信息的函数有:
strerror() 传入错误编号,返回错误信息,不打印。
perror() 自动打印errno对应的错误信息并换行(没有发生的错误无法察看)
printf("%m") 自动打印errno对应的错误信息,了解

如果代码出现了错误,会改变errno的值,记录错误编号,如果代码没有出错,errno是不会改变的。因此不能用errno判断是否出错,errno是用来记录错误信息的,返回值是用来判断是否出错的。

C程序员处理错误的一般流程:用返回值判断是否出错,如果出错,调用perror()打印错误信息,并进行错误分支的代码编程。
注:不是所有的函数都支持errno,有些函数就没有使用errno,比如线程相关的函数都没有使用。

 

环境变量和环境表
程序员如何操作环境变量,命令也是程序员写的程序。
环境表是字符指针的数组,char* arr[] ,是所有环境变量在内存中的首地址。
字符指针数组也可以用char **表示,char * arr(二级指针)表示,很多时候环境表就是使用二级指针表示.
extern char** environ;就可以直接获得环境表的首地址。
environ 也是一个外部的全局变量,但在声明时没有加上extern,因为需要调用者自己加,extern代表全局变量来自于其他文件中。

 

environ一般都不直接参与指针的移动,而是用一个局部变量代替environ进行指针的移动,因为environ是所有的程序公用的。

指针的算术运算(加法和减法),移动的字节数由他指向的数据类型的大小决定,指向int,移动sizeof(int)字节,(加1的情况下).

 

 

********************************************************************************

环境变量的库函数:(只针对本进程)
getenv()/setenv()/putenv()/unsetenv()/clearenv()

1.getenv() :按环境变量的名取得环境变量的值

2.setenv() : 新增环境变量或修改/不改 已存在的环境变量。

3.putenv() : 新增环境变量或修改已存在的环境变量

4.unsetenv() : 删除一个环境变量

5.clearenv() : 全部删除环境变量。

 

 

Unix/Linux的内存管理(UC编程开始)

内存分配和回收的相关函数及实现关系。(6个函数)
进程的内存空间划分
虚拟内存地址空间 机制

内存管理的相关函数:

自动管理 STL容器(各种数据结构)。

C++的内存管理:new 分配,delete回收

C程序员的内存管理:malloc()分配,free()回收

Unix系统函数,UC程序员:sbrk(),brk()
mmap()分配,munmap()回收

以上为用户层

------------------------------------

kmalloc() vmalloc() (内核层) (嵌入式方向使用)

 

--------------------------------
进程的内存空间划分

进程就是运行在内存中的程序。
程序就是硬盘上的可执行文件,是编译连接的产物。

进程的内存空间划分为以下部分:

1.代码区----存放代码(函数)的区域,函数指针就是指向代码区,是函数在代码区的地址。是只读区域。

2.全局区----存放全局变量和static修饰的局部变量。

3.BSS段----存放未初始化的全局变量
注:全局区和BSS段都会在main()函数执行前创建,区别在于BSS段在main()函数执行之前会自动清零一次。

4.栈区(stack)----没有用static修饰的局部变量,包括函数的形参。栈区的内存系统自动管理,程序员也可以用过特定函数进行管理和参与,但不建议使用。

5.堆区(heap)----也叫自由区,是程序员全权管理的区域。系统不会自动对堆区做任何事情。malloc()等函数都是操作堆区。堆区内存的分配和回收都是由程序员完成。

6.只读常量区----很多书上把这个区域并入了代码区,字符串的字面值(“abc"),和const修饰的全局变量。

虚拟内存地址空间:UNIX系统中,程序员所接触到的内存地址都是虚拟内存地址,而不是真实的物理内存地址。
虚拟内存地址 其实就是一个整数,本质上就是一个整数,是不能存储数据的。需要做内存映射后才能存储数据。每个进程在启动时就先天具备了0~4G的虚拟内存地址空间(32位系统),这些地址都是一个整数,而无法存储数据。虚拟内存地址必须映射到物理内存/硬盘文件后才能存储数据。所谓内存分配就是先分配未使用的虚拟内存地址,再用虚拟内存地址映射到物理内存/硬盘文件。如果使用了未映射的虚拟内存地址获取数据就会引发段错误。进程之间即使是相同的虚拟内存地址对应的物理内存也是不一样的。
Unix/Linux系统用一个整数(虚拟内存地址)代表一块物理内存。

 


虚拟内存地址是按字节管理的(一个字节有一个虚拟地址),但是在做内存映射时不是以字节为单位映射,而是用内存页做映射的基本单位。一个内存页(32位系统中)是4096字节(4k),函数getpagesize()可以获得内存页的大小.(空间换时间)
malloc(4) 分配4字节的虚拟地址+映射33页物理地址。
0~4G的虚拟内存地址分为用户空间和内核空间,用户空间是0~3G,3G~4G是内核空间,用户空间是给用户使用,内核空间给内核使用,用户空间的程序不能直接诶访问内核空间,但可以通过系统提供的系统函数(系统调用)进入内核空间。

 

关于数组和指针:
大多数情况下,数组和指针可以混合使用,但有区别:
1.sizeof()不同,数组的sizeof()是数组的长度*元素的大小,而指针
的sizeof()是4(32位系统)
2.指针可以改变地址的指向,而数组是常指针(指向地址不能改变)
3.函数的返回值不能是数组,因此如果返回数组时,返回值类型必须写指针。

 

进程内存空间的排列次序:(由小到大)
代码区,只读常量区,全局区,BSS段,堆区,栈区,其中栈区在3G左右的位置。


Linux系统中,文件几乎可以代表一切的东西,内存有文件与之对应,输入输出设备也有文件与之对应。目录也是文件。

cat /proc/pid/maps 可以察看内存映射的情况,其中pid就是进程编号ID,可以用getpid()函数获得进程号

 


字符串处理和数据结构是每个程序员的基本功,作为C程序员操作字符串的基本代码。

 

 

*******************出段错误的可能性:使用了未映射物理内存的虚拟地址(如野指针),或者对没有权限的内存进行了操作(对只读区进行修改)。

 

 

 

 

********************************************************************************

 

1.malloc()和free()函数

malloc()分配的堆区内存,一次申请小块内存时,系统会映射33个内存页,如果申请大块内存(31个内存页以上),系统会映射比申请的稍多一点的内存页数。

malloc()申请内存时,除了数据占用的空间外,还需要额外占用一些空间,用于存储附加数据。malloc()申请内存时,包含三个部分:数据本身的空间,附加数据的空间,空的空间(预留空间)。附加数据存于 底层维护的双向链表中。预留空间的大小不确定,可大可小。
int *pi=malloc(4);
free(pi); //free()释放到哪里结束由附加数据决定。

理论上因为malloc()会映射多个内存页,会导致未分配的内存只要在33页之内都能使用;但为了便于内存的管理(反复使用和控制),内存还是要先分配再使用

free()一定会释放被占用的虚拟内存地址,但不一定会解除内存映射,对于malloc()来说,free()不会释放最后的33个内存页。最后33个内存页在进程结束时才会释放。free()在大多数情况下会清空内存,但是不保证清0.
free()对于申请大块内存时 会全部解除映射。

malloc()虽然地址不连续,但是不影响内存的使用。malloc申请内存,在使用时不要过界,否则会影响下一次的内存分配和回收。

 

 

2.sbrk()和brk()

sbrk()和brk()是Unix的系统函数,机制和malloc()完全不同,它是借助系统维护的一个位置进行内存的分配和回收。


void* sbrk(int increment)
功能是分配/回收内存,大多数情况下用于分配内存。
参数是分配/回收的增量,为正数时是分配,为负数时是回收,为0时是取当前的位置地址。
返回移动之前的位置,如果出错返回(void*)-1

sbrk()在分配内存时非常方便,但回收内存时需要计算字节数,因此比较麻烦,不利于管理。brk()则相反。一般情况下,都是使用sbrk()分配内存,使用brk()释放内存(虽然两个函数都可以分配和回收)

sbrk()和brk()都是以一个内存页作为映射的基本单位的,一旦释放就会用是解除内存映射。

int brk(void *position)
功能是分配/回收内存,一般用于回收内存
参数就是新的位置,无论原来的位置在哪里
返回:成功返回0,失败返回-1.


srbk()和brk()都不会清除回收内存的数据,数据会在下次使用时被覆盖。


memset()函数可以设置内存的内容,一般用于清内存,
memcpy()可以复制内存的内容,一般用于内存的复制。

mmap()和munmap()是用户层能使用的功能强大的函数。可以设置一些内存相关的数据。比如首地址,比如权限。mmap()实现内存映射,包括:映射物理内存和硬盘文件。但映射硬盘文件需要使用文件描述符,需要文件的相关函数做支持。

多个权限和多个选项用位或 | 连接。
RWX
R--
-W-
--X
读+写 100 010(进行位或)------>110


void *mmap(void *addr,size_t size,int prot,int flags,int fd,off_t offset)
功能是内存映射,比sbrk()功能强大的多,失败返回-1。
参数:addr指定映射的首地址,为0交给内核选择,
size就是映射内存的大小,
prot就是内存的权限,一般PROT_READ|PROT_WRITE
flags是映射的标识,主要包括:MAP_SHARED MAP_PRIVATE 二选一即可,第一个代表这块内存其他进程可以共享,但只对映射文件有效。第二个就是其他进程不能共享,只能本进程使用,MAP_ANONYMOUS代表映射物理内存,不写就映射硬盘文件(默认)
fd 是文件描述符,映射文件时有效,映射物理内存时为0
offset是文件的偏移量,选择映射文件的位置,映射物理内存为0

返回:成功返回映射的首地址,失败返回MAP_FAILED


用户空间的程序不能进入内核空间,但很多功能需要内核实现,因此内核空间提供了一系列的函数,允许用户空间调用,从而进入内核空间。这一系列的函数统称为系统调用(system call) 比如:标C的malloc()不能进入Unix内核,但内存分配必须依赖内核,内核提供了系统调用sbrk() mmap(),帮助malloc()进入内核完成内存的分配。
系统调用的使用方式和标C函数没有任何的区别,因此无需过多关注。

 

**********************************************************************************

标C的函数不能直接访问内核,因此必须有文件相关的系统调用才能实现功能,文件读写系统函数:
open()

read()

write()

close()

ioctl()

其中ioctl()目前用不上,因此不讲。

open()和close()

open()创建/打开一个文件,返回文件描述符。
close()可以通过文件描述符关闭打开文件。

 

文件描述符本质是一个非负整数,代表一个打开的文件。文件描述符的范围是3~打开的最大数量,Unix是63,Linux可以到255,
0,1,2被系统预先占用,代表标准输入/标准输出和标准错误。文件描述符在关闭后可以重复使用。系统在选择文件描述符时,会找目前未使用的最小值。

文件描述符只是一个整数,如何代表一个打开的文件呢?
open()函数会打开一个文件,返回文件描述符,真实过程是:
open()先通过i节点找到硬盘上的文件,把文件的相关数据加载到内存中,放入文件表(看成结构体)中,然后找一个未使用的整数代表文件表,这个未使用的整数就是文件描述符。每个进程在启动时,都会创建一个文件总表,存放文件描述符和文件表的对应关系。总表中找不到的整数就是未使用的。open()每返回一个描述符,都会把这个描述符加入总表中。close()每关闭一个描述符,其实都是把描述符从总表中删除,不一定删除文件表,一个文件表可以对应多个文件描述符,只有没有任何文件描述符与之对应的文件表才会被删除。


文件有两套数据,一套是文件在硬盘上的,一套是加载到内存中的。
文件在硬盘上是用过i节点进行管理的,i节点也是整数,可以看出文件在硬盘上的地址。系统通过i节点(inode)找到文件和目录的。ls -i 可以察看i节点。


int open(char *filename,int flags,...)

功能: 打开一个文件,并返回文件的描述符,
参数:filename 就是文件名,要带路径。
flags是文件打开的标识,由一些宏组成
... 叫可边长参数,代表0~n任意类型的参数。

 

其中flags的常见的值:
O_RDONLY O_WRONLY O_RDWR 三选其一,打开文件的权限
O_CREAT 代表可以新建不存在的文件,需要使用第三个参数指定文件在硬盘上的权限。
O_EXCL 代表如果文件存在,返回-1,而不是打开(只是新建而不打开)
必须和O_CREAT结合使用
O_TRUNC 代表如果文件存在,打开时清空文件的内容。(小心使用,因为文件中的数据直接诶删除)
O_APPEND 代表以追加的方式打开。针对追加写


返回:成功返回文件描述符,失败返回-1.

int read(int fd,void *buf,int size)
int write(int fd,void *data,int length)

功能:读写文件函数
参数:fd都是文件描述符,buf是接受数据的首地址,size就是buf的大小。data是要写入的数据的首地址,length是数据的有效长度。

返回值:失败都是返回-1,成功都返回 实际读写的字节数。读文件时,返回0,代表读到了文件尾(结束条件)

 

 

目前为止,读写文件可以使用UC和标C函数,那么如何选择?
time a.out 可以测试运行时间

 

标C的输入输出函数都有缓冲区的存在,因此真实的读写文件的次数和循环的次数是不一样的。而UC函数在用户层是没有缓冲区。UC函数在频繁读写时,需要程序员自定义缓冲区以提升效率。缓冲区的大小不是越大的越好,需要测试找到最大值。(标C函数也可以用过改变缓冲区的大小提升效率)。

 

 

*****************************************************************************

lseek()

其他的一些文件相关函数。
off_t lseek(int fd,off_t offset,int whence)
功能:就是设置文件偏移量,确定读写的位置
参数:fd就是文件描述符
offset和whence 一起确定偏移量的位置。
offset指定偏移多少字节
whence指定偏移的起始位置
whence的三个值:SEEK_SET SEEK_CUR SEEK_END
比如:offset=10 whence=SEEK_SET,那么偏移量就是从文件头开始第10个字节。

返回值:成功返回新位置到文件头的偏移量,失败返回-1,可以用于获取文件的大小。

read()和write()每读写一个字节,偏移量会自动向后移动一个字节。
但是lseek()可以按你的要求移动。


vi编译器在使用wq保存退出的时候,有可能在最后加一个结束符,可以让类似cat命令实现换行。而读写函数不会。

其他一些文件相关函数:

dup()和dup2() ----复制文件描述符,不复制文件表,会造成多个文件描述符对应同一张文件表

dup()和dup2()的区别:
dup()复制文件描述符,新的描述符的值由系统指定,
dup2()复制文件描述符时,新的描述符的值由程序员决定,如果传入的值已被使用,先关闭然后在继续使用。(强行关闭)dup2()有可能出现读写数据来自不同的文件。比如:关闭a文件,复制后指向b文件。

一张文件表--->一个偏移量,如果dup(),dup2()创建的新的描述符和以前的描述符公用一张文件表的话,偏移量也只有一个。因此无论用哪个去写,都不会有覆盖行为。


fcntl()函数提供了很多的功能,主要包括:
1.复制文件描述符(类似dup()系列)
2.设置/获取文件描述符的状态,但是有些状态无法处理
3.文件锁的使用 。struct flock
int fcntl(int fd,int cmd,...)
功能:提供一些操作关于文件描述符的
参数:fd就是要操作的文件描述符号,cmd是采用何种操作,第三个参数根据cmd发生变化,cmd的一些情况为:
F_DUPFD(long) 复制文件描述符,新描述符的值由第三个参数传入long,和dup2()区别在于不会强行关闭已存在的描述符,只会继续寻找未使用的操作符(大于或者等于参数)。
F_SETFL(long)/F_GETFL(void)设置/获取文件描述符的状态,其中获取时没有第三个参数,只能返回文件权限和其他状态(不包括创建状态);设置时只能设置极少的状态(O_APPEND)
F_SETLK(struct flock*)/F_SETLKW(struct flock*)设置文件锁(阻塞和非阻塞两种方式)

返回值:不同的cmd的返回值不同。

关于位与:与0做位与,清0,与1做位与,不变。取二进制的某一位或某几位,只要把其他位与0做位与,而要取的位与1做位与即可。例如:取某整数的后8位,则a&0xFF。


关于文件锁(针对多进程或多线程)
如果有多个进程/线程同时读写文件,可能导致读写数据的混乱。解决方案有许多,从进程/线程的角度都有解决方案,从文件角度的解决方案就是 文件锁。
核心思想就是在同一时刻,只有一个进程/线程 读写文件,其他进程/线程等待。后来出了优化的方案:允许多个进程同时读,但不允许同时读或同时读写。这种方案叫读写锁,应用非常广泛。
读写锁就是由两把锁组成,读锁用于读操作,读锁(共享锁)的特点就是允许其他进程的读操作,但不允许其他进程的写操作。写锁(独占锁)用于写操作,写锁的特点就是不允许其他进程读写操作。

函数fcntl(fd,F_SETLK/F_SETLKW,struct flock*)可以加文件锁(解除文件锁也用它)

struct flock
{
short l_type;
short l_whence;
int l_start;
int l_len;
pid_t l_pid;
};


其中,l_type是锁的类型,包括:F_RDLCK读锁/F_WRLCK写锁/F_UNLCK释放锁

l_whence和l_start联合决定了锁定的初始位置,l_whence可以为SEEK_SET/SEEK_END/SEEK_CUR,表示锁定的参考起始点;l_start设定相对于参考点的偏移量;比如:l_whence选SEEK_SET,l_start为10,则锁定的起点在文件头后面的10个字节。

l_len是锁定的长度

l_pid是锁定的进程PID,只对F_GETTLK有效,其他时候为-1即可。


返回值:加锁失败返回-1。


读写锁的fcntl()函数并不能锁定read()和write(),能锁定的是其他进程的fcntl(F_SETLK)加锁行为。文件锁的正确用法是:调用read()函数之前先加读锁,调用write()函数之前先加写锁,使用完毕后释放锁。


F_SETLK和F_SETLKW区别:
前者是非阻塞的,加不上锁直接返回-1,后者是阻塞的,加不上锁会一直等待,直到锁定解除/信号中断。


stat()/fstat() 获取文件在硬盘上的各种状态,类似ls -il 的效果。
其中文件权限和文件类型是混在一起的,需要使用宏函数判断文件的类型:最常用的S_ISREG(),S_ISDIR()

C语言中对于时间有两种表示方式:time_t 秒差----时间和1970年1月1日0点0分0秒的秒差
struct tm 结构----年月日小时分秒星期的格式

计算机更多的是使用秒差,人则相反。

int access(char *filename,int mode)
这个函数可以测试是否有权限或者文件是否存在。
参数mode有四个值:
R_OK W_OK X_OK F_OK 是否有读写执行的权限,最后一个是判断文件是否存在,如果有权限返回0.

 

 


******************************************************************************

文件的相关函数

目录的相关函数
Unix/Linux进程管理

文件的相关函数:
chmod()----修改文件的权限,一般情况下用命令完成。
umask()----可以设置新建文件时的系统权限屏蔽字,默认的权限屏蔽字是0002,只屏蔽其他用户的写权限。umask()的使用方式:先改变,在还原(只有特殊文件才需要改权限屏蔽字)
老的权限 umask(新的权限)
新建文件
umask(老的权限)


truncate()/ftruncate()----指定文件的大小(可大可小)。
remove()----可以删除文件或者空目录
rename()----给文件或者目录改名


mmap()可以映射文件

 

 

目录相关函数
mkdir() :新建一个空目录
rmdir() :删除一个空目录(只能删除空的目录)
chdir() :切换当前目录(相当于命令cd)
getcwd() :获取当前目录的绝对路径(双返回的方式)
读目录内容的函数(字目录和子文件)
opendir() :打开一个目录
readdir():读目录的子项(一次只能读一个,循环)
closedir() :可以不调用

目录操作很多时候是需要使用递归的。readdir()经常和递归结合使用。

 

 


进程管理
Linux/Unix系统察看进程的命令是ps,在终端中只是敲ps,只会现实与终端有关的进程。
ps -ef 通用的察看所有进程的命令
ps -aux Linux的命令选项,Unix不直接支持(通过新增的ps支持)。

whereis可以察看文件/命令在哪里
whereis ps可以看到两个ps都在哪里。

分页看结果用 | more ,空格/回车翻行翻页,q退出
一般退出都是按q或者Esc键。
ps -ef | more

kill -9 进程ID: 可以杀死进程
进程的启动不是同时完成的,而是有先后次序,如果进程a启动了进程b,a叫做b的父进程,b叫做a的子进程。父进程可以有多个子进程,子进程只能有一个直接的父进程。

Linux系统的进程启动顺序:
1.系统启动进程0,进程0启动进程1(init进程)和进程2(有些系统只启动进程1),然后进程0不再做其他事情。
2.进程1和进程2负责启动其他的所有进程(直接或间接)

进程通常情况下都处于休眠状态,进程的常见状态

S:处于休眠状态,最常见的状态。
R:运行状态或可运行状态。
Z:僵尸进程,就是已经结束但资源没回收的进程。
s:代表有子进程
< :优先级高的进程
N: 优先级低的进程


系统用进程ID管理进程,每个进程的进程ID(PID)都是不能重复的,唯一代表一个进程。PID是一个非负整数,操作系统和当前用户都有进程数量的限制。PID结束后,PID可以回收。回收的PID可以重复使用,但必须延迟重用。即需要过一段时间才能重用。

 

进程的PID可以用函数getpid()获取,getppid()可以获取父进程的PID。getuid()可以获取当前用户的ID。

 

***************如何创建子进程:******************
1.fork()通过复制父进程自身去创建子进程。
2.vfork()+execl()可以创建全新的子进程。

fork()是非常复杂的简单函数。

pid_t fork(void) 返回进程PID
这个函数基本都会成功,出错的可能有:1.系统的进程已经满了。2.用户的进程满了。


fork()如何复制父进程:
1.子进程会复制父进程的内存区域,除了代码区,代码区父子进程共享。(内存地址也会复制,但是物理内存是不同的。)
2.父进程的输入/输出缓冲区的内容,子进程会复制。
3.如果父进程有文件描述符,子进程会复制文件描述符,但不复制文件表。
4.子进程会复制父进程的信号处理方式。
5.fork()之前只有一个父进程,之后有两个进程,一父一子。因此fork()之前的代码将会执行一次,而fork()之后的代码会执行两次。父子进程分别执行一次。子进程是从fork()开始执行代码的。
6.fork()函数能创建子进程,同时会返回两次,父进程返回子进程PID,而子进程会返回0,可以通过返回值的不同,让父子进程执行不同的代码分支。
7.fork()创建子进程后,父子进程同时运行,但谁先运行,谁先结束,都不确定。
8.fork()创建子进程后,父子进程同时运行,如果子进程先结束,会给父进程发信号,让父进程回收自己的资源;如果父进程先结束,子进程变成孤儿进程,会让进程1(init)成为新的父进程,init进程也叫孤儿院。

 

********************************************************************************

 

fork() /退出进程 /父进程等待子进程/vfork()/execl()/Unix/Linux的信号

****************break退循环,return 退函数,exit()退进程
程序员退出进程的方式:

正常退出:
(1)在主函数中执行了return语句,(特殊方式,只针对主函数有效)
(2)exit()函数退出进程(通用方式)
(3)_exit()或_Exit()函数退出进程(立即退出)
(4)最后一个线程结束

非正常退出:
(1)被信号干掉,比如:ctrl+c
(2)最后一个线程被取消

exit(int)/_exit(int)/_Exit(int)
_exit(int)/_Exit(int)在底层是一样的,_Exit()调用了_exit().
exit()函数会退出进程,但不一定是马上退出,允许通过atexit()函数注册一些其他的函数,在退出前会先执行注册过的函数
_exit()函数会立即退出,只做三件事情:
(1)关闭文件描述符
(2)让所有子进程变成孤儿进程
(3)发退出信号给父进程

如果没有特殊的需求,退出进程用exit()就可以。


wait()和waitpid()

wait()和waitpid()可以让父进程等待子进程的结束,并且取得子进程结束的方式(正常退出还是非正常退出)和退出码。

wait()必须等待任意一个子进程的结束,只要有子进程结束wait()就返回,如果没有子进程结束,父进程继续等待。
waitpid()可以等待多种方式的子进程,也可以不等待,因此更灵活。

wait()和waitpid()能回收僵尸子进程的资源。

pid_t wait(int *status)
功能是等待任意一个子进程的结束,并取得退出状态和退出码
参数:status是传出参数,返回子进程的退出状态和退出码
返回值:返回结束子进程的PID。

宏函数被用于判断子进程的退出状态和获取退出码:
WIFEXITED()判断是否正常退出
WEXITSTATUS()获取退出码,只有正常退出才有效。

pid_t waitpid(pid_t pid,int *status,int option)
功能:等待子进程的结束,但是比wait()更灵活(可以不等)
参数:pid是等待哪个/哪些子进程,包括四个值:
-1:等待任意一个子进程的结束
>0:等待特定的一个子进程(进程ID=pid)
0 :等待和父进程一个进程组的子进程(本组)
<-1:等待指定进程组的子进程(进程组ID=|pid|)
status和wait()中一样
option可以设置等待或者不等待,默认0为等待,WNOHANG为不等待。
返回:有子进程结束返回子进程的pid,如果不等待并且没有子进程结束返回0,失败返回-1.

 


vfork()+execl()创建子进程

vfork()函数从语法上和fork()没有任何区别,区别在于vfork()不会复制父进程的任何资源。子进程会占用父进程的资源继续运行,而父进程阻塞,停止运行。父进程的阻塞有两种方法可以解除:
1.子进程运行结束,把资源还给父进程
2.子进程调用了execl(),启动了一个全新的程序;也把资源还给父进程。(并行的方式)

第二种方法更常用,第一种方法方法没有实际意义。
vfork()会创建一个新的子进程,但是可以确保子进程先运行。
vfork()创建的子进程必须用exit()退出,return语句会有问题。

vfork()函数能创建子进程,但是不能提供代码和数据,execl()不能创建子进程,但是可以提供进程运行的代码和数据。

 

execl()函数不会创建新的进程,进程PID不变,用一个新的程序替换掉当前进程执行的程序。
int execl(char *path,char *cmd,...)
功能是启动一个全新的程序,当前程序将会被替换掉,但不会建立新的进程。
参数:path就是新程序的路径,包括文件名,不能出错
cmd就是运行程序的命令,比如:”a.out“
... 可以包括命令的参数/命令的选项,最后以NULL结束
返回:成功则启动新程序,没有任何的返回值;失败无法启动新程序,返回-1。


关于进程必须会写的代码:
fork()
vfork()+execl()


Unix和Linux的信号处理

信号(signal)是Unix/Linux系统中最常见的一种软件中断的方式。中断就是程序中止当前正在执行的代码,转而执行其他代码的过程。中断分为软件中断和硬件中断。软件中断主要方式就是信号。

信号的本质就是一个非负整数,不同的值可以代表不同的情况,信号都有一个宏名称,以SIG开头,比如:信号2 就是CTRL+C,的名字就是SIGINT,宏名称的定义在POSIX规范中,而值不保证一致。因此,编程时,信号都是使用宏名称而不是值,否则可能出现不一致。

信号在Unix和Linux中是不用的,Unix是1~48,Linux是1~64,但中间是不保证连续的。信号0有特殊用途,没有实际的意义。

信号分为可靠信号和不可靠信号,不可靠信号在Linux系统中是1~31,特点是不支持排队,因此多个相同的信号同时到来时可能出现信号的丢失。可靠信号是34~64,特点是支持排队,因此不会丢失。

命令kill -l 可以察看系统都有哪些信号。每种信号都有来源和处理方式

信号是无法确定何时到来的,在程序中无法判断信号的到来时间。

信号的处理方式:
1.默认处理,每个信号都有默认处理方式。如果不改变信号的处理方式,就采用默认处理。默认处理大多数就是退出进程。
2.忽略信号,不做任何的处理,就像信号没来过一样。
3.自定义处理,程序员需要写一个信号处理函数,然后让信号的处理方式改为信号处理函数。(重点)
注:有些信号是无法忽略或者自定义的。
信号可以在进程之间互发,但信号的发送有权限限制,当前用户只能给自己的进程发信号,root用户可以给所有进程发信号。

如何设置信号的处理方式:
函数signal()或函数sigaction()可以
sigaction()是增强版的signal(),但在应用中,signal()足够了,因此sigaction()介绍一下。

 


void (*func)(int) signal(int signum,void (*func)(int))

功能是:设置某个信号的处理方式为默认/忽略/自定义函数
参数:第一个参数就是设置哪个信号的处理方式,第二个参数就是函数指针,可以是:SIG_DFL(默认)/SIG_IGN(忽略)/自定义的函数名
返回:成功返回之前的处理方式(一般不用),失败返回SIG_ERR

信号处理的基本步骤:
1.一个头文件,#include<signal.h>
2.一个信号处理函数,格式:void func(int 信号值)函数名可变,参数为信号值
3.主函数中调用signal(),改变他的处理方式。

父子进程之间的信号处理方式
如果是fork()创建的子进程,子进程完全复制父进程信号处理方式,父子进程的信号处理方式一样。
如果是vfork()+execl()创建的子进程,父进程如果默认,子进程默认;父进程忽略,子进程忽略,如果父进程是采用自定义处理函数,子进程改为默认。

 

*******************************************************************************

信号:信号的发送函数/信号集和信号屏蔽/介绍一个sigaction()/信号应用的相关函数sleep() usleep() 计时器的使用

进程间通信(IPC)-------总体概念和IPC之一的管道

信号的发送方式:
1.键盘发送(少部分)
ctrl+c 信号2
ctrl+\ 信号3

2.硬件错误/代码问题(少部分)
段错误 信号11
总线错误 信号SIGBUS linux是信号7
整数除以0 信号SIGFPE

3.Unix命令kill发送信号(全部)
kill -信号 进程PID

4.使用Unix系统函数发送信号(有些函数是全部,有些是部分)
raise() kill() alarm() siggueue()
主讲kill() 介绍alarm()

如何判断某进程是否有发信号的权限?
信号0可以用于测试,因为信号0 没有实际意义,所以发过去后没有后果,只是知道是否能发。


raise()只能给本进程发信号,功能可以被kill()完全替代。等价于kill(getpid(),signum).

int kill(pid_t pid,int signum)
功能:给所有有发送权限的进程发信号
参数:pid就是信号的接收进程ID,包括:
>0 发送给特定的某个进程(进程ID=pid)
0 发送给本组进程
-1 发送给所有的有权限进程
<-1 发送给进程组ID=|pid|的所有进程。

多个同名进程可以用killall+进程名 全部杀死.


sleep(int n)函数会让进程休眠n秒,但是当有未忽略的信号到来时,休眠会被中断,返回剩余秒数。
usleep()函数也可以让进程休眠,但是以微秒作为休眠单位。

alarm()函数严格来说不算信号发送函数,只能给自己发特定的信号(闹钟信号)。可以用来定时做一些任务。

 

信号集:一个二进制位能代表一个信号,1代表有,0代表没有。理论上说,64位证书就能存放所有整数。但考虑到后面的扩展性,用了一个超大的整数去代表所有的信号的有无,类型是sigset_t(信号集)。倒数第N为代表信号N。
信号集是一种数据结构,包括逻辑结构/物理结构/运算结构。
逻辑结构就是在人的思维中的结构,物理结构就是计算机的底层如何实现(内存是否连续,连续就是数组方式,不连续就是链式表方式);运算结构就是提供哪些功能,也就是需要提供哪些函数。运算结构一般都会包括:
1.内存的分配和回收函数(数据结构的创建和销毁)
2.元素的添加(追加或插入),包括:单添和群添
3.元素的删除(从最后删除/从开头删除/从指定位置删除)包括单删和群删
4.元素的修改(不是所有数据结构都需要)
5.元素的查询和取出
6.特殊的需求,比如排序。

信号集提供的功能函数:
信号集的创建和销毁系统已经完成,没有函数。信号集的函数有5个。
sigaddset() --添加一个信号(把对应二进制位置1)
sigdelset()---删除一个信号(把对应二进制位置0)
sigemptyset()---清空信号集
sigfillset()---填满信号集(全部置1)
sigismenber()---是否包含某个信号(查询)


信号屏蔽:
有些关键代码不希望被信号打断,而程序员无法控制信号的到来。因此信号屏蔽技术就用来解决关键代码不被信号打断的问题。信号屏蔽不是屏蔽信号的到来,而是采用:信号可以到来,但来了以后暂时不做处理,等关键代码执行完毕后,解除了信号屏蔽再进行处理。(延迟处理)


如何实现/解除 信号屏蔽?
函数sigprocmask() 可以实现信号屏蔽和解除屏蔽。
屏蔽时把 新的需要屏蔽的信号传入,同时允许把旧的屏蔽的信号(信号屏蔽字)带出来;解除屏蔽时,把旧的信号屏蔽字再次设置进去即可。

int sigprocmask(int how,sigset_t *new,sigset_t * old)
功能:屏蔽信号或者解除屏蔽
参数:how是屏蔽的方式,包括:
SIG_BLOCK----相当于加法,屏蔽 老的+新的
SIG_UNBLOCK---相当于减法,屏蔽 老的-新的
SIG_SETMASK---相当于赋值,屏蔽新的,与老的无光
在使用时,用SIG_SETMASK即可。
new就是需要屏蔽的信号集
old是用来返回之前屏蔽的信号集,用于解除屏蔽
返回值:成功为0,失败为-1

可靠信号在信号屏蔽时不会丢失,不可靠信号如果同一个信号多次到来会丢失。
信号9不会被屏蔽。

介绍sigaction(),能实现更复杂的功能。

信号的应用之一-----计时器(了解)
linux系统中,每个进程都支持三个计时器:真实/虚拟/实用。其中真实计时器会记录程序的运行时间。计时器的功能就是一段时间之后(启动时间)每隔一段时间(间隔时间)生成一个信号,系统会处理信号。
计时器的设置和获取函数:
setitimer()和getitimer()

 


进程间通信(IPC)
进程之间不能通过内存直接互访,但进程之间有时候需要数据交互,因此进程间通信就非常重要了。linux系统以多进程为核心的操作系统,因此进程间通信在linux系统中频繁的被使用。

常见的IPC:
1.文件
2.信号
3.管道
4.共享内存
5.消息队列
6.信号量集(semaphore arrays)
7.网络编程(socket编程)

其中共享内存,消息队列,信号量集遵循相同的规范,在编程上有很多相似之处。统称为XSI IPC。


管道是unix IPC最古老的方式之一。现在较少使用。管道以管道文件作为交互的媒介。管道文件是一种特殊的文件,touch vi open()都无法建立管道文件。
想要创建管道文件需要mkfifo()函数或mkfifo命令可以创建管道文件。管道文件中不会留有数据,只是数据的中转站,因此管道文件在只有读或者只有写的时候会阻塞,直到读写管道都畅通时才会中转数据。
历史上的管道是半双工的,现在有全双工的。

管道分为有名管道和无名管道,有名管道可以用于任何进程间的通信,无名管道只能用于fork()创建的父子进程之间。有名管道就是程序员建立管道文件,然后进行IPC;无名管道就是内核处理管道文件,然后IPC。

IPC至少写两个程序,
有名管道的使用步骤:
1.先用mkfifo命令或函数创建管道文件。
2.用open()去打开管道文件
3.读写管道文件
4.关闭文件描述符
5.如果必要,可以用remove()删除管道文件(不一定做)。

 


*********************************************************************

XSI IPC之共享内存和消息队列

共享内存/消息队列/信号量集 遵循相同的规范,因此编程上有很多共性的东西。

共同点:

1. XSI IPC都是系统内核管理的,叫内核IPC结构。
2. XSI IPC都有外部的key,类型是key_t,可以定位内部的IPC结构。key本质上一个非负整数
3.外部key的获取方式有三种:
(1)宏 IPC_PRIVATE 做key,这种方式程序员基本上不用。因为这种方式能建IPC结构,但外部是不能获取的。
(2)定义一个公共的头文件,所有的key存在头文件中。(key的赋值需要单独定义一个.c文件)
(3)使用一个函数 ftok()生成key。ftok()以文件作为参数,这个文件/目录是必须存在的

4.在内核中的IPC结构都由一个ID代表,这个ID可以用函数获取,这个函数叫:xxxget(key,...),比如:shmget(key,...)共享内存 msgget(key,...)消息队列

5.创建IPC结构的函数也是xxxget(),其中的flag需要写成:IPC_CREAT|0666(权限)

6.在代码中用ID代表IPC结构,就像用fd代表一个打开的文件一样。

7.每种IPC结构都一共了一个xxxctl()函数,可以实现对IPC结构的查询/修改和删除。比如:shmctl() msgctl() ,其中,会有一个cmd参数,至少包括三个值:
IPC_STAT---查询
IPC_SET---修改
IPC_RMID---删除

8.系统提供了一套命令察看/删除IPC结构:
ipcs---察看IPC结构
ipcs -a 察看所有IPC结构,包括三个
iosc -m 察看共享内存
ipsc -q 察看消息队列
ipcs -s 察看信号量集

ipcrm ---删除IPC结构,按ID删除。

ipcrm -m ID 删除共享内存
ipcrm -q ID 删除消息队列
ipcrm -s ID 删除信号量集

 

共享内存(shared memory)

共享内存的媒介是一块系统内核管理的物理内存。这块物理内存允许所有进程挂接(映射),多个进程就直接映射到了同一块物理内孙,数据可以直接交互,使用完毕后脱接共享内存(解除映射)。
共享内存是所有IPC中最快的。

共享内存的使用步骤:(固定套路)
1.获取key,使用ftok()或写公用的头文件。
key_t key=ftok();
2.创建/获取共享内存的内部ID,使用函数shmget()。
3.挂接共享内存(映射),使用函数shmat().
4.正常使用共享内存(存取数据)
5.脱接共享内存(解除映射),使用函数shmdt().
6.如果确保不再使用,可以使用shmctl(IPC_RMID)删除

key_t ftok(char *path,int projectid)
path就是文件/目录的路径,必须存放,否则出错。
projectid是项目ID,0~255有限(只取后8位二进制),
成功返回key,失败返回-1

 


int shmctl(int shmid,int cmd,struct shmid_ds* ds)

功能:查询/修改/删除 共享内存
参数:shmid 就是共享内存的内部ID,cmd就是操作方式,包括:查询IPC_STAT /修改IPC_SET /删除IPC_RMID ds只对查询和修改有效,删除时传0即可。
返回:成功返回的比较复杂,和cmd有关,失败返回-1.


删除共享内存时不一定会立即删除,只有nattch为0的共享内存才能删除。如果nattch不为0,只会做一个删除标志而不是立即删除,当nattch为0的时候才删除。
共享内存的优点就是最快,缺点不能很好的处理多个进程同时写数据的情况。多个进程同时写数据的时候采用消息队列。

 

消息队列---队列是种数据结构,按次序存放元素,先入先出,队列中的元素就是消息。
消息队列的使用方式是先把数据放如消息中,然后把消息放入队列中。队列的管理由内核完成。

消息队列就是以内核管理的一个队列作为交互的媒介。比较常用的一种IPC(重点)

消息队列的使用步骤:
1.使用ftok()或公用的头文件提供一个key.
2.使用msgget(key,...)创建或者获取一个消息队列。
3.使用msgsnd()发送消息(把消息放入队列的尾端),或使用msgrcv()取出消息(把消息从队列中取出)
4.如果确保不再使用消息队列,使用msgctl(IPC_RMID)删除。

消息分为有类型消息和无类型消息。无类型消息可以是任意类型的数据,但是只能先入先出,如果放入次序和取出次序不同,一定会出现问题,更多的时候我们使用有类型消息。

有类型消息必须是一个结构体,
struct 名称(名称程序员随便起)
{
long mtype; //第一个参数必须是long,代表消息类型
数组或结构; //数据区,数据放在这里
};
其中,mtype必须大于0.

int msgsnd(int msgid,void* msgp,size_t msgsz,int flags)

功能:发送消息到消息队列的末尾。
参数:msgid 就是消息队列的ID,
msgp就是消息结构体的首地址
msgsz就是消息结构体中数据区的大小(不带类型)
flags取0或IPC_NOWAIT 0就是存储区满了但是会等待直到有存储区空余,IPC_NOWAIT是满了直接返回-1.
返回:成功返回0,失败返回-1.

ssize_t msgrcv(int msgid,void *msgp,size_t msgsz,long msgtype,int flags)
功能:按类型接收消息
参数:msgid/msgp/msgsz/flags和msgsnd一样
msgtype可以取三种值:
>0 :就是接收特定类型的消息(类型=msgtype)
0 :接收任意类型的消息(先入先出)
<0 :就是接收类型小于等于|msgtype|的消息。从小到大接收。比如:msgtype传了-3,接收消息的类型为1 2 3

返回:失败返回-1,成功返回实际接收的字节数。

 


****************************************************************************

(1)IPC---使用信号量集进行通信
(2)网络通信---网络常识,本地通信,网络通信

 

1.信号量集
1.1 概念
(1)什么叫信号量
信号量就是一个计数器,主要用于控制同时访问资源的进程个数,解决有限资源的分配问题。

(2)什么叫信号量集
信号量集就是指信号量的集合,也就是由多个信号量组成的数组,可以同时控制多种资源的分配问题。

1.2计数器的工作方式
(1)先对计数器进行初始化为最大值
(2)有进程申请资源,计数器减一
(3)如果计数器的值为0,终止进程对资源的申请,申请资源的进程进会阻塞
(4)有进程释放资源,计数器加一
(5)如果计数器的值大于0,阻塞的进程就可以拿到共享资源,直到计数为0时,其他进程继续阻塞。

1.3使用信号量集进行通信的步骤
(1)获取key值,可以直接赋值或使用ftok()生成。
(2)创建/获取信号量集,使用semget()函数
(3)初始化信号量集,给指定的信号量进行初始化,使用semctl()函数
(4)操作信号量集合,对指定的信号量进行 加/减操作。使用semop()函数
(5)如果不再使用信号量集,那么可以删除信号量集,使用semctl()函数

1.4 函数的解析
(1)semget()函数在头文件 #include <sys/sem.h>中,函数原型为:
int semget(key_t key, int nsems, int semflg);
第一个参数就是key值,即通过ftok()得到的那个,第二个参数就是信号量集的大小,也就是信号量的个数,第三个参数就是信号量集的操作方式。IPC_CREAT------不存在则创建,已存在则获取
IPC_EXCL----------如果存在则创建失败
当要获取的时候,第三个参数为0,不存在则失败。
返回值:成功返回信号量集的ID,失败返回-1
函数功能:创建/获取信号量集
注意:当创建一个信号量集时,需要在第三个参数中指定信号量集的权限。

(2)semctl()函数在头文件 #include <sys/sem.h>中,函数原型为 int semctl(int semid, int semnum, int cmd, ...),
第一个参数是信号量集的ID,即semget()获取的那个,第二个参数表示信号量在信号量集中的位置,第一个为0,第三个参数表示具体的操作,包括:

SETVAL----给信号量集中的第semnum个信号量设置值,设置的具体值由arg.val决定。即第四个参数联合中的第一个

IPC_STAT-----将semid指定的信号量集信息拷贝到arg.buf中。
IPC_SET------根据arg.buf中的内容设置给semid指定的信号量集

IPC_RMID-----删除信号量集

第四个参数为下面的联合(可用可不用)

union semun
{
int val; //当第三个参数为SETVAL时使用这个为其赋值
struct semid_ds *buf; //当第三个参数为IPC_SET|IPC_STAT时,通过这个为其赋值
unsigned short *array; /* Array for GETALL, SETALL */ struct seminfo *__buf; /* Buffer for IPC_INFO };


返回值:失败返回-1,成功返回值取决于cmd。

 

(3)semop()函数在头文件#include <sys/sem.h>中,函数原型为
int semop(int semid, struct sembuf *sops, unsigned nsops);
第一个参数是信号量集的id
第二个参数为
struct sembuf
{
unsigned short sem_num; //信号量集中的一个下标
short sem_op; //具体的操作,正是表示增加,负数减少
short sem_flg; //默认为0表示阻塞,也可以是IPC_NOWAIT表示不阻塞
};
第三个参数表示信号量集的大小

返回值:成功返回0,失败返回-1

函数功能:针对semid所指向的信号量集中的nsops个信号量进行具体的操作。

 

——————————————————————————————————————————————————

网络编程


1.网络常识:ISO按照逻辑划分出来7层网络协议:

应用层:和应用程序打交道的,进行数据的交互
表示层:将应用程序中的数据按照规则封装起来
会话层:聊天,对话的意思,控制会话的开始和结束等
传输层:用于数据交换的通道
网络层:通过具体的网络传送数据
数据链路层:对具体的信息进行编码转换等
物理层:路由器的交换机等


2.常见的协议:

TCP:传输控制协议,面向连接的协议,
UDP:用户数据报协议,面向无连接的协议
IP :互联网协议,信息传递的机制


3.IP地址

IP地址---是internet中唯一的一个地址标识,一般都是一个32位的整数,(IPV4),也有128位整数(IPV6)
将IP地址中的每一个字节转换为十进制,采用.隔开,这种IP地址的表示方法叫做:点分十进制表示法

IP地址分为网络号和主机号,将IP地址分成以下四类地址:
A类:0+7位网络地址+24位本地地址
B类:10+14位网络地址+16位本地地址
C类:110+21位网络地址+8位网络地址
D类:1110+28位多播地址


子网掩码采用点分十进制表示法进行表示,主要用于指明一个IP地址中哪些表示网络地址,哪些表示主机地址。不能单独使用,必须和IP地址搭配使用

IP地址 :192.168.182.48
子网掩码:255.255.255.0 &(按位于运算)
__________________________________________________
192.168.182.0 网络地址(48表示主机号)
可以判断两个不同的IP地址是否在同一个子网中

MAC地址也就是物理地址,也就是网卡地址,可以通过绑定Mac地址来限制上网的设备


端口号:
IP地址可以定位具体的主机,但是端口号可以用于定位具体的某个进程
端口号是unsigned short 类型,范围是:0~65535
其中0~1024的端口号一般被系统占用

按照内存地址从低到高依次存放,低位内存地址存放高位数据:大端系统
低位内存地址存放低位数据叫小端系统

字节序有两种,网络字节序和主机字节序
主机字节序一般表示当前主机的字节顺序
网络字节序一般表示不同主机之间的统一字节序

 


*******************************************************************

(1)网络编程
(2)多线程

 

1.网络编程
使用socket进行通信,socket---套结字,实际上是一个逻辑通信载体

(1)一对一通信的模型
服务器端:
(1.1)首先创建socket,使用socket()函数
(1.2)准备一个通信地址,使用结构体类型
(1.3)将socket()和通信地址进行绑定,使用bind()函数
(1.4)进行通信,使用read/write函数
(1.5)关闭socket(),使用close()函数

客户端:
(1.1)创建socket(),使用socket()函数
(1.2)准备一个通信地址,使用结构体类型
(1.3)将socket和通信地址进行连接使用connect()函数
(1.4)进行通信,使用read/write函数
(1.5)关闭socket,使用close函数


2.相关函数介绍

(1)socket()函数
在头文件#include <sys/socket.h>中,函数原型为
int socket(int domain, int type, int protocol);

第一个参数:表示一个域/协议簇,包括以下几个值:
AF_UNIX, AF_LOCAL:---这两个都表示本地通信
AF_INET :---使用IPV4进行通信
AF_INET6:---使用IPV6进行通信

第二个参数:指定协议,包括两个值:
SOCK_STREAM:使用数据流的形式通信,TCP协议
SOCK_DGRAM :使用数据报的形式通信,UDP协议

第三个参数:用来指定一些特殊的协议,一般用不到,设置为0即可。

返回值:成功返回一个文件描述符,错误返回-1

函数功能:创建用于交流的端点,通信载体

 

(2)准备的通信地址类型

a.通用的通信地址
struct sockaddr
{
sa_family_t sa_family;//表示协议簇/域,即socket()函数的第一个参数
char sa_data[14]; //表示一个地址
}
注意:此结构体一般很少直接使用,而绝大多数都是作为函数的参数使用

b.本地通信的结构体类型
#include<sys/un.h>

struct sockaddr_un
{
sa_family_t sun_family Address family; //协议簇
char sun_path[] Socket pathname;//socket文件的路径
};

c.网络通信的结构体类型
#include<netinet.h>
struct sockaddr_in
{
sa_family_t sin_family; //协议簇 AF_INET
in_port_t sin_port;//端口号
struct in_addr sin_addr;//ip地址
};

struct in_addr
{
in_addr_t s_addr;//ip地址
};


(3)bind()函数
在头文件#include <sys/socket.h>中,函数原型为
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数一:socket描述符,socket()函数的返回值

参数二:准备的通信地址

参数三:通信地址的大小

返回值:成功返回0,失败返回-1;

函数功能:将准备的通信地址和socket()进行绑定

 

(4)connect()函数
在头文件#include <sys/socket.h>中,函数原型为
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

函数的功能类似于bind()函数

 

(5)htons()函数
在头文件#include <arpa/inet.h>中,函数原型有下面四个:
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

功能:
htons函数表示将short类型参数的主机字节序转换为网络字节序,通过返回值返回转换之后的结果。

 

(6)inet_addr()函数
在头文件#include <netinet/in.h>,#include <arpa/inet.h>中,函数原型为 in_addr_t inet_addr(const char *cp);

功能:将参数指定的点分十进制形式的ip地址转换网络字节序的整数地址。

 

 

 

 


3.基于TCP的通信协议

服务器:
(1)创建socket,使用socket()函数,使用SOCK_STREAM
(2)准备通信地址,struct sockaddr_in结构体
(3)进行绑定,使用bind()函数
(4)监听,使用listen()函数
(5)接受客户端的连接请求,使用accept()函数
(6)进行通信,使用read/write/send/recv函数,专业的是使用后面两个
(7)关闭socket,使用close()函数

相关函数:

(1)listen()函数,在头文件#include <sys/socket.h>中,函数原型为:
int listen(int sockfd, int backlog);
第一个参数是sockfd描述符,
第二个参数是允许访问的最大连接数,即将连接的队列的最大值
返回值:成功返回0,失败返回-1;
功能:监听socket上的连接请求

(2)accept()函数
在头文件#include <sys/socket.h>中,函数原型为:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
第一个参数:socket描述符
第二个参数:用于保存所接受的客户端的地址
第三个参数:地址的大小
返回值:成功返回新的文件描述符,失败返回-1
函数功能:响应客户端的连接请求
注意:使用socket()函数创建的描述符主要用于等待客户端的连接,不参与信息的交互,而accept()函数返回的描述符主要用于针对当前客户端的信息交互通道

 

(3)char *inet_ntoa(struct in_addr in);
功能是将网络通信结构体中的第三个成员类型的ip地址转换成字符串形式的ip地址


客户端:
(1)创建socket
(2)准备通信地址
(3)进行连接
(4)进行通信
(5)关闭socket

 

 


****************************************************************

 

1.基于UDP通信的模型
2.线程和多线程以及多线程的同步问题

 

1.基于UDP通信的模型

1.1概念
TCP :传输控制协议,面向连接的协议
UDP:用户数据报协议,非面向连接的协议

1.2区别
(1)TCP是一种面向连接的协议,在通信的全程保持连接。
优点:可以保证数据的完整性和安全性以及准确性。并且可以重发一切数据。
缺点:服务器压力比较大,资源占用率比较高

(2)UDP是一种非面向连接的一个协议,在发送数据时连一下,不保持全程连接
优点:服务器的压力比较小,资源占用率比较底
缺点:不保证数据的安全性和完整性以及准确性

1.3基于UDP通信的模型

服务器端:
(1)创建socket,使用socket()函数
(2)准备通信地址,使用struct sockaddr_in结构体
(3)绑定socket和通信地址,使用bind函数
(4)进行通信,使用read/write/sendto/recvfrom函数,更多的是使用后面两个
(5)关闭socket,使用close函数


客户端:
(1)创建socket,使用socket函数
(2)准备通信地址,使用struct sockaddr_in结构体
(3)进行通信,使用read/write/sendto/recvfrom函数,更多的是使用后面两个
(4)关闭socket,使用close函数

 


1.4相关函数的介绍

(1)sendto函数在头文件#include <sys/socket.h>中,函数原型为:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen)

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

第一个参数:socket描述符
第二个参数:将要发送的数据的首地址
第三个参数:发送的数据的大小
第四个参数:发送的方式,默认给0即可
第五个参数:发送到的目标地址
第六个参数:目标地址的大小

返回值:成功返回发送的字节个数,失败返回-1。
函数功能:向指定的目标地址发送数据
注意:send函数相对于sendto少了一个目标地址参数,一般用于TCP通信中

(2)recvfrom()函数,在头文件#include <sys/socket.h>中,函数原型为:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flag
,struct sockaddr *src_addr, socklen_t *addrlen);

第一个参数:socket的描述符
第二个参数:存放数据的缓冲区地址
第三个参数:读取的数据大小
第四个参数:默认给0即可
五个参数:存放客户端的地址
第六个参数:客户端地址的大小

返回值:成功返回读取的数据大小,失败返回-1
函数功能:接受指定的消息

 

 

 


------------------------------------------------

2.线程

2.1线程的概念:
线程--隶属于进程,是进程中的程序流,在操作系统中支持多进程。而每个进程的内部支持多个线程,多个线程并行。

进程是重量级单位,每个进程都需要独立的内存空间等资源,新建进程对于资源的消耗比较大;线程是轻量级的,不需要申请独立的内存等资源,但是每个线程也有一个独立的栈区,线程实际上更多的是共享进程中的资源。

2.2线程的相关函数
(1)pthread_create函数在头文件#include <pthread.h>中,函数原型为:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr
void *(*start_routine) (void *), void *arg);
第一个参数:用于存放线程id的
第二个参数:线程的属性,直接给0即可
第三个参数:线程所调用的函数
第四个参数:给线程调用函数传递的实参。

返回值:成功返回0,失败返回错误编号

函数功能:用于创建一个 新的线程

注意:
(1)在编译链接的时候,需要加上-lpthread
(2)当程序结束时,所有子线程都结束

(3)pthread_join()函数在头文件#include <pthread.h>中,函数原型为int pthread_join(pthread_t thread, void **retval);
第一个参数:指定将要等待的线程ID
第二个参数:用于接收所等待线程的退出码
返回值:成功返回0,失败返回错误编号。
函数功能:等待thread所指向的线程结束。

Compile and link with -pthread.

(4)pthread_self()函数,在头文件#include <pthread.h>中,函数原型为pthread_t pthread_self(void);
功能:获取正在执行的线程id,返回值就是线程的id。编译的时候也应该加上-lpthread 选项。

(5)pthread_exit()函数,在头文件#include <pthread.h>中,函数原型为:void pthread_exit(void *retval);

参数:主要用于返回一个数据。
功能:终止正在调用的线程
exit函数,用于终止一个进程。

2.3关于线程的一个状态
线程主要有两种状态,
(1)非分离状态 -pthread_join
对于非分离状态的线程来说,线程资源的回收,需要等到join函数结束以后

(2)分离状态---pthread_detach
对于分离状态的线程来说,线程结束以后资源马上回收,无法使用pthread_join函数等待的。

(3)pthread_detach()函数在头文件#include <pthread.h>中,函数原型为:int pthread_detach(pthread_t thread);
参数:线程id
返回值:成功返回0,失败返回错误编号

功能:用于分离线程

 

 

 

 

 

 

 

**************************************************************************

(1)线程的取消和同步

1.1 线程的取消:
(1)pthread_cancel函数,在头文件#include <pthread.h>中,函数原型为:int pthread_cancel(pthread_t thread);
编译的时候也应该加上-lpthread选项

参数:要取消的线程ID
函数功能:对thread指向的线程发送取消的请求。
返回值:成功返回0,失败返回错误编号

(2)pthread_setcancelstate函数,在头文件#include <pthread.h>中,函数原型为:int pthread_setcancelstate(int state, int *oldstate);

第一个参数:设置取消状态,有以下两个值:
PTHREAD_CANCEL_ENABLE:可以被取消。
PTHREAD_CANCEL_DISABLE:不可以被取消。
第二个参数:带出原来的取消状态。
返回值:成功返回0,错误返回错误编号。
函数功能:设置线程是否允许被取消。


(3)pthread_setcanceltype函数,在头文件#include<pthread.h>中,函数原型为:int pthread_setcanceltype(int type, int *oldtype);

第一个参数:设置新的类型,包括下面两个值
PTHREAD_CANCEL_DEFERRED:推迟取消,推迟到下一个取消点取消
PTHREAD_CANCEL_ASYNCHRONOUS:立即取消
第二个参数:带出原来的取消类型

返回值:成功返回0,失败返回错误编号
函数功能:设置线程何时被取消。

 

 


1.2 线程的同步

(1)多线程之间共享进程的资源,多个线程同时访问相同的资源时,需要相互协调,以防止出现数据的不一样和不完整的问题,线程之间的协调和通信,叫做线程的同步。


(2)线程同步的思路:访问共享资源时,不能并行,而是串行

(3)线程同步的解决方案:
在线程中,提供了一个互斥量(互斥锁)实现线程的同步

(4)使用互斥量实现线程同步的步骤:

a.定义互斥量:
pthread_mutex_t lock;

b.初始化互斥量:
pthread_mutex_init(&lock,80);

c.使用互斥量进行加锁
pthread_mutex_lock(&lock);

d.使用共享资源

e.使用互斥量进行解锁
pthread_mutex_unlock(&lock);

f.销毁互斥量
pthread_mutex_destroy(&lock);

(5)使用信号量实现线程的同步

信号量就是一个计数器,用于控制同时访问资源的进程/线程数,如果信号量的值是1,等同于互斥量
使用信号量实现线程同步的流程:
a.定义信号量
sem_t sem;
b.初始化信号量
sem_init(&sem,0/*控制线程*/,最大值/*个数*/);
c.获取一个信号量,相当于计数器减1.
sem_wait(&sem);
d.访问共享资源
e.释放一个信号量(相当于计数器加1)
sem_post(&sem);
d.销毁信号量
sem_destroy(&sem);


相关函数介绍:
sem_init()函数在头文件#include <semaphore.h>中,函数原型为:
int sem_init(sem_t *sem, int pshared, unsigned int value);

 

posted on 2015-12-05 13:37  LyndonYoung  阅读(393)  评论(0编辑  收藏  举报