UNIX C 总结

Posted kernel001

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UNIX C 总结相关的知识,希望对你有一定的参考价值。

--day01--
王建立
QQ:2529866769
今天的内容:
一、计算机的框架
什么是操作系统?(汽车)
加油系统 油门 用户跟加油子系统交互的窗口。(接口)
方向系统 方向盘 用户跟方向系统的交互接口。
导航系统
。。。
汽车的操作系统有很多的子系统来完成。这些子系统互相协调工作,达到用户的目的。操作简练、效率高、安全性比较高...。

接口 多个子系统 每个子系统负责单一的工作。
操作环境 驾驶员位置

是不是驾驶员直接控制汽车的硬件?
通过接口间接控制硬件的。 看不见的 透明
框架
组件
操作系统管理硬件并且为上层的app提供服务。
对app的管理。

二、计算机的操作系统
计算机的操作就是管理计算机资源的一款系统软件。(软件资源和硬件资源)

sudo ?

bash是ubuntu操作系统上用户和操作系统的交互窗口程序。

用户需要一个和操作系统交互的环境。这个交互的环境有shell来营造。

补充:
系统调用是函数
库函数
#include <stdio.h>
#include <stdlib.h>
man 3 库函数
man 2 系统调用
man 1 命令
man 命令

编译器

GNU/Linux操作系统
linux是内核
操作系统有unix、linux、windows、macos、ios、。。。
LFS

套件

计算机操作系统包含的内容
1、内存管理
2、文件管理
3、文件系统
4、进程的管理
5、信号
6、 线程的管理
7、进程间通讯
8、网络通讯

三、计算机语言的发展史
32位机器
1000,0000,0000,0000,0000,0001,0000,0010
add 1,2
1000,0000,0000,0001,0000,0011,0000,0001
sub 3,1
把汇编指令翻译成机器指令,这个翻译过程,专业术语称为汇编。
伪指令

main C语言
#include <stdio.h>
#define PI 3.14
# sharp
! bang
& and
@ at
以hello.c为例演示程序从c到可执行文件的过程。
代码参见hello.c

补充:
echo 字符串 将字符串显示到屏幕上
$? 最近指令执行的返回值。
echo $?
框架 组件
第一步:预处理
gcc -E hello.c -o hello.i
预处理指令的处理 文件的包含 宏定义 条件编译
注释去除掉

第二步:需要将预处理后的文件编译成汇编语言文件
gcc -S hello.i -o hello.s

第三步:将汇编语言的文件汇编为机器语言
gcc -c hello.s

第四步:将目标文件和库文件 运行时文件链接,形成可执行文件

四、大型软件的组织

总结:
一、计算机的框架
二、什么是操作系统
三、计算机语言的发展史

 

 

--day02--
回忆昨天内容:
一、计算机的框架
接口

二、计算机操作系统
计算机的操作系统是一款软件。
用来管理计算机资源的软件。
计算机资源 软件资源 硬件资源

GNU/linux操作系统

三、计算机语言的发展史
机器语言、汇编语言、C语言
预处理、编译、汇编 、链接
今天内容:
一、大型软件组织
定义规则,规则就是协议。用户和中介的接口。
软件开发的过程:
定义规则,定义协议。软件为用户提供的功能。
在C语言中使用接口实现。接口就是函数。
两种用户 函数的使用者 函数的实现者
你要站在函数的实现者角度考虑函数。
1、函数要完成的功能
2、需要传递给函数的参数
3、函数的返回值的类型
附加信息
作者:
时间:

将协议的每个部分都要实现,在C语言中如何来表示协议。
头文件
为了避免重复包含,需要使用头文件卫士。

#ifndef 宏
#define 宏
/*文件的包含*/
/*宏定义*/
/*类型的定义*/
/*函数的声明*/
/*变量的声明*/
#endif
什么叫变量的声明?
变量的声明就是变量作用域的扩充,并不为这个变量分配空间。变量的空间已经在其他文件中分配。

变量的定义是为这个变量分配空间的。

 


头文件中协议的实现者 源程序
头文件中协议的使用者 源程序 main

举例说明 软件开发的组织 代码参见 tmath文件夹。

在工程中,编译的基本单位是源文件。

如何将多个源文件拼接成一个可执行文件? 链接的过程
nm 二进制文件
用来查看二进制文件中包含的符号。
什么是符号?
函数的名字 全局变量的名字 静态局部变量
[email protected]:~/1706/UC/day02/tmath$ nm add.o
00000000 T t_add
0000000e T t_sub

[email protected]:~/1706/UC/day02/tmath$ nm mul.o
0000000c T t_div
00000000 T t_mul

[email protected]:~/1706/UC/day02/tmath$ nm test.o
00000000 T main
U printf
U t_add
U t_div
U t_mul

链接发生在程序的编译阶段。这种链接称为静态链接
有一种链接发生在代码加载到内存执行的时候,这种链接称为动态链接。

运行时文件
*.o

 

#include <>和“”?
<> 在系统指定的路径下找头文件
“” 首先在当前路径下找,如果没有找到,再到系统指定的路径下找。
什么是系统指定的路径?如何知道系统的指定路径?
在编译的过程中使用 -v参数。
这个参数的意思就是将这个编译链接的过程中产生的信息输出到显示器。

二、复习C语言 (指针、类型别名)
1、变量和常量
不管是变量还是常量,在程序执行的过程中,都占用内存空间。
变量内存空间里的值是可以改变的,常量空间里的值是不能被改变的,是只读的。
const int a=3;
int *p=&a;
*p=5;

2、数据类型
什么是数据类型?
数据类型是在内存中占用的字节数。再者就是这些数据从内存中取出来的组织方式。
int var_a=321;
char var_c=‘v‘;
short
long
float
double
void
* 指针不是地址
int *p;
举例说明 指针的理解 代码参见point.c

gcc的参数:
-Wall 显示所有的警告信息
-Werror 显示所有的错误信息,警告也作为错误处理。

int *p;
int **p;
指针和一维数组的关系
int arr[3];
arr是一个数组类型常量。
arr是数组的名字。 这个数组里有三个元素。每个元素都是int类型的。
三个元素的地址空间是连续的。
int *p=arr;
p++;
char *q=arr;
q++;
char *p[3]; p是数组的名字,数组里的每一个元素都是指针类型的,这就是指针数组
int main(int argc,char *argv[]){

}

二维数组
int arr[2][3]={{1,2,3},{4,5,6}};
arr[0] 和arr
int[3] 类型
int (*p)[3]; p是指针类型的变量
int [3]; p+1 数组指针。

p=arr arr是数组的名字,常量
p是指针类型的变量
函数和指针的结合
void *malloc(size_t size);
int *p=(int *)malloc(1024);

int (*fun_t)(int,int);
fun_t 是指针类型的变量
int (int,int)是指针类型变量的访问方式。

int t_add(int,int);

fun_t=add;

int (*fun_t[4])(int,int);
fun_t 数组的名字 数组里有4个元素。每个元素都是指针类型的。

代码参见 tmath文件夹中的point.c
数据的高字节存放在内存的低地址中。大端 主机字节序

作业:编写代码,测试自己的主机字节序。


3、运算符的优先级及其结合性
4、变量的作用域和生命周期
int *p;
int **p;
int *p[3];
int (*p)[3];
int *f(void);
int (*f)(void);
int (*f[4])(void);

int char
int arr[3];
int *p=arr;
*(p+4)

今天的内容:
一、大型软件的组织
协议 接口
头文件
并行开发
源文件是编译的基本单位
链接 静态链接
运行时文件
二、c语言指针

 

 


--day03--
回忆昨天内容:
一、大型软件的组织
二、复习C语言的指针
数据类型

今天的内容:
一、为类型命名别名
struct node{
int num;
struct node *next;
};
typedef struct node node_t;
node_t n;

使用typedef为类型定义别名。
三个步骤:
1、定义一个类型的变量或常量
2、在第一步的基础上,在最前边加上typedef关键字,这时候,原来的变量或常量就是类型的别名(新类型)。
3、使用新类型定义变量或常量。

举例说明:
1、为int类型定义别名,count_t;
typedef int count_t;
count_t c;
2、在系统中经常会出现下边情况
uint32_t;
typedef unsigned long uint32_t;
typedef unsigned short uint16_t;
3、为指针类型定义别名
char *str_t;
变量 str_t 类型 *
typedef char *str_t;
str_t s,r;=====char *s,*r;
4、为一个结构体类型定义别名
struct node{
int num;
struct node *next;
};
typedef struct node node_t;
typedef node_t *list_t;
list_t h;
list_t *n;

5、为函数类型定义别名
int (*fun_t)(int,int);
fun_t 是变量
typedef int (*fun_t)(int,int);
fun_t 是类型的别名 指针类型 访问方式是int(int,int);

typedef int t_add(int,int);
t_add 类型的别名 int (int,int);
t_add *f;
以昨天代码为例,演示类型的别名

6、为数组类型命名别名
int arr_t[3];
arr_t是常量
int [3] 类型
typedef int arr_t[3];
arr_t是类型的别名 int [3]
arr_t a; a是什么? a是一个一维数组。
arr_t b[2];
验证数组的别名。代码参见 arr_alias.c

回调函数

二、环境变量
什么是环境变量?
操作系统为系统上运行的程序需要提供一个环境,在这个环境中有些资源的访问需要使用变量的形式提供,这就是环境变量。

bash交互窗口的环境变量。
程序和进程的区别?
程序是指令的集合。静态的。
进程是程序运行的实例。程序加载到内存运行起来,才是进程。
一个程序运行一次产生一个进程。每个进程都有自己的一个身份证号。这个编号专业术语称为PID。
环境变量是可以被子进程继承的变量。?
如何察看bash下的环境变量?
env
环境变量的格式:
name=value
切记等号的左右两边不允许出现空格

USER=tarena
如何访问环境变量的值?
$USER

echo $USER
bash下有两种变量 自定义类型的变量 环境变量
自定义类型的变量不能被子进程继承?
而环境变量是可以被子进程继承的。?
如何为自定义变量赋初值?
NAME=beijing 自定义变量也就是当前bash私有的变量。
将自定义变量转换为环境变量
export 变量的名字

PATH PS1

