C高级笔记
符号含义
1、hqyj@ubuntu:~$
- hqyj:用户名
- @:分隔
- ubuntu:主机名
- ‘:’:分隔
- ~:文件路径---> ~:家目录 /:根目录
- $:分隔
2、文件类型及标识符
- d:目录文件
- c:字符设备
- b:块设备block
- -:普通文件
- p:管道文件
- s:套接字文件
- l:链接文件
3、权限及标识符
r:读权限
w:写
x:可执行
-:无该权限
4、根目录下的目录文件说明:
- bin:二进制文件目录
- mnt:挂载文件目录
- root:超级用户文件目录
- usr:用户文件目录
- etc:配置文件目录
- dev:设备文件目录
- boot:启动文件目录
- proc:进程文件目录
5、常用指令
ls:列出指定路径下的文件
-l:列出文件的详细信息
-a:列出所有文件包含隐藏文件
cd:路径跳转 绝对路径:从根目录出发的路径 相对路径:从当前路径出发的路径
mkdir:创建目录文件
6、vi编辑器:
命令行模式
dd:剪切光标所在行
n+dd:剪切光标后n行
p:粘贴 yy:复制光标所在行
n+yy:复制n行
u:撤销
ZZ:保存并退出 v--->进入可视模式
gg=G:全文自动缩进
插入模式
i:光标前开始插入
a:光标后开始插入
o:光标下一行开始插入
底行模式
w:保存
q:退出
wq:保存并退出
x:保存并退出
set nu:设置行号
set mouse=a:使能鼠标
vsp + 文件名:横向展开另一个文件
sp + 文件名:纵向展开
7、文件操作
mv:移动文件到指定目录,或者重命名
ls:列出文件信息
cp:复制文件
mkdir:创建目录文件
touch:创建文件,修改文件的时间戳
cat:在终端显示文件内容,查看文件
head:默认查看文件的前十行
head -num filename:查看文件开头num行
tail:默认查看文件后十行
rm:删除文件,删除目录文件需要添加-r选项
rmdir:删除空目录文件
wc:统计字符数
-c :字节数
-w:单词数
-l :行数
echo:在终端上显示指定字符内容
grep:在指定文件中查询指定的字符串 grep “main” -n demo.c 在demo.c文件中查询到于main字符 串相关的内容并打印对应的行号
cut:剪切字符串
8、进程管理:
- ps:在终端上显示进程程序
- kill:杀死指定pid的进程程序
9、指针
地址:内存单元每个字节的编号,32位
指针及地址
指针变量:存放地址信息的变量
指针变量的大小:64位8字节
10、*号的作用:
- 乘法
- 在定义变量时说明该变量是一个指针变量
- 对地址使用操作该地址下的数据
11、&号的作用:
- 位与
- 取出变量的地址信息
12、指针的运算:
指针和指针: 同类型指针相减,大地址减去小地址,两个地址间间隔的元素个数
指针的算数运算
指针+1:偏移的地址由该指针指向的数据类型决定
13、指针和数组:
1. 指针数组 本质是一个数组,该数组内的元素全是指针
2. 数组指针 本质是一个指针,该指针指向的数据是一整个数组的地址
14、函数
作用:代码封装,实现特定功能的代码模块函数声明;
不会在程序中开辟空间,但是会表示该函数存在函数的返回值:
由函数定义时决定,函数返回值类型必须和函数定义的数据类型相同
传参:
- 数值传参 相当于在函数空间中对于实际参数进行了复制操作,修改形 式参数的值不会影响到实际参数
- 地址传参 相当于将实际参数的地址传递给函数,函数内利用指针接收 该地址,在函数空间中通过该地址可以修改到实际参数的值
15、全局变量:
全局可见的数据
16、函数调用
函数名();
17、函数指针:
本质是一个指针指向该函数的入口地址
18、memset函数
函数的功能: 将指定地址的空间的数据设置成指定数据,设置空间的大小由参数传递
函数原型: void *memset(void *s, int c, size_t n);
函数使用需要添加的头文件: #include 函数的参数说明: void *s:
需要设置成数据的首地址 int c:
想要设置成的字符的数据 size_t n:
想要设置的内存空间的大小 函数的返回值 函数返回设置了数据的地址的起始地址
19、malloc和free函数
函数的功能:
malloc功能是在堆空间中开辟空间,函数返回值是可以操作这片
空间的大小 free功能是释放掉堆空间开辟的空间
函数原型: void *malloc(size_t size);
void free(void *ptr);
函数使用需要添加的头文件: #include
函数的参数说明:
malloc: size_t size:
需要在堆空间中开辟的空间大小 free:
void *ptr:释放堆空间的起始地址
函数的返回值说明: malloc:返回值为指针类型,是在堆空间中开辟空间的起始地址 free函数无返回值
20、结构体:
将不同类型的数据进行打包
声明结构体:
struct 结构体类型名
{
成员1;
成员2;
};
定义结构体变量 struct
结构体类型名 变量名;
初始化结构体 struct 结构体类型名 变量名 = {成员1,成员2};
结构体成员的访问 结构体变量.结构体内的成员名
21、typedef:
作用是取别名,可以将一种数据类型定义一个新的名字表示
22、结构体数组:
本质是数组,数组内的元素全是结构体
23、结构体指针:
本质是一个指针指向结构体变量
24、联合体(共用体):
联合体中可以存储不同类型的数据,但是这些数据将会使用同一片空间
25、Makefile:
make是工程管理工具,可以通过读取makefile文件执行大量的编译效果
make工程管理工具会根据文件的时间戳决定文件的更新程度,如果没有修改则可以省略没修改文件的编译步骤达到可以减少编译时间的目的
26、函数指针:
函数指针本质是一个指针,可以通过指针指向一个存在的函数
数据结构
栈:
是一种线性表;先进后出;只允许在一端操作。
定义顺序栈:
//大小固定
typedef int typedata;
#define MAXSIZE 100
typedef struct node{
typedata data[MAXSIZE];
int top;
}Seqstack, *Pseqstack;
//大小可由用户自定义
typedef int typedata;
typedef struct node{
int MAXSIZE;
typedata *data;
int top;
}Seqstack, *Pseqstack;
栈的创建:
Pseqstack stack_create()
{
Pseqstack p = malloc(sizeof(Seqstack));
if(NULL == p)
return NULL;
p->top = -1;
return p;
}
Pseqstack stack_create(int len)
{
Pseqstack p = malloc(sizeof(Seqstack));
if(NULL == p)
return NULL;
p->top = -1;
p->MAXSIZE = len;
p->data = malloc(sizeof(typedata)*len);
if(NULL == p->data){
free(p);
return NULL;
}
return p;
}
栈判空判满:
判空:top == -1;
判满: top == MAXSIZE-1;
int stack_empty(Pseqstack p)
{
if(NULL == p)
return -2;
if(-1 == p->top){
return 0;
}else{
return -1;
}
}
int stack_full(Pseqstack p)
{
if(NULL == p)
return -2;
if(p->MAXSIZE-1 == p->top){
return 0;
}else{
return -1;
}
}
压栈:
int stack_push(Pseqstack p, typedata data)
{
if(!stack_full(p)){
return -1;
}
p->top++;
p->data[p->top] = data;
return 0;
}
出栈:
typedata stack_pop(Pseqstack p)
{
if(!stack_empty(p)){
return -1;
}
//return p->data[p->top--];
typedata data = p->data[p->top];
p->top--;
return data;
}
获取栈顶元素:
typedata stack_get_top(Pseqstack p)
{
return p->data[p->top];
}
链式栈:
存储结构是链式的,理论没有上限,所以不需要判满
队列:
仍然是线性表;只允许一端输入数据,一端输出数据;先进先出。
创建一个顺序队列:
#define MAXSIZE 10
typedef struct node{
int data[MAXSIZE];
int front, rear;
}Sequeue, *Psequeue;
//创建
Psequeue queue_create()
{
Psequeue p = malloc(sizeof(Sequeue));
if(NULL == p)
return NULL;
p->front = p->rear = MAXSIZE - 1;
return p;
}
//判空
int queue_empty(Psequeue q)
{
if(NULL == q){
return -2;
}
if(q->front == q->rear){
return 0;
}
return -1;
}
//判满
int queue_full(Psequeue q)
{
if(NULL == q){
return -2;
}
if(q->front == (q->rear+1)%MAXSIZE){
return 0;
}
return -1;
}
//入队
int queue_in(Psequeue q, int data)
{
if(0 == queue_full(q)){
return -1;
}
q->rear = (q->rear+1) % MAXSIZE;
q->data[q->rear] = data;
return 0;
}
//出队
int queue_out(Psequeue q, int *data)
{
if(0 == queue_empty(q)){
return -1;
}
q->front = (q->front+1) % MAXSIZE;
*data = q->data[q->front];
return 0;
}
链式队列:
一端入队,一端出队;先进先出。
//定义队列元素的节点
typedef struct node{
int data;
struct node *next;
}Node, *Pnode;
//定义队列结构体
typedef struct queue{
Pnode pf, pr; //pf指向队头 ,pr指向队尾
}Linkqueue, *Plinkqueue;
//创建队列
bool queue_create(Plinkqueue *Q)
{
//创建一个队列的对象(管理队列)
*Q = malloc(sizeof(Linkqueue));
if(NULL == *Q){
return false;
}
//创建一个队列的具体节点,且该节点不存数据
(*Q)->pf =(*Q)->pr = malloc(sizeof(Node));
(*Q)->pf->next = NULL;
return true;
}
//判空
bool queue_empty(Plinkqueue q)
{
if(NULL == q){
return false;
}
if(q->pf->next == NULL){
return true;
}
return false;
}
//入队
bool queue_in(Plinkqueue q, int data)
{
if(NULL == q){
return false;
}
Pnode p = malloc(sizeof(Node));
if(NULL == p){
return false;
}
p->data = data;
p->next = NULL; //p->next = q->pr->next;
q->pr->next = p;
//让pr指向新的队尾
q->pr = p;
return true;
}
//出队
bool queue_out(Plinkqueue q, int *data)
{
if(NULL == q){
return false;
}
if(true == queue_empty(q)){
return false;
}
Pnode p = q->pf->next;
//判断出队的元素是否是队列的队尾,如果是,需要
if(p == q->pr){
q->pr = q->pf;
}
q->pf->next = p->next;
*data = p->data;
free(p);
return true;
}
树:
先序:A B C D E F G H K 中序:B D C A E H G K F
A
B
C
D
E
F
G
H
K
B D C A E H G K F
声明二叉树的结构体:
typedef struct node{
char ch;
struct node *lchild, *rchild;
}Btree, *pBtree;
创建一个二叉树:
pBtree btree_create()
{
pBtree pa = malloc(sizeof(Btree));
pBtree pb = malloc(sizeof(Btree));
pBtree pc = malloc(sizeof(Btree));
pBtree pd = malloc(sizeof(Btree));
pBtree pe = malloc(sizeof(Btree));
pBtree pf = malloc(sizeof(Btree));
pa->data = 'A';
pb->data = 'B';
pc->data = 'C';
pd->data = 'D';
pe->data = 'E';
pf->data = 'F';
pa->lchild = pb;
pa->rchild = pc;
pc->lchild = NULL;
pc->rchild = NULL;
pb->lchild = NULL;
pb->rchild = pd;
pd->lchild = pe;
pd->rchild = pf;
pe->lchild = NULL;
pe->rchild = NULL;
pf->lchild = NULL;
pf->rchild = NULL;
}
//先序遍历
void btree_pre_order(pBtree b)
{
if(NULL == b){
return ;
}
//先打印数据
printf("%c ", b->data);
//再遍历左孩子
btree_pre_order(b->lchild);
//再遍历右孩子
btree_pre_order(b->rchild);
}
IO
进程
1. 进程
进程是一个程序的一次执行的过程,每一个进程都分配一个虚拟的4G内存。
0-3G : 用户
3G-4G : 内核
2. 进程和程序的区别
程序是静态的
进程是动态的
3. 进程的内存管理
正文段、用户数据段、系统数据段
4. 进程号PID
唯一的标识一个进程
5. 进程的类型
交互进程(ctrl+z / jobs -l / bg / fg / & / kill -l)
ctrl + z : 使进程进入挂起状态(T), 被挂起的进程称为作业
jobs -l: 查看挂起的进程
bg % 作业号 :使作业恢复前台运行, 不能ctrl+c结束
fg % 作业号 : 使作业恢复前台运行, 可以ctrl+c结束
kill -l : 查看信号种类
kill -9 PID : 杀死进程
ps -ajx : 查看进程的运行状态
批处理进程(运维)
守护进程( 1 init )
6. 进程的运行状态
ps -ajx
运行态R:此时进程或者正在进行,或者准备运行
内核调度程序到CPU上执行 running
等待态:此时进程需要满足一些条件,如果不满足就等待
可中断S:如果进程收到信号会醒来 ctrl+c
不可中断D:如果进程收到信号不会醒来
停止态T:此时进程被中止SIGSTOP
死亡态Z:已终止的进程、僵尸进程
但还在进程向量数组中占有一个task_struct结构
task_struct{
pid_t pid;
R;
...
};
< 高优先级
N 低优先级
L 有些页被锁进内存
s 会话组组长
位于前台的进程组
l 多线程,克隆线程
ctrl + alt + f1 - f6 : 打开字符终端
用户名:farsight
密码:1
为了多用户使用计算机
结束字符终端: alt + f7
top
top -p PID : 动态查看进程状态
renice -5 PID : 改变进程的NI值(默认0)
7.进程相关的函数
fork/exit
创建进程、退出进程
wait/waitpid
回收进程资源
#include <unistd.h>
pid_t pid; //进程号
pid_t fork(void);
功能:创建进程
参数:无
返回值:
pid < 0 :创建失败
pid == 0 :子进程
pid > 0 :父进程
从fork函数往下分为两个进程开始运行。
父进程和子进程执行顺序是随机的。
fork函数特性:
1.子进程创建时,几乎拷贝了父进程全部内容,包括代码段、数据段、堆栈段、文件描述符、虚拟地址空间
2.同一个父进程创建的子进程都是属于同一个进程组
pkill -9 -g PGID
3.进程是管理资源的最小单位
#include <stdlib.h>
void exit(int status); //刷新缓存
//系统调用函数
#include <unistd.h>
void _exit(int status); //不能刷新缓存
pid_t wait(int *status);
功能:父进程等待子进程结束,回收它的资源
参数:一般设为NULL
WEXITSTATUS(status) 获取子进程返回值
WIFEXITED(status) 判断子进程是否正常结束
子进程先与父进程退出---父进程未回收资源---子进程会变成僵尸进程
危害:占用进程号、内存空间、PCB进程控制块等
解决:wait / waitpid / 信号
注意:任何进程结束都会变成僵尸进程,只是时间有长有短
父进程先与子进程退出---子进程会变成孤儿进程---被init进程接管(收养)
init进程:系统启动后运行的第一个用户空间进程,pid=1,会定期扫描系统,收养孤儿进程。
注意:孤儿进程一般没什么危害
pid_t waitpid(pid_t pid, int *status, int options);
功能:父进程自动回收子进程结束后的资源
参数:
pid: -1 代表所有的子进程
status: NULL
options:
0 阻塞
WNOHANG 非阻塞
waitpid(-1, NULL, 0); == wait(NULL);
exec函数簇
1. 概念:
函数族提供了一种在进程中启动另一个程序执行的方法。
它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。
比如bash用到了exec函数来执行我们的可执行文件。
2. 在Linux中使用exec函数族主要有以下两种情况
当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec 函数族让自己重生。
如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。
3. 函数
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
返回值:
成功不返回
失败返回 -1 更新 errno
注意:
exec函数的参数表传递方式以函数名的第五位字母来区分:
字母为"l"(list)的表示逐个列举的方式;
字母为"v"(vertor)的表示将所有参数构造成指针数组传递;
以p结尾的函数可以只给出文件名
以"e"(enviromen)结尾的两个函数execle、execve就可以在
envp[]中设置当前进程所使用的环境变量
使用execle和execve可以自己向执行进程传递环境变量,但不会继承Shell进程的环境变量
事实上,这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用
守护进程daemon
1.守护进程:
在linux中与用户交互的界面叫终端,从终端运行起来的程序都依附于这个终端,
当终端关关闭时,相应的进程都会被关闭,守护进程可以突破这个限制。
2.特点:
在后台服务的进程
生存期很长
守护进程独立于控制终端
比如:init进程 pid=1 开机运行 关机才结束
3.守护进程创建流程:
1. 创建子进程,父进程退出
fork(void);
2. 在子进程中创建新会话
setsid(void);
3. 修改工作目录
chdir("");
4. 修改umask (增加安全性)
umask();
5. 关闭文件描述(回收资源)
close();
线程的概念
1. 每个用户进程有自己的虚拟地址空间
2. 系统为每个用户进程创建一个
task_struct 来描述该进程 struct task_struct
3. task_struct 和地址空间映射表一起用来表示一个进程
4. 由于进程的虚拟地址空间是私有的,因此进程切换开销很大
5. 为了提高系统的性能,linux引入轻量级进程, 起名为线程
6. 在同一个进程中创建的线程共享该进程的地址空间
7. Linux里同样用task_struct来描述一个线程。
线程和进程都参与统一的调度
总结:
1. 通常线程指的是共享相同虚拟地址空间的多个任务
2. 使用多线程, 大大提高了任务切换的效率
线程不需要虚拟内存,为什么?
每个进程中至少有一个线程,就是主线程,还可以产生多个线程
共享4G内存空间,线程切换只需要虚拟CPU(寄存器)
同样用task_struct来描述一个线程,线程和进程都参于统一的调度
进程代表资源分配的最小单位
线程是最小的调度单位
线程概念:同一个地址空间中的多个任务
线程相关的函数
创建线程
#include <pthread.h>
pthread_t tid; //线程号
int pthread_create(
pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数:
thread: &tid 线程号的地址
attr: 线程函数的属性 一般设为NULL,默认属性
start_routine: 线程函数的函数名
arg: 给线程函数传递的参数 不传参设为NULL
Compile and link with -pthread.
回收线程资源
主线程等待子线程结束,然后回收它的资源(阻塞)
int pthread_join(pthread_t thread, void **retval);
参数:
thread: tid
retval: 接收pthread_exit的返回值,设为NULL
把主线程和子线程分离开来,子线程结束,会自动回收资源
(非阻塞)
int pthread_detach(pthread_t thread);
参数:
thread:线程号 tid
结束子线程
void pthread_exit(void *retval);
线程传参
{
值传递
地址传递
}
关闭线程
int pthread_cancel(pthread_t thread);
优点:线程间很容易进行通信
通过全局变量实现数据共享和交换
缺点:多个线程同时访问共享对象
时需要引入同步和互斥机制
同步和互斥 :保护共享资源,避免竟态
同步:多个任务按理想的顺序/步调来进行
互斥:不能同时访问
都是为了避免竟态:多个任务同时访问共享资源
线程的同步------信号量
#include <semaphore.h>
sem_t sem;
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能: 初始化信号量
参数:
sem: &sem
pshared: 0 线程 1 进程
初值: 0 or 1
int sem_wait(sem_t *sem); //-1
当sem_init初值为0的时候,会阻塞
int sem_post(sem_t *sem); //+1
buf: 全局变量
主线程:输入字符串
子线程:打印字符串的长度
线程的互斥------互斥锁
pthread_mutex_t mutex;
//初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const
pthread_mutexattr_t *mutexattr);
参数:
&mutex;
NULL (默认属性)
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
同步:信号量
sem_t sem;
sem_init(&sem, 0, 0);
sem_wait(&sem); //-1
sem_post(&sem); //+1
互斥:互斥锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
}
传统的进程间通信
无名管道
有名管道
信号
原始的方式:用文件中转
无名管道 (pipe)
查看命令: man 2 pipe
头文件:#include <unistd.h>
函数原型: int pipe(int pipefd[2]);
pipefd[2] :无名管道的两个文件描述符,int型的数组,大小为2,pipefd[0]为读端,pipefd[1]为写端
返回值:
成功:返回0
失败:返回-1
无名管道的特点:
a、没有名字,因此无法使用open()打开
b、只能用于亲缘进程间(如父子进程、兄弟进程、祖孙进程等)通信
c、半双工工作方式,读写端是分开的,pipefd[0]为读端,pipefd[1]为写端
d、是一种特殊的文件,只存在内存中,由内核进行管理
e、对于它的读写可以使用文件IO如read、write函数
f、无名管道的操作属于一次性操作,如果对无名管道进行读操作,数据会被全部读走
单工:固定一种方向进行通信 广播
半双工:同一时间只能由一端发送到另一端 对讲机
全双工:通信方向都可以,同时可以发送也可以接收电话
既然说是管道,所以可以想象成一条水管,连接两个进程,
一个进程负责输入数据,另一个进程负责接收数据,反过来也一样。
所以在无名管道中也一样,无名管道的两端,每一端都可以读和写。
若一端为读就要关闭他的写功能,另一端就只能为写关闭读功能。
注意事项:
1. 当管道中无数据时,执行读操作,读操作阻塞
2. 无名管道的大小是固定的,管道一旦满,写操作就会导致进程阻塞
3. 对无名管道的操作,类似一个队列,后写入的数据不会覆盖之前的数据,会在其后面存储,读取完的数据会从管道里面移除
4. 将读端关闭,向无名管道中写数据,管道破裂,进程收到信号(SIGPIPE),默认这个信号会将进程杀死
5. 当管道中有数据,将写端关闭,读操作可以执行,之后数据读完,可以继续读取(非阻塞),直接返回0
有名管道 (fifo)
有名管道也叫命名管道,在文件系统目录中存在一个管道文件。
管道文件仅仅是文件系统中的标示,并不在磁盘上占据空间。在使用时,在内存上开辟空间,作为两个进程数据交互的通道。
1、查看命令:man 3 mkfifo
2、头文件:
#include <sys/types.h>
#include <sys/stat.h>
3、函数原型:
int mkfifo(const char *pathname, mode_t mode);
功能: 创建有名管道
参数:
pathname: 文件路径名
mode: 八进制 0666
在shell中使用mkfifo命令: mkfifo filename
eg:
mkfifo f1
or
if(mkfifo("fifo",0666) == -1)
{
perror("mkfifo ");
return -1;
}
特点:
1. 有名管道存在文件系统中,数据存在内存中
2. 可以用于无亲缘关系的进程
3. 只有读端和写端同时存在管道才能打开成功。
数据传输特点:
1、读端不存在时,写端写入数据将会阻塞
2、读端意外结束,写端再写数据将会管道破裂,该进程结束
3、有名管道的数据存储在内存中,数据交互在内核中
有名管道和无名管道的异同点
1、相同点
open打开管道文件以后,在内存中开辟了一块空间,管道的内容在内存中存放,有两个指针—-头指针(指向写的位置)和尾指针(指向读的位置)指向它。读写数据都是在给内存的操作,并且都是半双工通讯。
2、区别
有名在任意进程之间使用,无名在父子进程之间使用
信号
简单概念:信号是在软件层次上对中断机制的一种模拟
kill把信号发送给进程或进程组;
raise把信号发送给(进程)自身.
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
成功则返回0, 出错则返回-1
raise(signo); 等价于 kill(getpid(), signo);
alarm();//设置闹钟
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
可以为当前进程定义闹钟,时间到了会发出SIGALRM信号。
每个进程只能有一个alarm,当重新定义时,会重新计时。
如果之前定义了一个闹钟,则这次定义返回的是上次闹钟剩余的时间,否则返回0.
pause();//程序暂停
pause函数的作用,是让当前进程暂停运行,交出CPU给其他进程去执行;
当前进程进入pause状态后,当前进程会表现为“卡住、阻塞住”;
要退出pause状态,当前进程需要被信号唤醒。
信号的三种处理方式:
1.忽略 2.默认 3.自定义信号处理函数
sighandler_t signal(int signum, sighandler_t handler);
//捕获信号,设置信号的处理方式
//handler: SIG_IGN:忽略
// SIG_DFL:默认
// 自定义的信号处理函数
SIGINT : ctrl + c
SIGQUIT : ctrl +\
SIGTSTP : ctrl + z
进程间通信方式
传统进程间通信方式:
无名管道
有名管道
信号
system V的IPC对象:
共享内存(share memory)
消息队列(message queue)
信号灯集
BSD: 套接字
ipcs
ipcs -m :查询显示当前系统的共享内存
ipcs -q :查询显示当前系统的消息队列
ipcs -s :查询显示当前系统的信号灯集
ipcrm -m shmid:删除某个共享内存
ipcrm -q msgid:删除某个消息队列
ipcrm -s semid:删除某个信号灯集
IPC步骤:
key id
ftok ----> shm_get/msg_get/sem_get ---->
shmmat/shmdt/shmctrl
msgctrl/msgsend/msgrecv
semctrl/semop
```
ftok函数
函数原型:
功能:
参数:
返回值:
共享内存share memory
是一种通信效率最高的进程间通信方式,
进程间通信时直接访问内存,不需要进行数据的拷贝。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
步骤:
1. ftok
2. shmget
3. shmat
4. 进程间通信 fork
5. shmdt
6. shmctl
消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
步骤:
1. ftok
2. msgget
3. 进程间通信
4. msgsnd
5. magrcv
6. msgctl
信号灯集
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
步骤:
1. ftok
2. shmget
3. shmat
* 4. semget
* 5. semctl
6. 进程间通信
* 7. semop
* 8. semctl
9. shmdt
10. shmctl
网络编程
进程通信
{
1.无名管道 2有名管道 3.信号
1.共享内存 2.消息队列 3.信号灯集
套接字
}
网络编程
{
历史:1957年 前苏联发射两枚卫星
1958年提出DARPA
1968年6月DARPA提出“资源共享计算机网络”
1974年12月两人正式发表第一份TCP协议详细说明,但此协议在有数据包丢失时不能有效的纠正
ARRPA 网前期有两个缺点{
1.只能连接ARRPA主机
2.数据交互不能保证传输稳定
}
tcp/ip协议
{
1.tcp 解决数据传输问题
2.ip 解决不同网络之间的连接
}
osi七层模型:
应用层 app
表示层 数据加密解析
会话层 建立逻辑名字和物理名字的联系
传输层 确保数据的传输,流量控制
网络层 数据分组,选择路由
数据链路层 把数据组装成可以发送的帧格式
物理层 把数据转化成物理信号
tcp/ip协议模型:
应用层
传输层 tcp/udp
网络层
网络接口和物理层
tcp/ip协议传输的数据格式组装过程
tcp 稳定可靠,有连接,无失序,数据流式
udp 不稳定,无连接,有失序,数据报式
tcp协议
{
ip地址:标识唯一主机 ipv4 4字节 点分形式:192.168.7.33
端口号:标识进程 2字节 0到1023系统 1024-5000应用 5001 - 65535
地址族:说明ip的类型
cs架构
服务器:{
1.创建套接字
int socket(int domain, int type, int protocol);
返回值: 成功返回文件描述符, 失败返回-1
参数:domain:地址族 ipv4 AF_INET
type: 套接字类型 tcp SOCK_STREAM
protocol:协议 0默认协议
2.绑定套接字信息 ip地址 端口号 地址族
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
返回值:成功返回0 失败-1
参数:sockfd:套接字文件描述符
addr:存储信息的结构体地址
{
127.0.0.1 本机回环地址,作于测试
0.0.0.0 本机地址
}
addrlen:结构体大小
大小端:
{
大端存储:低字节存在高位,高字节存低位
小端存储:低字节存在低位,高字节存高位
}
htons() 把短整型转换位网络字节序
ntohs() 把网络字节序转换为短整型
innet_addr() 把字符串转换为网络字节序
inet_ntoa() 把网络字节序转换为字符串
3.监听套接字
int listen(int sockfd, int backlog);
设置监听队列
返回值:成功返回0 失败 -1
参数: sockfd:套接字文件描述符
backlog:监听队列的大小一般3到4
4.被动等待连接 阻塞
accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值:成功返回客户端文件描述符,失败-1
参数: sockfd:服务器套接字
addr:存客户端信息
addrlen:结构体大小
通信:接收客户端信息
处理信息
返回信息
}
客户端{
1.创建套接字
2.绑定/可选
3.连接connect
}
}
三次握手:
SYN请求连接信号 0代表没确认 1代表确认
seq信息编号 记录发送数据的编号
ACK确认信号 0代表没确认 1代表确认
ack确认的包的编号 值应该是seq加1
一:客户端向服务器发送建立连接请求(SYN=1),数据包编号seq=x,客户端从CLOSED->SYN_SENT
二:服务器收到客户端请求:发送建立连接请求(SYN=1),数据包编号seq=y
回复客户端请求(ACK=1, ack=x+1) 服务器从LISTEN->SYN_RCVD
三:客户端收到服务器请求:回复服务器(ACK=1, ack=y+1) 客户端从SYN_SENT->ESTABLISHED
服务器收到最后一条信息之后,从SYN_RCVD-->ESTABLISHED
四次挥手:
一:客户端向服务器发送断开连接请求(FIN = 1),数据包编号u,客户端从ESTABLISHED-》FIN_WAIT_1
二:服务器收到客户端请求:发送(ACK = 1, ack=u+1),剩余数据 (seq = v)服务器从ESTABLISHED-》CLOSE_WAIT 客户端从FIN_WAIT_1-》FIN_WAIT_2
三:服务向客户端发送断开连接请求(FIN=1, seq=w, ack=u+1) 服务器从CLOSE_WAIT-》LAST_ACK
四:客户端收到服务器请求:(ACK=1, ack=w+1) 客户端从FIN_WAIT_2-》TIME_WAIT 等待两个最长报文时间,进入CLOSED
当服务器收到最后一次确认从LAST_ACK-》CLOSED
昨天服务器的两个问题
1.不能连续发送和接收(进程,线程)
{
fork()
while(1)
{
if(pid == 0)
{
while(1)
{
read
}
}
write
}
}
2.不能同时连接多个客户端
while(1)
{
connfd = accept()
if(pid == 0)
{
功能
}
}
并发服务器模型
{
多进程实现
{
优点:相互独立抗干扰
缺点:开辟进程的数量是有限的,占用资源,进程交互比较麻烦
}
eg:
int main()
{
1.创建套接字 socket
2.绑定套接字 bind
3.监听套接字 listen
捕获17号信号,回收子进程资源 signal
while(1)
{
4.被动等待连接 accept
fork()
if(pid == 0)
{
处理客户端请求
if(0==read)
close(connfd);
exit(0);
}
}
close(sockfd);
}
难点:资源回收, signal()捕获17号信号,回收子进程资源
多线程实现
{
优点:交互简单,资源占用小
缺点:抗干扰能力弱,需要考虑共享数据同步互斥
}
int main()
{
1.创建套接字 socket
2.绑定套接字 bind
3.监听套接字 listen
while(1)
{
4.被动等待连接 accept
pthread_create();
pthread_detach();
}
close(sockfd);
}
void*func(int *arg)
{
while(1)
{
处理客户端请求;
if(0 == read)
{
break;
}
}
pthread_exit(NULL);
}
难点:资源回收,pthread_detach() 线程设置为分离属性,子线程自己回收资源
IO多路复用
1.使用多进程,和多个客户端交流,需要读写分离,可以控制发送数据给哪个客户端
}
UDP
无连接,有失序,数据报式,不稳定
服务器{
1.创建套接字 SOCK_DGRAM
2.绑定套接字信息
while(1)
{
交互
}
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
}
客户端{
1.创建套接字 SOCK_DGRAM
2.绑定套接字信息(可省略)
while(1)
{
交互
}
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
}
}
IO模型
{
1.阻塞 read fgets rcv
缺点:阻塞之后没办法执行其他功能
优点:逻辑简单,资源占用不高
2.非阻塞 write 缓存区满(阻塞)
缺点:需要循环遍历,cpu占用高
优点:不会影响其他功能执行
3.信号驱动
4.io多路复用{
select
poll
epoll
}
5.异步IO
在系统注册回调,如果有文件描述符准备好,直接执行回调,控制驱动
}
int fcntl(int fd, int cmd, ... /* arg */ );
参数:fd:需要操作的文件描述符
cmd:对文件操作的命令
不定参数:
F_GETFL (void)获取文件描述符flags
F_SETFL (int)设置文件描述符flags
IO多路复用
{
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:把关心的位图,传递给内核,让内核帮忙轮询,如果有准备好的事件就标记并且返回准备好的文件描述符个数
返回值:成功返回准备好的文件描述符个数
失败-1,0代表超时;
参数:nfds:最大文件描述符加1
readfds:读事件的位图
writefds:写事件的位图
exceptfds:其他事件位图
timeout:struct timeval {
long tv_sec; /* seconds */ 秒
long tv_usec; /* microseconds */微秒
};
void FD_CLR(int fd, fd_set *set); 删除位图中某个元素
int FD_ISSET(int fd, fd_set *set); 判断文件描述符是否准备好 准备好返回真,没准备好返回-1
void FD_SET(int fd, fd_set *set); 把某个文件描述符加入到位图
void FD_ZERO(fd_set *set); 把位图清空
int fd1, fd2, fd3;
fd1 = open()
fd2 = open()
fd3 = open()
fd_set readfd, temp;
FD_ZERO(&readfd);
FD_SET(fd1, &readfd);
FD_SET(fd2, &readfd);
FD_SET(fd3, &readfd);
int nfds = fd3+1;
temp = readfd;
while(1)
{
readfd = temp;
select(nfds,&readfd,NULL,NULL,NULL);
for(i = 0; i < nfds; i++)
{
if( FD_ISSET(i, &readfd))
{
read()
}
}
}
}
select 缺点:需要轮询,需要两次拷贝, 长度有限
poll:解决select长度有限的问题
使用数组存储文件描述符,达到长度可变
int poll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);
返回值 成功个数 失败-1 超时0
参数:fds:存储关心文件描述符的数组首地址
nfds:关系文件描述符的个数
time:超时 -1 阻塞 0非阻塞 》0 时间 ms
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents;
};
epoll
1.创建
int epoll_create(int size);
创建一个epoll句柄
返回值:成功返回文件描述符,失败-1
参数:可处理的文件的范围
2.插入
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
操作epoll
EPOLL_CTL_ADD 添加
Add fd to the interest list and associate the settings specified in event with
the internal file linked to fd.
EPOLL_CTL_MOD 修改
Change the settings associated with fd in the interest list to the new settings
specified in event.
EPOLL_CTL_DEL 删除
Remove (deregister) the target file descriptor fd from the interest list. The
event argument is ignored and can be NULL (but see BUGS below).
参数:epfd, epoll的文件描述符
op , 操作的命令
fd, 操作的文件描述符
epoll_event, 操作的具体内容
3.给内核处理
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
等待内核返回准备好的文件描述符
返回值:成功返回准备好的个数,失败-1,超时0
参数:epoll文件描述符
events 准备好的文件描述符内容
maxevents 数组大小
timeout:-1 阻塞 0非阻塞 >0 ms
4.具体操作
fds[i].events & EPOLLIN
超时检测:
1.使用io多路复用设置超时检查
2.信号unsigned int alarm(unsigned int seconds);
3.设置套接字属性
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
功能:设置套接字属性
返回值:成功返回0,失败-1
参数:sockfd:套接字文件描述符
level:设置的协议是哪一层的
optname:具体指令
optval:根据不同指令传递的值
optlen:结构体大小
设置属性表:level
SOL_SOCKET
---------------------------------------------------
参数optname 宏的作用 对应参数optval的类型
SO_BROADCAST 允许发送广播数据 int
SO_DEBUG 循序调试 int
SO_DONTROUTE 不查找路由 int
SO_ERROR 获的套接字错误 int
SO_KEEPALIVE 保持连接 int
SO_LINGER 延迟关闭连接 struct linger
SO_OOBINLINE 带外数据放入正常数据流 int
SO_RCVBUF 接收缓冲区大小 int
SO_SNDBUF 发送缓冲区大小 int
SO_RCVLOWAT 接收缓冲区下限 int
SO_SNDWAIT 发送缓冲区下限 int
SO_RCVTIMEO 接收超时 struct timeval
SO_SNDTIMEO 发送超时 struct timeval
SO_REUSEADDR 允许重用本机地址和端口 int
SO_TYPE 获得套接字类型 int
SO_BSDCOMPAT 与BSD系统兼容 int
===================================================
IPPROTO_IP
------------------------------------------------------
IP_ADD_MEMBERSHIP 加入到组播组中 struct ip_mreq
IP_MULTICAST_IF 允许开启组播报文的接口 struct ip_mreq
套接字分类
{
1.流式套接字
2.报式套接字
3.unix域套接字 只能进行本机通信
{
struct sockaddr_un {
sa_family_t sun_family; /* AF_UNIX */
char sun_path[108]; /* Pathname */
};
作用:传递文件描述符,传递凭证
}
}
ip地址分类
{
A:一个字节为网络号,3个字节为主机号
学校,国企
以0开始
范围 0.0.0.0到127.255.255.255 0.0.0.0 本机 127.0.0.1回环 以.255结尾广播地址
B:两个字节为网络号,2个字节为主机号
私有
以10开始
范围128.0.0.0 到 191.255.255.255
C:三个字节为网络号,1个字节为主机地址
私有
以110开始
范围192.0.0.0 到 223.255.255.255
D:组播地址
以1110开始
范围224.0.0.0 到239.255.255.255
E:保留地址
以11110开始
}
广播
{
向广播地址发送消息
发送广播{
1.设置允许发送广播选项 setsockopt()
2.向广播地址发送信息 以255结尾
}
接收广播{
1.我绑定的地址需要是一个广播地址或者是0地址
}
}
组播
{
加入组播地址才能收到发给组播地址的信息
struct ip_mreq
{
struct in_addr imr_multiaddr; //组播地址
struct in_addr imr_interface; //加入到组播组中的地址
};
发送组播{
1.创建套接字
2.允许发送组播地址
}
接收方{
1.创建套接字
2.绑定
3.加入到组播组中
}
}
}