PS1 这个环境变量是提示符资源的环境变量。
通过设置PS1环境变量就可以改变bash的提示符
export PS1="\W\$"

《鸟哥私房菜》
在bash启动的时候,会首先执行一个脚本程序。这个脚本程序中可以配置bash的环境变量。
文件的名字是~/.bashrc

在文件的末尾增加一行,内容是:
export PS1="\W\$"

PATH:
察看PATH的内容:
UC$echo $PATH
/home/tarena/workdir/android2.3/android-source/jdk1.6.0_16/bin:/bin:/usr/bin:/usr/X11R6/bin:/usr/local/bin:/home/tarena/workdir/linux-x86/sdk/android-sdk_eng.root_linux-x86/tools

以冒号作为分隔符,有多条路径。
export PATH=$PATH:.

三、静态库文件的制作和使用
静态库文件的命名 lib库名.a
函数库分为两种。静态库和动态库
静态库的制作和使用步骤(tmath文件夹)
1、需要将添加到静态库的文件编译为目标文件
tmath$ls
add.c mul.c process.c t_math.h
tmath$gcc -c *.c
tmath$ls
add.c mul.c process.c t_math.h
add.o mul.o process.o
2、将第一步生成的目标文件打包到静态库文件中
tmath$ar -r libpmath.a *.o
ar: creating libpmath.a
tmath$ls
add.c libpmath.a mul.o process.o
add.o mul.c process.c t_math.h
3、使用静态库文件链接源文件生成可执行文件
gcc point.o -Ltmath -lpmath -o p

补充:
gcc的参数
-I路径 将路径添加到系统的指定的路径中。(搜索头文件的指定路径中)

-L路径 这个路径是库文件所在的路径
-l库名


四、动态库的制作和使用
动态库文件的命名 lib库名.so

动态库的制作和使用步骤:
1、将需要加入到动态库的文件编译成目标文件。(与位置无关的目标文件)
tmath$gcc -c -fPIC *.c
tmath$ls
add.c mul.c process.c t_math.h
add.o mul.o process.o

2、将生成的目标文件打包到动态库文件中
tmath$gcc -shared -o libpmath.so *.o
tmath$ls
add.c libpmath.so mul.o process.o
add.o mul.c process.c t_math.h

3、将源程序链接动态库文件,生成可执行文件。
day03$gcc point.c -Ltmath -lpmath -Itmath

day03$a.out
a.out: error while loading shared libraries: libpmath.so: cannot open shared object file: No such file or directory

链接的时候 加载的时候
连接器 加载器
问题的原因是加载器找不到libpmath.so文件
a.out 程序依赖于动态库libpmath.so
如何察看你的可执行文件依赖哪些动态库?
ldd 可执行文件的名字

需要加载器找到这个库文件。
1、使用环境变量告诉加载器动态库的搜索路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:tmath

2、加载器和链接器都有默认路径。/lib或者/usr/lib

动态库和静态库的区别
使用静态库生成的可执行文件,静态链接的。可执行文件不再依赖于静态库。
使用动态库生成的可执行文件,动态链接的。可执行文件依赖于动态库。

动态链接是发生在程序加载到内存中的时候发生链接。
动态库也称为共享库。

可执行文件和动态库文件有效的隔离。
总结:
一、为类型定义别名
二、环境变量
三、静态库的制作和使用
四、动态库的制作和使用

 


--day04--
回忆昨天内容:
一、使用typedef为类型命名别名。
数据类型

二、环境变量
特别要注意的就是环境变量的格式
name=value

PATH 环境变量
LD_LIBRARY_PATH
三、静态库的制作和使用
四、动态库的制作和使用
今天的内容:
一、程序中的错误处理
在系统中定义了一个全局变量errno。在这个全局变量中存放着系统调用或者库函数出错的信息(错误编号)。然后根据错误编号获取错误信息。

举例说明: 打开一个文件,如果这个文件不存在,查看错误信息。 代码参见 file.c

perror(3) strerror(3)
#include <stdio.h>
void perror(const char *s);
功能:输出一条系统的错误消息
参数:
s:用户给定的字符串。
返回值:
不用返回。

补充:
FILE *stdin; 标准输入 键盘
FILE *stdout; 标准输出 显示器
FILE *stderr; 标准错误输出 显示器

#include <string.h>
char *strerror(int errnum);
功能:返回错误编号对应的描述信息
参数:
errnum:指定错误编号
返回值:
错误号对应的错误描述信息
如果错误号没有定义,返回Unknown error nnn。

二、GDB调试工具的使用
gcc gdb bash makefile
如何使用gdb调试工具调试程序?
1、在编译链接程序的时候,加上-g/-ggdb参数。编译输出的可执行文件中包含调试信息。
tmath$ls
add.c mul.c point.c test.c t_math.h
tmath$gcc add.c mul.c test.c
tmath$ls
add.c a.out mul.c point.c test.c t_math.h
tmath$ls -l a.out
-rwxrwxr-x 1 tarena tarena 7332 8月 3 11:19 a.out

tmath$gcc add.c mul.c test.c -g
tmath$ls -l a.out
-rwxrwxr-x 1 tarena tarena 9580 8月 3 11:20 a.out

2、使用gdb调试工具对带有调试信息的可执行文件进行调试
gdb a.out

gdb调试命令
l list 列出程序清单
b 函数的名字或者行号 breakpoint 设置断点
r run 执行程序
p 变量名 输出变量的值
n next 下一条
s step 下一步
q quit 退出调试

int a,b,c;
a=b=c=3;
函数的参数。指针类型的变量作为函数的参数。
值-结果参数。举例说明 代码参见 value.c

三、动态加载
在程序中,根据程序的需要动态加载某个库函数,这种行为称为动态加载。
系统为实现动态加载提供了以下函数:
dlopen(3)
#include <dlfcn.h>
void *dlopen(const char *filename,int flag);
功能:加载一个动态库文件,返回一个地址
参数:
filename:指定了动态库的文件名字
flag:
RTLD_LAZY:懒加载
RTLD_NOW:立即加载
返回值:
NULL 失败


char *dlerror(void);
功能:获取dlopen、dlclose、dlsym函数产生的错误。
参数:
void

返回值:
返回一个字符串。这个字符串描述了错误产生的原因。


void *dlsym(void *handle,const char*symbol);
功能:在内存查找动态库中的symbol加载到内存的地址。
参数:
handle:dlopen(3)的返回值。指定了要操作的库函数
symbol:指定了要找符号。
返回值:
NULL 代表错误
返回symbol加载到内存的地址。


int dlclose(void *handle);
功能:将与handle相关的动态库文件的引用计数减1。库文件的引用计数减到0的时候,动态库从内存中卸载。移除
参数:
handle:指定了要关闭的动态库。是dlopen(3)的返回值
返回值:
非0 错误
0 成功

Link with -ldl.

举例说明 动态加载的使用。将libpmath.so动态库文件加载到内存,并使用函数库中的函数。

代码参见 dynamic.c


四、内存管理

寄存器
cache
内存
硬盘

页 页表 页框

虚拟内存 物理内存 虚拟地址空间 物理地址
演示段错误 segment.c


总结:
一、系统中的错误处理
errno perror(3) strerror(3)

二、gdb调试工具的使用
三、动态加载
按需加载 dlopen(3) dlclose(3) dlsym(3)

四、内存管理基础
页 页表 页框
物理内存 虚拟内存 虚拟地址 物理地址
在有操作系统的机器上,cpu能看到的是虚拟地址


--day05--
回忆昨天内容:
一、系统中的错误处理
errno 全局变量 最近一个系统调用或者库函数错误产生错误码
可以根据错误码找到错误的原因。
perror(3) strerror(3)
二、gdb调试器的使用 入门级
要调试的可执行文件必须带有调试信息。

三、动态加载
按需加载 dlopen(3) dlerror(3) dlsym(3) dlclose(3)
四、内存管理的基础

计算机的存储体系结构
寄存器 cache 内存 硬盘 网络

虚拟内存 物理内存 虚拟地址空间 物理地址

页 页框 页表

今天内容:
一、内存管理(续)
每个进程都有自己独立的4G的虚拟地址空间。
冯.诺伊曼体系结构
哈佛体系结构
section 代码段 只读数据段 栈段

举例说明 数据所属的段。
代码参见 memory.c
如何获取进程自己的pid?
getpid(2)
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
功能:获取进程的id
参数:
void
返回值:
pid_t 当前进程的pid


cat /proc/pid/maps

初始化和赋值是两码事

栈段和代码段 数据段

栈段 栈段是一个容器,栈段中有多个栈帧。
一个进程只有一个栈段。

每个函数都有自己的栈帧,在函数结束的时候,函数的栈帧释放。
函数中变量的空间分配在栈帧上的,变量的空间释放了。
变量的生命结束了。
什么样的变量的空间分配在栈帧上?
函数中的自动局部变量和函数的形参的空间分配在栈帧中。 函数

静态的局部变量和全局变量的空间分配在数据段。进程
但两者的作用域不同。
stack
heap
代码段 数据段 堆、 栈
堆的生命周期 要么程序员手动释放堆空间。否则,进程结束的时候。

 


二、使用mmap将物理地址映射到进程的虚拟地址空间。

mmap(2)
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
功能:将文件或者设备映射到内存
参数:
addr:指定了映射区域的起始地址。NULL 地址有内核决定

length:指定了映射区域的长度
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:
2选1:
MAP_SHARED:对映射区域的更新给其他进程看,也同步到下层的文件。

MAP_PRIVATE:对映射区域的更新不给其他进程看。也不同步到下层的文件

MAP_ANONYMOUS:映射区域的内容被初始化为0.不与任何文件相关。
如果指了这个参数。fd和offset被忽略。

fd:文件描述符(文件)
offset:文件的起始位置(文件)
返回值:
MAP_FAILED 错误 errno被设置
返回映射区域的首地址。


int munmap(void *addr, size_t length);
功能:解除文件到内存的映射
参数:
addr:mmap(2)的返回值
length:指定了映射区域的长度。
返回值:
0 成功
-1 失败 errno被设置

补充:
一切皆文件。
举例说明,使用mmap将物理地址映射到进程的虚拟地址空间,然后对虚拟地址空间进行操作。代码参见mmap.c

 

总结:
一、进程的映射
代码段 数据段 堆 栈
周六日 复习C语言
二、使用mmap将物理地址映射到进程的虚拟地址空间

 

--day06--
回忆上次课程内容:
一、内存管理
代码段、数据段、堆、栈(栈段、栈帧)

代码段、只读数据段、数据段、bss段(未初始化的数据段)、堆、栈。

虚拟内存、物理内存、虚拟地址、物理地址。
页、页框、页表

二、使用mmap将物理地址空间映射到进程的虚拟地址空间

char name[12];
char *n;

sizeof(n)? 4 *
sizeof(name)? 12 char[12];

今天内容:
一、malloc的实现机制(缓冲机制)
库函数跟系统调用之间的关系
什么是缓冲?
内存分配的原理。

封装 函数A的实现代码中调用了函数B。函数B的功能是函数A主要的功能,这样就说函数A封装了函数B。

举例说明 malloc的实现机制(缓冲) 代码参见malloc.c
080f8-08119
33块

内存管理结束了

二、文件的操作(文件内容的操作)

文件的打开、关闭、读、写、读写位置的重定位
标c中 fopen(3) fclose(3) fread(3) fwrite(3) fseek(3)...
系统调用 open(2) close(2) read(2) write(2) lseek(2)
open(2)
#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 open(const char *pathname, int flags,...);
功能:打开一个文件,有可能创建一个文件
参数:
pathname:指定要打开的文件的名字
flags:

三选一:
O_RDONLY:只读
O_WRONLY:只写
O_RDWR:可读可写

另外,可以按位或0个或多个创建标记和状态标记
O_CREAT: 如果文件不存在,创建文件。第三个参数mode指定了新文件的权限。新文件的权限为mode & ~umask

O_EXCL:如果和O_CREAT在flags中被一起指定,文件存在,报错。

O_NOCTTY:
O_TRUNC:打开的文件存在,并且是一个普通文件,有写的权限,那么文件内容清空

O_APPEND:以追加方式打开文件,文件的都写位置被定位在文件的末尾。


...:可变参数.可变参数的类型、个数取决于可变参数前边的那个参数。
返回值:
返回一个新的文件描述符
-1 错误 errno被设置

补充:

文件描述符 小的 非负整数?
文件描述符是当前进程没有打开的、最小的文件描述符。

进程 程序运行的实例。每个进程都有自己的pid。每个进程还有自己的户口本,专业术语称作PCB (process control block)

PCB中记录了进程使用到的资源。(软件资源 硬件资源)
struct task{

};
有一个成员记录了,这个进程打开的文件。

进程默认从bash继承0 1 2三个文件描述符
0 标准输入 键盘 STDIN_FILENO
1 标准输出 显示器 STDOUT_FILENO
2 标准错误输出 显示器 STDERR_FILENO

文件的类型
linux下一切皆文件。
-rw-rw-r-- 1 tarena tarena 664 8月 7 11:04 malloc.c

- 普通文件
d 文件夹类型的文件
c 字符设备文件
b 块设备文件
s socket文件
p 管道文件
l 软链接文件

权限的问题
rw-rw-r-- 文件的权限
linux是一款多用户多任务的操作系统
所谓的多用户就是多个用户可以同时登陆操作系统,在操作系统上维护自己的工作。

老大 root
为每个用户指定了用户的地盘 用户工作主目录(家目录)~
把用户分成很多的组。一个用户可以属于多个组。一个用户组包含多个用户。

r read
w write
x execute

文件的拥有者 属主 u 属组 g
其他人 o
所有的用户 a

改变文件的权限
chmod 权限 文件的名字
权限的构成
u+x
u-x
g-w
g+w

可以使用数字表示权限。8进制的数据

0664


umask是什么玩意?
umask是权限掩码

umask
0002
-------w-

创建文件的时候,将掩码中出现在权限位去除。

创建普通文件的时候。权限默认为是0666。
rw-rw-rw- 默认指定的权限 rw-rw-rw-
-------w- umask rwxrwxr-x
rw-rw-r-- 最终的权限 rw-rw-r--

mode&~umask

0033

----wx-wx
rw-rw-rw-
rwxr--r--
rw-r--r--

close(2)
#include <unistd.h>
int close(int fd);
功能:关闭一个文件描述符
参数:
fd:指定要关闭的文件描述符
返回值:
-1 错误 errno被设置
0 成功

举例说明
1、以只读方式打开文件。代码参见r_file.c
注意:为了方便对文件的操作,将文件操作中使用的头文件进行封装。p_file.h

2、以读写方式打开文件,文件不存在,创建文件,权限指定为0644.文件存在,报错,文件已经存在。
代码参见 rw_file.c

 

3、以读写方式打开文件,文件不存在创建文件,指定文件的权限为0664。文件存在,将文件内容清空
代码参见 rw1_file.c

read(2)/write(2)
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
功能:从文件中读取数据
参数:
fd:指定了具体的文件,从这个文件里读取数据
buf:将读取的数据存放到buf指定的地址空间里
count:向系统申请的要读取的字节数
返回值:
返回的是实际读取到的字节数。0到达了文件的末尾。
-1 错误 errno被设置


#include <unistd.h>
ssize_t write(int fd,const void *buf,size_t count);
功能:向文件写数据
参数:
fd:指定了具体的文件
buf:指定了内容存放的地址
count:指定了写入文件的字节数
返回值:
-1 错误 errno被设置
返回写入文件中的字节数

举例说明 文件操作系统调用的使用 编写代码实现cat命令的功能。编译成可执行文件的名字为pcat
代码参见pcat.c

总结:
一、malloc的实现原理 缓冲机制
二、文件的操作
open close read write


三、使用mmap将文件映射到进程的虚拟地址,通过对内存里数据的修改,直接修改文件的内容。

--day07--
回忆昨天内容:
一、malloc内存分配的原理(缓冲机制)
封装

二、文件的操作
open close read write

文件描述符 权限 权限掩码 文件的类型
pid PCB

作业:
编写代码实现cp命令的功能。
cp 源文件 目标文件
编译生成可执行文件pcp

今天的内容:
一、lseek(2)重新定位文件的读写位置。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
功能:重新定位文件读写位置距离文件起始的偏移
参数:
fd:指定了具体的文件
offset:偏移
whence:
SEEK_SET:offset就是文件的偏移位置 指向文件的头部
SEEK_CUR:代表当前位置 当前位置+offset
SEEK_END:文件的大小+offset 文件的尾部
返回值:
-1 错误 errno被设置
返回的是位置距离文件起始的字节数。
举例说明 lseek(2)的使用.代码参见 lseek.c

补充:
day07$od -tx1 -tc hello
0000000 68 65 6c 6c 6f 0a
h e l l o \n
0000006

day07$vi hello
day07$od -tx1 -tc hello
0000000 68 65 6c 6c 6f 0a 77 6f 72 6c 64 0a
h e l l o \n w o r l d \n
0000014

二、使用mmap将文件映射到进程的虚拟地址空间,然后对内存的操
作直接反应到文件中。
将文件hello映射到内存,在内存中对文件的内容进行修改,改变实际文件的内容。
代码参见 mmap_file.c

三、文件的重定向
什么是文件的重定向?
我们讲述的文件是流式文件。操作的都是文件流。文件的重定向就是改变文件的流向。文件操作有两种流,输入流和输出流。
文件重定向分为文件输出重定向和文件输入重定向。
文件输出重定向。
完成文件描述符复制的两个系统调用
dup(2) dup2(2)
#include <unistd.h>
int dup(int oldfd);
功能:复制文件描述符
参数:
oldfd:源文件描述符
返回值:
-1 错误 errno被设置
返回最小的、没有被使用的文件描述符

int dup2(int oldfd, int newfd);
功能:复制文件描述符
参数:
oldfd:源文件描述符
newfd:目的文件描述符
返回值:
-1 错误 errno被设置
成功返回目的文件描述符

举例说明 使用dup(2)和dup2(2)实现文件的输出重定向。
代码参见 direct.c


四、文件锁的使用
文件锁分为两种 读锁(共享锁) 写锁(互斥锁)
文件对锁的实现分为两类 建议锁 强制锁
如何对文件加锁?
使用系统调用fcntl(2)对文件进行加锁。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
功能:操作文件描述符
参数:
fd:指定要操控的文件描述符
cmd:指定了操作文件描述符的命令
建议锁使用的命令:
F_GETLK:用来测试是否可以加锁,如果可以加锁,返回F_UNLCK。
不可以加锁,在字段l_pid中保存了一个pid。是hold着这把锁的进程的pid。

F_SETLK:为文件描述符设置锁。如果有互斥锁,其他进程hold着互斥锁,立即返回-1,错误,errno被设置

F_SETLKW:和F_SETLK一样,但是如果想要加的锁和文件已有记录锁冲突,进程阻塞等待其他进程对记录锁的释放。

...: 可变参数,参数的类型和个数取决于cmd。
struct flock *
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) */
...
};

返回值:
0 成功
-1 错误 errno被设置

举例说明文件锁的使用
有两个进程PA和PB,PA进程对文件加读锁,测试PB进程对同一个文件也加读锁,是否成功?
代码参见 PA.c PB.c

测试文件锁是否能添加。代码参见PC.c

五、库函数和系统调用之间的关系
以文件操作为例。
fopen fclose fputc fgetc 库函数
open close read write 系统调用

举例说明 代码参见 file.c

FILE 是结构体类型的别名

flags fileno mode

fopen(3)
首先调用open(2),打开一个文件,将open(2)的返回值记录在FILE结构体中的_fileno成员。分配一块内存,这块空间叫文件内容缓冲区。然FILE中的一些指针成员指向这块内存。最后返回FILE类型的对象。

fputc(3)
调用fputc的时候,向文件内容缓冲区写数据。如果缓冲区没有空间接纳这个字符,调用write(2)将缓冲区的数据写入到文件中,然后再将fputc中的数据写入到缓冲区。如果缓冲有空间接纳数据,直接将数据写入缓冲区即可。

fgetc(3)
调用fgetc的时候,首先从缓冲区读取数据,如果缓冲区有数据,理解返回读取到的字节。如果缓冲区没有数据,调用read(2)从文件中读取数据 到缓冲区,然后fgetc了获取到数据才返回。


fclose(3)
首先刷新缓冲区,将缓冲区内存写入到文件中,然后调用close(2)关闭文件描述符,最后释放缓冲区。

库函数操作文件称为缓冲文件。
系统调用操作文件称为非缓冲文件

库函数可以跨平台 系统调用不能跨平台

总结:
一、lseek的使用
二、使用mmap将文件映射到内存,对内存的操作直接反应到文件中
三、文件输出重定向
四、文件锁的使用
五、库函数和系统调用之间的关系

 

--day08--
回忆昨天内容:
一、lseek(2)的使用
实现文件读写位置的重定位

二、使用mmap映射文件到内存,在内存中的操作,直接反应到文件里。
三、文件输出重定向的实现
dup(2)
dup2(2)

四、文件锁的使用
建议锁
写锁 读锁

五、库函数和系统调用之间的关系
fopen fclose fputc fgetc
open close read write

缓冲文件 非缓冲文件

今天的内容:
一、获取文件的元数据(meta data)
通过read write可以对文件的内容进行读写。
但是今天我们要操作的是文件的元数据(文件的属性信息)
day08$ls -l hello
-rw-rw-r-- 1 tarena tarena 0 8月 9 09:17 hello
文件的类型
文件的权限
文件的硬链接数
属主
属组
文件的大小
时间

文件的链接
分为两种 硬链接和软链接

每个文件有且仅有一个自己的inode。
硬链接的两个文件有同一个inode。
如何为一个文件创建硬链接?
ln 源文件 链接文件

如何创建软链接文件?
ln -s 源文件 链接文件

如何使用程序获取文件的元数据?
使用stat(2)获取文件的元数据
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
功能:获取文件信息
参数:
path:指定了文件的名字。
buf:用来存储这个文件的元数据。
返回值:
0 成功
-1 错误 errno被设置

struct stat 这个结构体中有哪些成员,需要了解
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection*/
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /*blocksize for file system I/O */
blkcnt_t st_blocks; /*number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
举例说明 获取文件的元数据 代码参见 pstat.c

时间的问题。系统记录的是从1970年1月1日0:0:0开始的一个秒数。需要将这个长整型的数转换为字符串格式。
ctime(3)
#include <time.h>
char *ctime(const time_t *timep);
功能:将时间的整数值转换为字符串格式
参数:
timep:指定要转换的长整型的数。
返回值:
NULL 错误
非空 转换后的字符串的首地址

关于用户的问题,用户看到的是文件的名字,而系统管理中使用的是用户的id,数字。
在程序中输出的是数字,但是需要转为字符串格式,方便用户的使用。那如何根据用户的id,找到用户的名字?

使用到函数getpwuid(3)
#include <sys/types.h>
#include <pwd.h>
struct passwd *getpwuid(uid_t uid);
功能:过去passwd文件中的一条记录
参数:
uid:指定用户的uid

返回值:
指向匹配uid的记录的首地址
NULL 没有找到匹配的记录或者错误的产生。
如果是错误产生 设置errno
struct passwd {
char *pw_name; /* username */
char *pw_passwd; /* user password */
uid_t pw_uid; /* user ID */
gid_t pw_gid; /* group ID */
char *pw_gecos; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};

在linux系统中用户的信息存放在/etc/passwd文件中
root:x:0:0:root:/root:/bin/bash
tarena:x:1000:1000:tarena,,,:/home/tarena:/bin/bash
以:分隔的七列
第一列:用户的名字
第二列:是否有密码? x有密码
第三列:用户的id。 uid
第四列:用户的组id。 gid
第五列:用户的注释信息
第六列:用户的工作主目录
第七列:用户登陆成功启动的程序

需要将gid转换为组名
getgrgid(3)
#include <sys/types.h>
#include <grp.h>

struct group *getgrgid(gid_t gid);
功能:获取组文件的一条记录
参数:
gid:指定用户组的id
返回值:
NULL 没有找到匹配的记录或者错误产生 如果错误产生errno被设置

返回一个地址

struct group {
char *gr_name; /* group name */
char *gr_passwd; /* group password */
gid_t gr_gid; /* group ID */
char **gr_mem; /* group members */
};

文件权限的处理
S_ISREG(m) is it a regular file?
S_ISDIR(m) directory?
S_ISCHR(m) character device?
S_IRWXU 00700 mask for file owner permissions
S_IRUSR 00400 owner has read permission
S_IWUSR 00200 owner has write permission
S_IXUSR 00100 owner has execute permission

st_mode& S_IRWXU 得到的是文件拥有者对文件的权限。
000,111,000,000
000,100,000,000
000,010,000,000
000,001,000,000


作业:编写代码实现ls -l filename的功能。
编译生成可执行文件pls。

二、文件夹的操作
linux操作系统下一切皆文件。文件夹也是文件的一种。
drwxrwxr-x 2 tarena tarena 4096 8月 9 14:57 dir
对文件夹读写操作。
什么是文件夹的内容?
文件夹的内容,就是文件夹里的文件或文件夹。

rwx
r 读
w 写
x 通过
使用程序来访问文件夹的内容。
opendir(3) closedir(3) readdir(3)
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
功能:打开一个文件夹
参数:
name:指定了要打开的文件夹的名字
返回值:
NULL 错误 errno被设置
返回一个指针,指向文件夹流的指针。

DIR FILE

closedir(3)
#include <sys/types.h>
#include <dirent.h>

int closedir(DIR *dirp);
功能:关闭文件夹
参数:
dirp:opendir(3)的返回值
返回值:
-1 错误 errno被设置
0 成功

readdir(3)
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
功能:从文件夹读取数据
参数:
dirp:opendir(3)的返回值。
返回值:
返回一个地址
如果到达了文件的末尾,NULL被返回,errno不变
如果错误产生,NULL被返回,errno被设置

struct dirent{
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen; /*length of this record */
unsigned char d_type; /* type of file; not supported
by all file system types */
char d_name[256]; /* filename */
};

举例说明 编写代码实现浏览指定文件夹下的所有文件
代码参见 dir_op.c

position

作业的补充:
如果filename是普通文件,将普通文件的元数据输出。
如果filename是文件夹,将文件夹里的所有的文件的元数据输出

三、文件操作的杂项

getcwd(3)
access(2)
chdir(2)


mkdir(2)
umask(2)
rmdir(2)
unlink(2)
link(2)
symlink(2)
rename(2)
remove(3)
chmod(2)
到此 ,文件和文件系统结束了。
进程管理

总结:
一、获取文件的元数据
链接文件
用户 属组
二、文件夹的操作
权限 文件夹的内容
三、文件操作的杂项

 

--day09--
回忆昨天内容:
一、获取文件元数据
二、文件夹的操作
三、文件操作的杂项

今天的内容:
进程的管理
一、进程的基础
进程和程序的区别
每个进程有自己的pid、PCB
操作系统上运行的所有进程构成一颗树。
如何查看这颗树?
pstree(1)
树根进程是init pid是 1

进程间的亲缘关系两种 父子关系 兄弟关系

使用top(1)命令观察进程的状态
使用ps(1)命令也可以查看进程。

二、进程的创建
父进程创建子进程

父进程如何去创建子进程?
父进程调用fork(2),创建新的子进程。
#include <unistd.h>
pid_t fork(void);
功能:创建一个子进程
参数:
void:
返回值:
在父进程中 -1 错误 errno被设置
成功 在父进程中 子进程的pid被返回
在子进程中 0 被返回

写时复制技术

举例说明 使用fork(2),创建新的子进程
代码参见 fork.c

三、进程的退出
1、注意return和exit(3)的区别
return只是从函数返回。结束函数的生命周期。
exit(3)结束进程的生命周期

exit(3)
#include <stdlib.h>
void exit(int status);
功能:使进程正常终止
参数:
status:指定进程的退出状态码。
返回值:
返回这个值 status & 0377给父进程
举例说明 exit(3)的使用
代码参见 exit.c

目前,在bash下执行的程序,这个进程的父进程是bash。

2、可以使用atexit(3)或者on_exit(3)向进程注册函数,在进程退出的时候调用这写注册的函数。
从main函数中返回的时候,进程结束了吗?
进程没有结束。

atexit(3)
#include <stdlib.h>
int atexit(void (*function)(void));
功能:向进程注册函数,在进程退出的时候被调用
参数:
function:指定遗言函数

返回值:
0 成功
非0 失败

注意:
1、同一个函数注册一次就被调用一次,注册多次就被调用多次。
2、函数注册的顺序和调用的顺序相反
3、注册的遗言函数被子进程继承

void (*function)(void)

举例说明 使用atexit向进程注册遗言函数
代码参见 atexit.c

on_exit(3)
#include <stdlib.h>
int on_exit(void (*function)(int , void *), void *arg);
功能:注册一个函数在进程终止的时候被调用
参数:
function:指定了遗言函数的名字
arg:指定了function函数的第二个参数,function的第一个参数是exit(3) 的退出状态码。

返回值:
0 成功
非0 失败

void (*function)(int , void *)

举例说明 使用on_exit(3)注册遗言函数
代码参见 on_exit.c

_exit(2)

父进程创建子进程,然后父进程马上退出,子进程还没有终止的情况下,子进程过继给init进程。这些子进程被称为孤儿进程。

演示孤儿进程的现象
代码参见alone.c

四、进程资源的回收
在进程终止的时候,会向父进程发送SIGCHLD信号,父进程收到这个信号以后,调用wait(2)家族的函数,去回收子进程的资源。

在父进程还没有回收子进程的资源的时候,这时候,子进程处于僵尸状态。这时候的子进程称为僵尸进程。

举例说明僵尸进程 代码参见zomble.c

wait(2)
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
功能:等待进程状态的改变
参数:
status:如果不为空,存储子进程的信息。
这个整数可以使用宏来检测。
WIFEXITED(status) 如果子进程正常终止,返回true。
WEXITSTATUS(status) 返回子进程的退出状态码。只有在上边的宏返回真的时候,被使用
WIFSIGNALED(status) 如果子进程被信号终止,那么返回true
WTERMSIG(status) 只有在上边的宏为真的情况下使用。返回的是使子进程终止的信号编号。

返回值:
-1 错误
返回终止的子进程的pid。

举例说明 使用wait(2)回收子进程的资源。
代码参见 wait.c

补充:
如何给进程发送信号,终止进程?
kill -信号编号 pid

信号编号 2 3 9

pid_t waitpid(pid_t pid, int *status, int options);
功能:等待子进程状态的改变
参数:
pid:指定了要等待的子进程的pid。
<-1: 等待任意子进程,子进程的组id等于pid的绝对值。
-1:等待任意子进程
0 :等待任意子进程,这写子进程的组id等于当前进程组id。
>0:pid指定了要等待的子进程pid。


status:如果不为空,存储子进程的信息。
options:可以改变是否等待子进程的终止。
WNOHANG:如果没有子进程退出,立即返回。
0 如果没有子进程退出,阻塞等待子进程退出。

返回值:
成功 返回子进程的pid
错误 -1
如果WNOHANG被指定 。0 代表等待的所有子进程都没有终止。

wait(&s)===waitpid(-1,&s,0)
进程组中有1个或更多个进程。一般情况下子进程和父进程属于同进程组。

总结:
一、进程的基础
pstree ps top
二、进程的创建 fork(2)

三、进程的退出
exit _exit atexit on_exit
孤儿进程

四、回收子进程的资源

wait waitpid

僵尸进程


--day10--
回忆昨天内容:
一、进程的基础
二、进程的创建
三、进程的退出
exit(3) _exit(2) atexit(3) on_exit(3)

四、进程退出的时候,使用wait(2)回收子进程的资源。

今天内容:
一、环境变量
bash下的环境变量。
每个进程都默认从父进程继承环境变量
bash本身就是一个程序,这个程序运行的时候,bash进程
可以定义只能之自己这个进程中使用的变量,这种变量称为自定义变量。
用户可以使用
export 环境变量的名字
将自定义变量变为环境变量。环境变量可以被子进程继承。

如何使用程序访问环境变量?
系统维护着一个全局变量 extern char **environ;
这个全局变量的名字就是环境变量列表的首地址。
借用这个全局变量,遍历环境变量列表。
代码参见 trav_l.c

int main()
int main(void)
int main(int argc,char *argv[])
int main(int argc,char *argv[],char *envp[])
使用int main(int argc,char *argv[],char *envp[])遍历环境变量的列表。代码参见trav_l1.c

操作环境变量的函数
getenv(3)
#include <stdlib.h>
char *getenv(const char *name);
功能:获取环境变量的值
参数:
name:指定了环境变量的名字
返回值:
找不到为NULL
返回环境变量值字符串的首地址

环境变量的操作 代码参见 env_op.c

putenv(3)
#include <stdlib.h>
int putenv(char *string);
功能:增加或改变环境变量的值
参数:
string:name=value格式。如果name不存在,增加到环境变量列表。
如果存在,将环境变量的值改为value。
返回值:
成功0 非0 失败

setenv(3)
#include <stdlib.h>
int setenv(const char *name,const char *value,int overwrite);
功能:增加或改变一个环境变量
参数:
name:指定环境变量的名字
value:指定环境变量的值
overwrite:如果环境变量存在,overwrite设置为非0,改变环境变量的值为value;如果overwrite设置为0,环境变量的值不改变。

返回值:
0 成功
-1 错误 errno被设置

int unsetenv(const char *name);
功能:删除环境变量
参数:
name:指定了要删除的环境变量
返回值:
0 成功
-1 错误 errno被设置
clearenv(3)

二、加载新的映像
使用新的映像替换旧的映像。
加载新的映像使用execve(2)家族的函数
#include <unistd.h>
int execve(const char *filename, char *const argv[],
char *const envp[]);
功能:执行程序
参数:
filename:指定了要执行的程序。必须是可执行文件
argv:是传递给程序的参数。相当于命令行参数的传递
envp:是传递给程序的环境变量

返回值:
-1 错误 errno被设置
成功 不返回。

execl(3)
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., char * const envp[]);

int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

l:需要将argv数组中的每个元素罗列出来,传给函数
v:传递的是指针数组的首地址
p:PATH环境变量。如果带p,到PATH环境变量指定的路径下找命令。如果不带p,必须告诉程序可执行文件的路径。
e:如果带e,可以在程序加载的过程中设定环境变量。如果不带e。代表新的进程默认继承父进程的环境变量。


day10$ps -o "pid,ppid,pgrp,comm"
PID PPID PGRP COMMAND
19077 19066 19077 bash
19885 19077 19885 ps

 

举例说明 execve(2)的使用,加载新的映像。
代码参见 execve.c

举例说明 在子进程中加载新的映像。
代码参见 exec_ps.c

perl
bash
python

补充:
在bash下键入a.out的时候,发生了什么?
bash调用fork(2)创建子进程,然后使用exec(3)系列的函数将a.out的映像替换掉子进程从父进程继承下来的映像。这也是所有的bash外部命令运行的原理。

bash运行环境下了,命令分为两种,一种是外部命令
另一种是内部命令。
如何查看一个命令是内部命令还是外部命令
type 命令

内部命令和外部命令的原理是什么?
内部命令的实现在bash程序中,和bash属于同一个程序。在内部命令执行的时候,不需要创建子进程。内部命令的执行和bash是同一个进程。

外部命令就是和bash不是同一个程序。执行外部命令的时候,就fork(2) exec(3) 外部命令的执行和bash的执行不是同一个进程

fork(2)和exec(3)的配合使用
fork(2)只是创建了进程的空间。但是exec才更新了fork(2)出来的子进程映像。


作业:
自己编写代码实现bash的功能。编译生成可执行文件 psh。
内部命令和外部命令
cd

三、system(3)的使用
#include <stdlib.h>
int system(const char *command);
功能:执行一个shell命令
参数:
command:指定了linux的shell命令
返回值:
错误 -1
command的退出状态码。

/bin/sh -c command

bash---->fork----->fork--->fork
a.out sh ls
bash--->fork
a.out ls

举例说明 system(3)的使用 代码参见system.c
编写代码实现延时功能 代码参见 delay.c

四、文件输入重定向的实现
利用学过的知识实现文件输入重定向的功能。
编写代码 实现输入字符串,将字符串转换为大写,然后输出。
代码参见 upper.c
ctrl+d EOF

编写代码对upper.c进行封装,实现文件输入重定向的功能。
代码参见 wrap.c

总结:
一、环境变量
二、使用新的映像替换旧的映像
三、system的使用
四、文件输入重定向
作业:


--day11--
回忆上次课程内容:
一、加载新的映像
exec(3)系列的函数
二、环境变量
三、system(3)的使用
四、文件输入重定向
今天内容:
一、管道
管道分为两种: 无名管道 有名管道
无名管道用于具有亲缘关系的进程间通讯。无名管道是单工的。
有内核管理的一块内存空间。
使用管道,系统提供了pipe(2)
#include <unistd.h>
int pipe(int pipefd[2]);
功能:创建管道
参数:
pipefd[2]:用于返回管道的两端。pipefd[0]指向管道的读端。
pipefd[1]指向管道的写端。

返回值:
0 成功。
-1 错误 errno被设置

使用管道实现两个进程间的通讯。
步骤:
(一)、父进程创建管道
(二)、父进程创建子进程(子进程继承了父进程的文件描述符)
(三)、父进程负责的工作
1、关闭读端
2、通过管道的写端文件描述符,写数据到管道空间。
3、阻塞等待子进程的结束。子进程结束的时候,收尸

(四)、子进程负责的工作
1、关闭写端
2、从管道中读取数据
3、将读取到的数据,输出到显示器
4、结束进程。

代码的实现 pipe.c

有名管道
有名管道其实是一个文件,这个文件只能用于两个进程间通讯的桥梁。不存储任何数据内容。
如何创建一个有名管道的文件。使用mkfifo(3)
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
功能:创建一个有名管道文件
参数:
pathname:指定了有名管道文件的名字
mode:指定了管道文件的权限 mode & ~umask
返回值:
0 成功
-1 错误 errno被设置

编码实现管道文件的创建。 文件名字由命令行第一个参数传入,权限为0664.
代码参见 mkfifo.c
编写代码向有名管道写数据。代码参见PA.c
编写代码实现从有名管道读取数据 代码参见PB.c

进程结束了。 尝试将管道添加到psh中

二、信号的基础
什么是信号?
信号就是软中断。
软中断就是软件模拟的中断机制。
中断又是什么?
正常的执行流程、中断处理程序

正常的执行流程、信号处理程序是两条执行路线,但是属于同一个进程。
系统为我们提供了哪些信号呢?
kill -l

信号有名字和编号。

信号的产生到消失的过程。
信号的产生、信号阻塞、信号递达进程、信号处理
信号的未决状态 就是信号产生了,但是信号还没有被进程处理,这期间,信号的状态为未决状态。

进程可以设置对信号的阻塞。

三、改变信号的处理函数
进程对信号的处理有默认动作。默认处理是终止进程。
除此之外,还有两种 忽略信号 用户自定义
进程从父进程继承信号处理函数。
SIG_DFL 默认
SIG_IGN 忽略
doit 用户自定义的信号处理函数

系统提供了signal(2) 用来改变信号的处理函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:
参数:
signum:指定了信号的编号
handler:指定了signum信号的处理函数
SIG_IGN, SIG_DFL, 用户自定义函数
返回值:
SIG_ERR 错误
返回的是旧的信号处理函数的地址

typedef void (*sighandler_t)(int);

举例说明 编码实现进程忽略2号信号 代码参见signal2.c
编码实现进程对2号信号的处理采用用户自定义的函数。
代码参见 signal_2u.c


四、信号的产生
信号产生的三种形式:
1、硬件产生信号 ctrl+c ctrl+\
2、使用命令为进程发送信号 kill -信号编号 pid
3、使用库函数或者系统调用为进程发送信号
kill(2)
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能:给一个进程发送信号
参数:
pid:指定了接收信号的进程的pid
sig:指定了具体的信号
返回值:
0 成功
-1 错误 errno被设置

举例说明 编写代码实现kill命令的功能。代码参见pkill.c

raise(3)
#include <signal.h>
int raise(int sig);
功能:发送信号给当前进程
参数:
sig:指定信号的编号
返回值:
0 成功
非0 错误

举例说明,使用raise给当前进程发送信号 代码参见 raise.c

alarm(2) 产生SIGALRM信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
功能:产生SIGALRM信号,将这个信号发送给当前进程
参数:
seconds:指定了闹钟的时间。如果这个参数为0.取消闹钟。

返回值:
返回剩余的没执行的时间值。

举例说明 编写代码实现每一秒钟输出的数字。代码参见 count.c

五、信号阻塞和未决信号

信号集类型 sigset_t

系统对sigset_t 类型进行了封装

#include <signal.h>
int sigemptyset(sigset_t *set);
功能:初始化信号集为空,不包含任何信号
参数:
set:指定要初始化的信号集
返回值:
0 成功
-1 错误

int sigfillset(sigset_t *set);
功能:初始化信号集为满,包含所有的信号
参数:
set:指定要初始化的信号集
返回值:
0 成功
-1 错误

int sigaddset(sigset_t *set,int signum);
功能:添加指定的信号到信号集
参数:
set:指定信号集
signum:指定信号
返回值:
0 成功
-1 错误

int sigdelset(sigset_t *set,int signum);
功能:从指定信号集删除指定的信号
参数:
set:指定信号集
signum:指定信号
返回值:
0 成功
-1 错误

int sigismember(const sigset_t *set, int signum);
功能:测试信号是否是集合的一员
参数:
set:指定信号集
signum:指定信号
返回值:
-1 错误
1 是
0 不是

通过sigprocmask(2)设置信号集为进程的屏蔽字。
#include <signal.h>
int sigprocmask(int how,const sigset_t *set,\
sigset_t *oldset);
功能:检查或者改变阻塞信号
参数:
how:
SIG_BLOCK:原来的set和set的并集
SIG_UNBLOCK:将set集合中的信号从当前进程的set中移除。
SIG_SETMASK:将set设置为当前进程的信号屏蔽字

set:新的信号屏蔽字。
oldset:保存进程原来的信号屏蔽字。如果为NULL,不保存。
返回值:
成功 0
-1 错误

举例说明 编写代码实现对2号信号的阻塞。
代码参见 blocked2.c

多次发送2号信号,进程对2号信号阻塞,在解除阻塞的时候,信号处理函数只执行一次,造成了信号的丢失。这样的信号叫不可靠信号。 1~31

34~64 称为可靠信号,不会有信号丢失。

检测进程的未决信号
sigpending(2)
#include <signal.h>
int sigpending(sigset_t *set);
功能:检测未决信号
参数:
set:未决信号掩码被存放在这个集合中
返回值:
成功 0
错误 -1

举例说明 检测进程的未决信号集
代码参见 pending.c


总结:
一 管道 (无名管道 有名管道)
二、信号基础
三、信号的处理
signal(2)
四、信号的产生
五、信号阻塞和未决信号

 


--day12--
回忆昨天内容:
一、管道
无名管道 用于具有亲缘关系的进程间通讯
有名管道 其实是一个管道文件,这个文件只是通讯桥梁的作用,文件中不保存任何数据。

二、信号的基础
信号的编号、名字
信号的产生、阻塞、未决、递达、处理
可靠信号 不可靠信号
三、改变进程对信号的处理函数
signal(2)
信号处理函数有三种类型
SIG_DFL SIG_IGN 用户自定义的信号处理函数

四、信号的产生
硬件 命令 函数
kill(2) raise(3) alarm(2)
五、信号阻塞和未决信号
sigset_t 类型 类型的操作函数
sigprocmask(2) sigpending(2)

今天内容:
一、pause(2)的使用
#include <unistd.h>
int pause(void);
功能:等待信号的到来
返回值:
-1 错误 errno被设置
只有在信号处理函数执行完毕的时候才返回。

利用所学的知识,编码实现sleep函数的功能。
unsigned int psleep(unsigned int seconds);
代码参见 psleep.c

二、信号从产生到处理的全过程
1、进程正在运行,按下ctrl+c键
2、ctrl+c是硬件中断,使用进程切换到内核态。
3、驱动程序将ctrl+c键解释为2号信号
4、在内核态中将进程的PCB的2号信号设置为1.继续执行
5、当进程从内核态回到用户态的时候,检测进程的PCB中有哪些信号到达?如果没有信号到达,直接切换回用户态。如果有信号到达,调用信号的相关处理函数。信号处理函数执行完毕的时候,调用sigreturn(2)返回到内核态。继续第五步。

三、可重入函数
信号处理函数的栈帧是私有的。
信号处理函数和进程的执行是异步的。
如果这两条执行路线出现对共享资源的竞争,这事就大了。
尽量避免竞争。
使我的函数尽量不去访问栈帧以外的资源。
如果函数中使用了全局变量、静态的局部变量、malloc的内存。那么这个函数就是不可重入函数。
可重入函数只能访问栈帧里的内容。如果这个函数只有地洞局部变量,那么这个函数就是可重入函数。
举例说明 信号处理函数和进程竞争共享资源。
代码参见 count.c

四、作业
进程组 有一个或多个进程
父进程 子进程 孙子进程

作业分为前台作业和后台作业,前台作业只有一个,后台作业有多个。
按键产生的信号只能发送给前台作业。

将前台作业转换为后台作业
ctrl+z
后台作业转换为前台作业
fg %作业号
在后台运行作业
bg %作业号
查看后台作业
jobs
在作业启动的时候,直接将作业放到后台执行
作业&

补充一句:
子进程结束的时候,子进程向父进程发送SIGCHLD信号,父进程收到,就去收尸。

五、使用setitimer(2)设置计时器
系统计时器做了解
系统运行一个进程时候,进程消耗的时间包含三部分
用户时间 进程消耗在用户态的时间
内核时间 进程消耗在内核态的时间
睡眠时间 进程消耗在等待I/O、睡眠等不被调度的时间
内核为系统中的每个进程维护三个计时器
真实计时器 统计进程的执行时间
虚拟计时器 统计进程的用户时间
实用计时器 统计进程的用户时间和内核时间

这三个计时器除了统计功能以外,还可以按照自己的规则,以定时器的方式工作,向进程周期性的发送信号。

利用这个功能设计一个计时器
setitimer(2)
#include <sys/time.h>
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
功能:设置定时器的间隔值
参数:
which:
ITIMER_REAL:
ITIMER_VIRTUAL:
ITIMER_PROF:

new_value:指定了定时器的新值
old_value:保存了定时器的旧值
返回值:
0 成功
-1 错误 errno被设置

ITIMER_REAL:真实 SIGALRM
ITIMER_VIRTUAL:虚拟 SIGVTALRM
ITIMER_PROF:实用 SIGPROF

struct itimerval{
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};
struct timeval{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */

};
秒 微秒
1秒=1000毫秒
1毫秒=1000微秒

举例说明 编写代码实现定时器,起始时间是进程启动3秒,然后每隔0.5秒发送一个SIGALRM信号。
代码参见 timer.c

信号结束了

六、system v IPC
消息队列 共享内存 信号量集

在内核管理的内存,用于进程间通讯的内存,称为system v ipc object
操作系统需要管理这些对象。
如何查看当前系统里有哪些对象?
ipcs

在操作系统中这些对象,每一个都有自己的id。便于操作系统的管理。
在用户态需要获取这些对象的id。
获取这些对象的id。需要在用户态有一个键值(唯一的)
将键值和id绑定,这样就可以获取到对象的id。

如何获取这个键值?
ftok(3)
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
功能:获取system v ipc的一个键值 key
参数:
pathname:指定一个文件名,这个文件是存在的,可访问的
proj_id:必须是非0.取这个数的有效低8位。
返回值:
-1 错误 errno被设置
返回一个key值。

举例说明 使用ftok(3)获取一个键值
代码参见ftok.c

消息队列
从系统中获取一个消息队列。如果系统里没有,创建消息队列,将这个消息队列的id给我返回。如果有这个消息队列,返回id即可。

msgget(2)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
功能:获取一个消息队列的id
参数:
key:ftok(3)的返回值
msgflg:
IPC_CREAT:如果不存在创建,存在,不创建
IPC_EXCL:如果和IPC_CREAT一起指定,存在的时候,报错。
mode:指定了消息队列的权限
返回值:
-1 错误 errno被设置
返回消息队列的id

举例说明 使用msgget(2)从内核获取消息队列
代码参见msgget.c

向消息队列中发送消息和从消息队列获取消息
msgsnd(2)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, \
size_t msgsz, int msgflg);
功能:向消息队列发送消息
参数:
msqid:指定了存放消息的消息队列的id。
msgp:指向了消息的地址
msgsz:指定了消息内容的长度
msgflg:
IPC_NOWAIT:非阻塞
0 阻塞
返回值:
成功 0
-1 失败 errno被设置

将一份拷贝追加到消息队列中


ssize_t msgrcv(int msqid, void *msgp,\
size_t msgsz, long msgtyp,int msgflg);
功能:从消息队列接收消息
参数:
msqid:指定了消息队列的id
msgp:指向了消息的地址
msgsz:指定了消息内容的长度
msgtyp:指定了消息的类型
msgflg:
IPC_NOWAIT:没有消息的时候,立即返回错误 errno被设置
0 没有消息的时候,阻塞等待

返回值:
-1 失败 errno被设置
成功 返回实际拷贝到mtext中的字节数。


需要用户自定义这个类型
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
typedef struct msgbuf msgb_t;
msgb_t *st=malloc(sizeof(msgb_t)+strlen(mtext)-3);
st->mtext

举例说明 两个进程通过消息队列实现进程间的通讯。
代码参见 send.c recv.c

总结:
一、pause(2)函数的使用
二、信号从产生到处理的整个过程
三、可重入函数
四、作业 前台作业和后台作业
五、使用setitimer(2)实现定时器
六、进程间通讯 system v ipc
消息队列

 

--day13--
回忆昨天内容:
一、pause(2)的使用
sleep的实现
二、信号从产生到处理的整个过程
三、可重入函数
四、作业
五、使用setitimer(2)设置定时器
初值 间隔值 三个信号
六、system v ipc
消息队列 共享内存 信号量集
创建一个键值
使用键值获取消息队列的id
向消息队列发送数据|从消息队列获取消息

今天的内容:
一、共享内存
1、获取一个键值 ftok(3)
2、使用键值获取共享内存的id shmget(2)
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
功能:分配一块共享内存段。获取和key值相关的共享内存段的id。
参数:
key:ftok(3)的返回值
size:指定了共享内存段的尺寸
shmflg:
IPC_CREAT:如果没有和key值相关的内存段,创建
IPC_EXCL:
mode: 指定共享内存段的权限
返回值:
成功返回共享内存段的id
失败 -1 errno被设置

编写代码创建一块共享内存段,获取该内存段的id
代码参见 shmget.c

3、将共享内存关联到进程的虚拟地址空间 shmat(2)
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:将一块共享内存段添加到进程的地址空间
参数:
shmid:指定了共享内存段的id
shmaddr:NULL 有系统选择地址
shmflg:
SHM_RDONLY:共享内存段 只读
0
返回值:
返回共享内存段附加到进程的地址
错误 (void *) -1 errno被设置


4、向内存读写数据
5、解除进程的虚拟地址到共享内存的关联 shmdt(2)
int shmdt(const void *shmaddr);
功能:解除共享内存段和进程地址空间的关联
参数:
shmaddr:指定了共享内存段的起始地址
返回值:
成功 0
-1 错误 errno被设置

举例说明 使用共享内存段实现进程间的通讯
代码参见 shmA.c shmB.c

二、网络的基础知识

协议就是规则。
物理层协议 定义了电气规则
网帧 定义了网络传输的基本单位 链路层协议
(以太网、令牌环网)
在一个局域网里只允许出现一种网帧。

网络通讯中采用TCP/IP协议家族
TCP/IP 协议分为4层或者5层
物理层、链路层 网络层、传输层、应用层
链路层 网络层、传输层、应用层

ip 地址 192.168.1.12 逻辑地址 4G 40亿
MAC地址 网卡的物理地址 6个字节 4G *64K
每块网卡都有自己的一个身份证。
如和查看本地的ip地址和mac地址
sudo ifconfig

windows 下 ipconfig /all

要实现通许,需要知道对方的ip地址和端口号
网络设备
集线器 hub 就是对电气的放大和分流 物理层 1层交换
交换机 交换的是网帧 链路层 2层交换
路由器 交换的是ip报文 网络层 3层交换

ip地址的分类
既然要做网络通讯,就必须知道ip地址的构成
ip地址是由网络号和主机号组成
那么我们就需要知道ip地址的主机号 网络号
如何获取一个ip地址的网络号?
需要使用子网掩码
192.168.1.130/25

192.168.1.130
255.255.255.128
192.168.1.128 网络号
127-2 125

192.168.1.125/25

192.168.1.125
255.255.255.128
192.168.1.0


192.168.1.125/24
192.168.1.130/24

局域网内数据的传送过程
如何查看本机的路由表?
sudo route -v

如何查看arp表?
sudo arp -a

如何知道两台机器是否互通?
ping 目标ip地址
172.30.3.93

总结:
一、system v IPC
共享内存
二、网络基础
子网掩码
三次握手
TCP/IP协议分层


--day14--
回忆昨天的内容:
一、system V ipc
共享内存
1、获取键值
2、使用键值获取共享内存的id
3、将共享内存附加到进程的地址空间
4、数据操作
5、解除共享内存的附加

二、网络基础
TCP/IP协议的分层
IP 地址 (网络号 主机号) 端口号
IP地址和MAC地址
子网掩码
路由表 arp表
三次握手
今天的内容:
一、基于TCP传输层的编程模型
TCP是面向连接的,安全可靠的。
三次握手
服务器端编程模型
1、创建一个用于网络通讯的设备 通讯端点
socket(2)
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建一个用于通讯的端点
参数:
domain:
AF_INET:应用于IPV4地址家族的
AF_INET6:应用于IPV6地址家族的
type:
SOCK_STREAM:可靠的、基于连接的 双向的、队列式的 TCP
SOCK_DGRAM:支持数据包 不可靠的 无连接的 UDP

protocol:
0
返回值:
-1 错误 errno被设置
返回一个新的文件描述符

2、将这个通讯端点和本机的ip地址、端口号做绑定
bind(2)
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:绑定名字到地址。创建socket以后,socket中有地址空间但是没有具体地址放在这个地址空间。需要将具体的地址和socket的地址空间绑定。
参数:
sockfd:已经创建好的socket,但是这个socked没有具体的地址
addr:指定了具体的地址,将这个地址绑定到sockfd中
addrlen:指定了addr的大小、字节数
返回值:
0 成功
-1 错误 errno被设置

地址家族的通用结构

struct sockaddr{
sa_family_t sa_family;
char sa_data[14];
};

ipv4和ipv6
man in.h
#include <netinet/in.h>
in_port_t uint16_t
in_addr_t uint32_t
sa_family_t <sys/socket.h>

struct in_addr{
in_addr_t s_addr;
};

ipv4的具体地址
struct sockaddr_in{
sa_family_t sin_family; //AF_INET.
in_port_t sin_port; //Port number.
struct in_addr sin_addr; //IP address.
};

INADDR_ANY IPv4 local host address
这是一个宏,宏的本质是一个整数。代表了本机的所有的地址。

 

端口号 0~65535
但是5000以下最好不要用。因为已经被国际组织使用了。

sin_port 采用网络字节序
需要将本机字节序的数字转换为网络字节序
系统提供了函数。处理本机字节序和网络字节序的问题
#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);

h host n net
s short l long to


ip地址 字符串格式 二进制格式 互相转换
inet_pton(3)
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
功能:从字符串格式转换为二进制格式 IPV4 IPV6
参数:
af:
AF_INET or AF_INET6

src:字符串格式的ip地址
dst:存储了网络地址结构的信息
返回值:
1 成功
0 src无效
-1 错误 errno被设置

struct in_addr

#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src,
char *dst, socklen_t size);
功能:二进制到字符串的转化
参数:
af:
AF_INET or AF_INET6
src: struct in_addr
dst:用来存储字符串的空间
size:指定了空间的有效字节数
返回值:
NULL 错误 errno被设置
非空 返回dst的地址,字符串的首地址。

ip地址 127.0.0.1 本地地址 环回地址
测试机器的网络设备工作是否正常。

setsockopt(2)

3、在这个通讯端点上监听客户端连接的到来,如果有连接的到来,将到达的连接存放在缓冲区中(队列的数据结构)
listen(2)
int listen(int sockfd, int backlog);
功能:在socket监听连接。将sockfd标记为被动连接。接收即将到来的客户端请求。
参数:
sockfd:指定了被监听的socket
backlog:指定了未决连接的最大数。
返回值:
0 成功
-1 错误 errno被设置

4、从这个缓冲区队列中取出一个客户端连接,返回一个连接描述符用于和客户端的通讯。(这个连接描述符称为conn_fd)
accept(2)
int accept(int sockfd, struct sockaddr *addr, \
socklen_t *addrlen);
功能:在socket上接收一个连接
参数:
sockfd:指定了监听的socket
addr:在这个地址空间里填充了客户端的地址和端口号
addrlen:空间里指定了addr的长度。如果addr为NULL,那么也要设置为NULL
返回值:
-1 错误 errno被设置
返回一个非负的整数,就是连接描述符

5、使用conn_fd和客户端通讯
(1)获取客户端的请求
read(2)
(2)处理客户端请求
(3)响应客户端
write(2)

6、关闭conn_fd,终止和客户端的通讯
close(conn_fd);

客户端的编程模型
1、创建一个用于通讯的设备(通讯端点)
socket(2)
2、使用这个端点连接到服务器(IP地址和端口号)
connect(2)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:在socket上发起一个连接
参数:
sockfd:指定socket,将这个socket连接到addr的地址空间
addr:指定具体的地址空间,要连接到的地址空间。server
addrlen:指定了addr的长度
返回值:
0 成功
-1 错误 errno被设置

3、向服务器发送消息
4、等待服务器端的响应消息
5、处理服务器的响应消息
6、关闭设备,结束本次通讯。

举例说明 编写基于TCP的通讯模型
服务器端代码 参见 server.c
客户端代码参见 client.c
172.30.3.93

二、并发服务器的实现
三种方法实现服务器的并发 多进程 多线程 多路复用
使用多进程实现服务器的并发功能
什么时候?什么地方?子进程才登场。
fork(2)

子进程负责的任务
子进程负责和客户端的通讯
关闭s_fd。
信息处理
关闭和客户端的连接


父进程负责的任务
父进程负责从未决连接队列中取出一个连接,创建和客户端通讯的文件描述符
创建子进程

关闭和客户端的连接
负责对子进程收尸。非阻塞收尸

 

三、基于传输层是UDP的编程模型

 

--day15--
回忆昨天的内容:
一、基于TCP的网络编程模型
服务器端 客户端

二、并发服务器的实现
多进程实现并发服务器
创建子进程的时机 子进程负责的任务 父进程负责的任务

今天的内容:
一、基于UDP的网络编程模型
服务器端
1、创建socket.
2、将fd和服务器的ip地址和端口号绑定
3、recvfrom阻塞等待接收客户端数据
4、业务处理
5、响应客户端


客户端:
1、创建socket
2、向服务器发送数据sendto
3、阻塞等待服务器的响应信息
4、处理响应信息
5、断开通讯

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd,void *buf,size_t len,\
int flags,
struct sockaddr *src_addr,\
socklen_t *addrlen);
功能:从一个socket上接收消息
参数:
sockfd:指定socket。socket(2)的返回值
buf:存放消息的缓冲区地址
len:指定buf的最大尺寸
flags:0
src_addr:存放的是对面的地址
addrlen:是一个值-结果参数。src_addr的长度
返回值:
成功 返回接收到的字节数
-1 错误 errno被设置
0 对面down机

#include <sys/types.h>
#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);

功能:在socket上发送消息
参数:
sockfd:指定socket
buf:存放数据的缓冲区首地址
len:buf中有效的字节数
flags:0
dest_addr:目标地址
addrlen:目标地址的长度
返回值:
-1 错误 errno被设置
成功 返回发送出去的字节数。

编写代码实现基于udp的网络通讯。
代码参见:
userv.c uclie.c

172.30.3.93

网络通讯

二、线程的基础
线程 执行的基本单位,线程共享进程的资源
进程 进程是资源分配的基本单位

每个线程有自己的tid。thread_id
每个线程有自己私有的栈帧。

三、线程的创建
系统提供了函数pthread_create(3)用于创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine) (void *),\
void *arg);
功能:创建一个新的线程
参数:
thread:存放线程id的缓冲区。
attr:NULL 缺省属性
start_routine:线程的执行函数
arg:start_routine函数的唯一参数。
返回值:
0 成功
错误 错误码

Compile and link with -pthread.
void *(*start_routine) (void *)

举例说明 创建新的线程
代码参见 pthread_c.c

getpid(2)获取进程的pid。
pthread_self(3)来获取线程自己的tid。
#include <pthread.h>
pthread_t pthread_self(void);
功能:获取当前线程的id
参数:
void
返回值:
返回线程的id。

四、线程退出、汇合、分离
线程的退出
1、return和exit(3)的区别
return只是函数的返回,在线程处理函数中,只是代表了线程的结束。而exit(3)代表的是进程的结束。进程中的所有线程就终止了。

2、使用函数pthread_exit(3)来终止一个线程
#include <pthread.h>
void pthread_exit(void *retval);
功能:终止当前线程
参数:
retval:指定传递给另一个线程的值,那个线程调用pthread_join(3)接收这个值。

返回值:
不返回给调用者。
3、pthread_cancel(3)
#include <pthread.h>
int pthread_cancel(pthread_t thread);
功能:给线程发送取消请求
参数:
thread:指定了接收请求的线程id。

返回值:
0 成功
非0 错误码

注意:使用pthread_cancel终止的进程,在使用pthread_join(3)获取线程退出信息的时候,获取到的是PTHREAD_CANCELED。

线程的汇合
pthread_join(3)等待线程的汇合
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
功能:汇合一个终止的线程
参数:
thread:指定了等待汇合的线程的id
retval:
返回值:
成功 0
失败 返回错误码

举例说明 线程的退出和汇合
代码参见pthread_e.c

线程的分离
pthread_detach(3)
#include <pthread.h>
int pthread_detach(pthread_t thread);
功能:分离一个线程
参数:
thread:指定要分离的线程
返回值:
0 成功
非0 错误码

举例说明 线程的分离
代码参见 pthread_d.c

新建的线程和进程中已经存在的线程是异步的。
这些线程会对公共资源形成竞争。怎么解决竞争?
1、可重入函数
2、让异步的线程同步的访问共享资源。

五、线程同步
条件变量 mutex锁 信号量
举例说明 多个线程异步访问共享资源(临界资源)
代码参见 count.c

使用mutex锁解决临界资源的问题
什么是mutex锁?
pthread_mutex_t 是一个类型 mutex锁类型

mutex所是一个互斥设备。
一个mutex锁类型的变量有两中状态
unlocked:不被任何线程拥有
locked:被一个线程拥有

一个mutex锁从来不能被两个线程同时拥有。
如果一个线程想拥有的mutex锁,被另外的线程占用。那么这个线程挂起执行,直到另外线程放弃才能得到。

对临界资源的访问要遵守三歩:
1、先获取mutex锁
2、访问临界资源
3、释放mutex锁

phtread_mutex_init(3)
#include <pthread.h>

静态初始化一个mutex锁
pthread_mutex_t fastmutex=PTHREAD_MUTEX_INITIALIZER;

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
功能:初始化一个mutex锁
参数:
mutex:指定要初始化的mutex锁
mutexattr:NULL 默认
返回值:
0

int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:获取mutex锁,如果这个锁不被其他线程占有,立即返回,拥有了这把锁。将锁的状态改变为locked。
这把锁被其他线程占有。挂起线程,直到其他线程解锁为止。
参数:
mutex:指定了要获取的mutex锁
返回值:
非0 错误
0 成功


int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:获取mutex锁,在其他线程占有这个mutex锁的时候,非阻塞。立即返回,错误。EBUSY
参数:
mutex:指定要获取的mutex锁
返回值:
非0 错误
0 成功
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:解锁mutex
参数:
mutex:释放mutex锁
返回值:
非0 错误
0 成功
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁mutex锁
参数:
mutex:指定要销毁的mutex锁
返回值:
非0 错误
0 成功

改进count.c。使用mutex锁让进程同步访问临界资源。
总结:
一、基于UDP的编程模型
二、线程的基础
三、线程的创建
四、线程的退出、汇合、分离
五、线程同步 mutex锁


--day16--
回忆上周内容:
一、基于UDP的网络编程
不可靠的、无连接的
二、线程的基础
三、线程的创建
四、线程的退出、汇合、分离
五、线程同步
mutex锁
条件变量
信号量

今天内容:
一、线程同步
条件变量
什么是条件变量?
线程A等待某个条件成立,条件成立,线程A才继续向下执行。线程B的执行使条件成立,条件成立以后唤醒线程A,以继续执行。这个条件就是条件变量。
pthread_cond_t 类型 就是条件变量的类型
对类型的封装如下:
#include <pthread.h>
//条件变量的静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int pthread_cond_init(pthread_cond_t *cond,\ pthread_condattr_t *cond_attr);
功能:初始化一个条件变量
参数:
cond:指定要初始化的条件变量
cond_attr:NULL 默认属性
返回值:
0 成功
非0 错误


int pthread_cond_signal(pthread_cond_t *cond);
功能:启动在等待条件变量变为真的一个线程
参数:
cond:指定条件变量
返回值:
0 成功
非0 错误

int pthread_cond_broadcast(pthread_cond_t *cond);
功能:启动所有的等待条件变量为真的线程
参数:
cond:指定条件变量
返回值:
0 成功
非0 错误

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
功能:等待条件变量为真
参数:
cond:指定等待的条件变量
mutex:等待之前需要解开的锁
返回值:
0 成功
非0 错误

操作步骤:
1、先原子解mutex锁。
2、让线程等待条件变量变为真。
3、条件变量为真的时候,加锁

int pthread_cond_timedwait(pthread_cond_t *cond,
pthread_mutex_t *mutex, \
const struct timespec *abstime);
功能:超时等待,超时返回错误
参数:
cond:指定等待的条件变量
mutex:等待之前需要解开的锁
abstime:指定等待的时间
返回值:
0 成功
非0 错误
int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁条件变量
参数:
cond:指定要销毁的条件变量
返回值:
0 成功
非0 错误

生产者和消费者的例子
链表实现 生产者生产出来对象放到链表的头部
消费者从链表的头部取出消费。
第一思考:两个线程如何同步访问链表的头部
第二思考:如果链表为空,消费者等待生产者线程生产对象。
第三思考:生产者线程,生产出对象通知消费者。

代码参见: cond.c


信号量
多个线程使用多个资源的时候,使用信号量实现线程的同步。
sem_t类型
类型的操作如下:
sem_init(3)
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化信号量
参数:
sem:指定了要初始化的信号量
pshared:0 应用于多线程 非0 多进程
value:指定了信号量的初始值
返回值:
0 成功
-1 失败 errno被设置

sem_destroy(3)
#include <semaphore.h>
int sem_destroy(sem_t *sem);
功能:销毁信号量
参数:
sem:指定要销毁的信号量
返回值:
0 成功
-1 错误 errno被设置

sem_post(3)
#include <semaphore.h>
int sem_post(sem_t *sem);
功能:信号量的值加1操作
参数:
sem:指定的信号量,就是这个信号量的值加1.
返回值:
0 成功
-1 错误 errno被设置

sem_wait(3)
#include <semaphore.h>
int sem_wait(sem_t *sem);
功能:信号量的值减1.如果信号量的值为0.阻塞等待
参数:
sem:指定了要操作的信号量
返回值:
0 成功
-1 错误 errno被设置
int sem_trywait(sem_t *sem);

int sem_timedwait(sem_t *sem, \
const struct timespec *abs_timeout);


使用信号量,实现生产者和消费者的例子
使用环状队列实现。
代码参见 sem.c

线程结束了

二、system v IPC
信号量集
信号量集就是数组,数组里的每一个元素都是信号量类型的。

1、先获取键值
ftok(3)
2、使用键值获取信号量集的id
semget(2)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
功能:获取信号量集的id
参数:
key:ftok(3)的返回值
nsems:指定了信号量集中的元素个数
semflg:
IPC_CREAT:
IPC_EXCL:
mode:指定信号量集的权限
返回值:
非负整数,是信号量集的id。
-1 错误 errno被设置

3、设置信号量集中的信号量的初值或者获取信号量的值。
semctl(2)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
功能:信号量的控制操作
参数:
semid:指定了信号量集
semnum:指定了信号量在数组中的下标
cmd:指定了对信号量的控制操作命令
GETVAL:获取到第几个信号了的semval值。 semval的值
SETVAL:设置第几个信号量的semval值为arg.val。 0

...:可变参数。这个有没有,什么类型?取决于cmd。
返回值:
-1 错误 errno被设置

需要自定义
union semun {
int val; /* Value for SETVAL */
struct semid_ds *buf; /* Buffer for IPC_STAT,IPC_SET */
unsigned short *array; /*Array for GETALL, SETALL */
struct seminfo *__buf; /* Buffer for IPC_INFO
(Linux-specific) */
};

4、对指定的信号量加法或者减法操作
semop(2)
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
功能:信号量操作
参数:
semid:指定了信号量集
sops:指定了对某个信号量的具体操作
nsops:指定了要操作的信号量的个数。

返回值:
0 成功
-1 错误 errno被设置

操作定义在这个结构体中
struct sembuf{
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */

};
sem_flg:
IPC_NOWAIT
IPC_UNDONE 自动撤销操作

sem_num:指定了要操作的信号量在数组的下标
sem_op:指定了操作的类型
操作类型分为3种
>0 semval+sem_op semval做加法
=0

<0:
semval>sem_op的绝对值,这个操作立即执行。

semval<sem_op的绝对值
申请的资源数>可用资源数 IPC_NOWAIT 非阻塞
sem_flg=0 阻塞等待资源可用

举例说明 使用信号量集实现进程间的通讯。
两个进程
PA.c 设置信号量的值 ,每隔3秒,semval的值减1
Pb.c 每隔1秒,获取信号量的值,


三、http服务器开工

 







































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































































以上是关于UNIX C 总结的主要内容,如果未能解决你的问题,请参考以下文章

李迟2021年9月知识总结

总结!嵌入式linux基础学习笔记

C/C++ 时间知识总结

马哥教育M28-孙雪峰-前四节课学习总结

熬夜整理的C/C++万字总结

C语言入门基础总结-- 1预备知识