编译和链接
文件类型介绍
- .c c语言源文件
- .h 程序所包含的头文件
- .i 已经预处理过的C源代码
- .s 汇编语言源代码
- .o 编译后的目标文件
gcc编译器使用
gcc可以编译其他的语言,具体有预处理,编译,汇编,链接四个步骤
常用选项
- -o 生成目标
- -c 取消链接步骤,编译源码并最后生成目标文件 也就是编译后停下来
- -S 生成汇编语言文件后停止编译(.s文件) 也就是汇编后停下来
- -L 指定链接库文件的路径,一般分为静态库和共享库
- -l 指定要链接的库的名称
- -I 指定头文件的目录
- -D 传递宏定义
要注意多文件的编译 - 示范
gcc -Wall fun.c main_fun.c –o main_fun
gcc –Wall –c main_fun.c –o main_fun.o
正式内容
魔术变量
__FILE__
文件名__FUNCTION__
函数名__LINE__
当前是第几行
标准流
程序在运行的时候会默认有三个流stdin(文件描述符为0),stdout(文件描述符为1),stderr(文件描述符为2)分别是标准输入,标准输入,错误信息 也就是说这三个变量是全局变量
常用类库(如果想知道函数的用法可以使用man查询)
可以安装手册来快速查看函数被使用是要的类库
- stdio.h 标准输入输出库
- fgets 从指定的流读取内容(一般常用这个代替scanf)
- stdbool.h
- bool 可以使用基本数据类型bool 这个类型代替的是
_Bool
- bool 可以使用基本数据类型bool 这个类型代替的是
- stdlib.h
- exit 结束进程
- system 创建一个子进程执行command
- atoi 将字符串转化成整型
- limits.h 里面定义了一些基本数据类型的最大值最小值
- INT_MAX int类型的最大值
- error.h 里面定义了错误的相关处理等信息
- errno 错误的错误码
- strerror 将错误码转化为实际的错误信息
- string.h
- bzero 将一段内存区域清零
- time.h 里面定义了一些和时间相关的函数
- time 获取时间戳
- localtime 将时间戳转化为当前的时间 这个会返回一个tm_t的结构体,可以使用strftime来转换成指定格式
- strftime 格式化时间
- unistd.h unix标准头文件 里面定义了一些常用的unix函数
- sleep 睡眠当前程序
- close 关闭打开文件 参数是一个文件描述符
- read write lseek 文件IO的相关函数
- _exit 结束当前进程,但是不刷新标准输出缓冲区
- fcntl 里面定义了文件IO的相关函数
- open 以文件IO的形式打开文件 这个是可以打开设备文件的 返回值是一个文件描述符
- dirent.h 关于目录读取的相关内容
- opendir readdir closedir
- sys/stat.h 存放系统相关命令的内容
- chmod fchmod
- stat lstat 获取文件属性
- sys/types.h 存放一些固定的系统相关的结构体内容,大部分的库相关的结构体都会定义在这里
- sys/wait.h
- wait 等待子进程结束 并接收返回值
- pthread.h posix标准的线程库 这个是第三方库,编译时要指定-lpthread 不然会出错
- semaphore.h 信号量相关库
- sys/ipc.h ipc通信相关库 ipc即进程system IPC 下面的库和这个搭配使用
- sys/shm.h 创建共享内存相关库
- sys/msg.h 消息队列相关库
- sys/sem.h 信号灯集相关库
- sys/socket.h 网络相关库
- socket 创建一个套接字
- bind 绑定套接字
- listen 将套接字主动变为被动
- accept 阻塞等待客户端请求
- conncet 连接服务器
- send,sendto,recv,recvfrom 接收发送数据
- sys/select.h
- select fd_set监听函数
- sqlite3.h sqlite相关库
- sqlite3_open 打开数据库
- sqlite3_exec 执行sql语句
静态库和共享库
静态库是将被程序使用的函数等代码复制到程序中,所以程序在运行时无依赖,而共享库是将不将库文件复制到程序中,所以程序体积不会有太大变化,而且易于升级库文件,一般推荐使用共享库 共享库一般需要注意如何让系统能找到要加载的共享库
库的扩展名
- 静态库 .a
- 共享库 .so
进程相关
c语言创建进程要使用到unistd.h库中的fork函数 fork函数创建进程后子进程会复制父进程的所有资源,从fork的下一条指令开始执行,若父进程先结束,子进程将会被init进程收养,转为后台执行,若子进程先结束,如果父进程未回收好子进程,子进程将会变成僵尸进程
进程相关函数一般使用unistd.h库,但是这是在linux情况下,windows下面情况不确定
进程相关术语
- 中断 中断一般是指在cpu正常工作时,硬件可以发出中断,cpu会马上处理中断,处理完后返回继续工作
进程类型
一般linux进程分为下面三种
- 交互进程 shell就是这种类型
- 批处理进程 顺序执行作业
- 守护进程
守护进程
守护进程的生命周期一般比较长,通常在系统启动时运行,系统关闭时结束。Linux以会话、进程组的方式管理进程,每个进程属于一个进程组,会话是一个或多个进程组的集合,通常用户打开一个终端时,系统会创建一个会话,一个会话最多有一个终端,所有通过该终端运行的进程都属于这个会话,终端关闭时,所有该会话下的所有相关进程都会被释放
Linux下C语言守护进程创建
- 创建子进程,父进程退出
- 这样是为了让子进程变成孤儿进程,被init进程收养 可以通过fork的返回值来判断是子进程还是父进程(返回值等于零为子进程)
- 子进程创建新会话
- 这样是为了让子进程成为新的会话组长,同时脱离原先的终端
- 脱离终端后 如果没有创建终端stdin/stdout/stderr 无法在使用
- 更改当前工作目录
- 守护进程一直在后台运行,其工作目录不能被卸载 所以要重新设定当前工作目录
- 重设文件权限掩码
- 文件权限掩码设置为0 只影响当前进程
- 关闭打开的文件描述符
- 这样是为了关闭所有从父进程继承的打开文件
// 创建守护进程实例 创建一个守护进程 用来每隔一秒获取时间记录在time.log中
int main(){
pid_t pid;
FILE *fp;
time_t t;
int i;
if((pid=fork())<0){
perror("fork"); //创建子进程失败
exit(-1);
}
else if (pid>0){
exit(0); // 关闭父进程
}
setsid(); // 开启新的会话
umask(0); //更改文件掩码
chdir("/tmp") // 修改工作目录
for(i=0;i<getdtablesize();i++){ // 关闭父进程的文件描述符
close(i);
}
if((fp=fopen("time.log","a"))==NULL){
perror("fopen");
exit(-1);
}
while(1){
time(&t);
fprintf(fp,"%s",ctime(&t));
fflush(fp);
sleep(1);
}
}
进程间通信
- 早期UNIX进程间通信
- 无名管道
- 无名管道只能用于具有亲缘关系的进程,而且是单工模式 创建时会返回两个文件描述符,分别用于读写管道 如果想要实现读写需要两个无名管道
- 有名管道
- 有名管道对应管道文件,可以用于任意进程之间通信 读写等操作靠的是文件IO 要先创建有名管道,然后读写端同时开启,如果只有一端的话,打开管道会阻塞 内容存放在内存中
- 信号
- 信号是在软件层次上对中断机制的一种模拟,linux通过信号通知用户进程,不同的信号代表不同的事件,进程有三种响应方式 缺省 忽略 捕捉
- 无名管道
- System V IPC 每个IPC对象都有一个唯一的ID,创建后会一直存在,直到被显式删除 且每个IPC对象有一个唯一的key
- 共享内存
- 共享内存是一种最高效的进程通信方式,进程可以直接读写内存,不需要任何数据的拷贝,所以效率最高,访问方式就好像直接访问内存,使用前要将内存进行映射而且使用时要注意同步和互斥配合使用
- 使用步骤
- 创建/打开共享内存
- 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
- 读写共享内存
- 撤销共享内存映射 如果没有主动撤销,进程结束自动撤销
- 删除共享内存对象
- 消息队列
- 消息队列就是一个消息的列表。用户可以在消息队列中添加消息,读取消息,消息队列可以按照类型来发送/接收消息 消息可以自定义,但是必须有一个
long mtype
成员,代表消息类型,其他成员都属于消息正文
- 消息队列就是一个消息的列表。用户可以在消息队列中添加消息,读取消息,消息队列可以按照类型来发送/接收消息 消息可以自定义,但是必须有一个
- 信号灯集
- 信号灯集是一个或多个计数信号灯的集合,信号灯的解释可以看下面的信号量,不过这里的信号灯集是可以用于进程之间的通信的,信号灯集可以申请多个资源时解决死锁问题
- 步骤
- 打开/创建信号灯
- 初始化信号灯 如果是创建的进程要初始化信号灯
- P/V操作
- 删除信号灯
- 共享内存
- 套接字
exec函数族
unistd.h
进程调用exec函数族执行某个程序 进程当前内容被指定的程序替换,可以实现父子进程执行不同的程序
- execl 执行命令
- execlp
int execl(char *path,char *arg,...)
int execlp(char *file,char *arg,...)
execv和execvp差别和上面类似 file会在环境变量PATH中找程序
int execv(char *path,char *argv[])
int execvp(char *file,char *argv[])
返回EOF代表失败 所有exec函数族最后一个参数要加NULL
examples
if(execl("/bin/ls","ls","-a -l","/etc",NULL)<0)
- execv
- execvp
进程相关联函数
默认函数不写库都在unsitd.h
- 回收子进程
- wait 随机回收一个子进程,那个先结束就先回收(子进程自动结束),回收未成功会处于阻塞状态 失败会返回EOF 可以通过一些特定的函数来获取子进程的返回值等其他信息
- waitpid 回收指定pid的子进程 可以指定回收方式 参数这里不写出了
- 进程通信相关函数
- pipe 创建无名管道
- mkfifo 创建有名管道文件
- 信号 可能要使用signal.h
- kill 对指定的进程发送信号
- raise 对当前进程发送信号
- alarm 指定定时器 到时间会向当前进程发送定时器信号 一个进程只能设定一个定时器 第二次调用会重新设定定时器
- pause 进程阻塞 知道被信号中断
- signal 指定信号的响应方式
- 如果想要知道更多相关函数查阅手册
- ftok 创建一个IPC所需要的唯一的key值
- 共享内存
- shmget 创建共享内存
- shmat 映射共享内存地址
- shmdt 撤销共享内存映射
- shmctl 共享内存控制 可以通过执行这个函数进行删除共享内存等操作
- 消息队列
- msgget 打开/创建消息队列
- msgsnd 向消息队列发送消息
- msgrcv 从消息队列接收消息
- msgctl 控制消息队列
- 信号灯集
- semget 打开/创建信号灯集
- semctl 可以进行初始化信号灯集等操作
- semop 信号灯P/V操作 需要用的特定的结构体
线程相关
下面相关函数在pthread.h库中
在任何一个线程中调用exit会导致进程结束
线程相关术语
- 同步 同步是指多个任务按照约定的先后次序相互配合完成一件事
- 信号量 可以用来实现线程间同步的一种机制,信号量代表某一类资源,其值表示系统中该资源的数量,资源足够就可以运行不然就会阻塞,可以通过信号量来实现锁 信号量是一个受保护的变量,只能通过三种方式访问
- 初始化
- P操作(申请资源) 资源不做会阻塞
- V操作(释放资源) 通知系统资源数增加了
- 临界资源 一次只允许一个任务(进程,线程)访问的共享资源
- 互斥锁 使用互斥锁保证一个资源在访问时是临界资源
线程相关函数
- pthread_create 创建线程
- pthread_join 等待线程执行完成,并接受返回值
- semaphore.h 信号量库
- sem_init 初始化信号量
- sem_wait P操作
- sem_post v操作
- pthread_mutex_init 初始化互斥锁
- pthread_mutex_lock 获得锁
- pthread_mutex_unlock 释放锁
- pthread_cond_init 初始化条件
- pthread_cond_wait 进入等待集合
- pthread_cond_singal 唤醒等待集合
- pthrad_exit 退出线程
- fflush 刷新缓冲区
- 这里省略了相关参数和需要的变量类型,如果需要请查手册
网络相关
基本知识点
c/s模式基本创建过程
TCP:
- Server:socket(),bind(),listen(),accept()
- Client:socket(),bind(),connect(),close()
UDP
- Server:socket(),bind(),recvfrom(),sendto()
- Client:socket(),bind(),sendto(),recvfrom()
网络字节序
一般来说本地字节序是采用大端法表示的,而网络字节序是采用小端法表示的
TCP/UDP编程
TCP相关函数(下面函数一般返回值小于0代表发生错误)
- socket 创建一个套接字(下面是参数介绍和参数常用的值) 成功返回文件描述符
- int domain
- AF_INET,AF_INET6 ipv4和ipv6
- int type 套接字类型
- SOCK_STREAM 流式套接字 唯一对应于TCP
- SOCK_DGRAM 数据包套接字 唯一对应于UDP
- protocol 一般填0 使用原始套接字的时候需要注意
- int domain
- bind 绑定IP地址等信息
- int sockfd 通过socket得到的fd也就是文件描述符
- sockaddr *addr 这个参数一般不适用sockaddr这个结构体而采用sockaddr_in这个结构体,如果是IPV6使用sockaddr_in6或者sockaddr_storage,关于ipv6详细的信息使用man 7 ipv6查看
- sockaddr_in 使用这个时先使用bzero将变量的内存区域清零
- sin_family 协议类型 2字节
- sin_port 端口号(端口为0代表自动分配端口) 2字节
- sin_addr 地址 4字节
- 绑定到任意IP上,如果这样写的话,只要发送到这个机器上,端口一致,协议一致就能被接收到,如果有两块网卡照样能接收到 sock_in.sin_addr.s_addr=htonl(INADDR _ANY)
- sockaddr_in 使用这个时先使用bzero将变量的内存区域清零
- socklen_t addrlen 地址长度
- listen 监听端口,将主动套接字转化为被动套接字
- sockfd 套接字的文件描述符
- backlog 同时允许几路客户端与服务器正在连接(正在进行三次握手)一般填5 ARM最大为8
- accept 阻塞等待客户端连接请求 ,成功时返回已经建立好连接的新的newfd(文件描述符)
- 这个的参数和bind一致,不过用处不一样,这个的sockaddr可以获得连接客户端的ip地址等信息,如果不关心可以设置为NULL
- connect 向服务器发送连接请求 后面的读取发送操作可以通过socket返回的文件描述符
- 参数和bind一致,不过地址要新建一个sockaddr_in用来记录服务器的地址
- send 发送数据到连接端(这个的用法和write差不多一致不过新增了一个参数flags)
- flags 表示发送方式一般填0,下面是其他的值的意思
- MSG_DONTWAIT 以非阻塞的方式发送,也就是立即发送
- MSG_OOB 发送TCP类型的带外数据(out-of-band)
- flags 表示发送方式一般填0,下面是其他的值的意思
-
- recv 接收来自连接端的数据(这个的用法和read差不多一致不过新增了一个参数flags)
- flags 表示发送方式一般填0,下面是其他的值的意思
- MSG_DONTWAIT 非阻塞版本
- MSG_OOB 接收TCP类型的带外数据(out-of-band)
- MSG 从缓冲区的顶端取数据,但是不改变缓冲区,也就是说缓冲区下一次读仍然还是原来的内容。
- flags 表示发送方式一般填0,下面是其他的值的意思
- recv 接收来自连接端的数据(这个的用法和read差不多一致不过新增了一个参数flags)
- setsockopt
- 用法:允许绑定地址快速重用
int b_reuse=1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&b_reuse,sizeof(int));
- 字节序转换相关函数
- inet_pton 将ip地址转化为网络字节序的形式 成功返回1
- inet_pton(AF_INET,“192.168.12.12”,(void *)&target)
- inet_ntop 将网络字节序转化为本地字节序的形式 成功返回1
- inet_ntop(AF_INET,address,target,len)
- 主机字节序转换成网络字节序
- htonl,htons这两个唯一的不同在于接受参数的类型不同一个接受u_long,一个接受u_short
- 网络字节序到主机字节序
- ntohl,ntohs这个和上面的不同处相似
- inet_pton 将ip地址转化为网络字节序的形式 成功返回1
UDP相关函数(下面函数一般返回值小于0代表发生错误)
- sendto 这个函数和recv差不多,但是多了发送者信息的参数也就是sockaddr这个类型,还有目标信息的长度,可以通过这个来获取是谁发送的数据
- recvfrom 这个函数和recv差不多,但是多了目标信息的参数也就是sockaddr这个类型,还有目标信息的长度
IO多路复用模型
服务器端常用的IO模型有:
- 同步阻塞IO
- 同步非阻塞IO
- IO多路复用模型 也称为异步阻塞IO
- 异步IO 也称为异步非阻塞IO
介绍
多路复用IO即在同一个线程内同时处理多个TCP连接,最大优势是减少系统开销,不必创建/维护过多的线程
大致过程:
- 将关心的文件描述符加入到集合中(fd_set),文件描述符的分配是递增的
- 调用select/poll函数去监控fd_set中那些文件描述符(阻塞等待集合中文件描述符有数据)
- 当有资源更新时,select会停止阻塞并且更新fd_set,然后退出,在select之后fd_set中存放有数据的文件描述符
- 判断文件描述符是否在更新后的fd_set中
- 依次处理文件描述符中的数据
IO多路复用相关函数
- fd_set 存放文件描述符的集合
- fd_set的实现原理
- 在linux中一个进程可以分配的文件描述符最多1024个,也就是0-1023,但是标准流占3个,所以我们取1024位就可以了,也就是128个字节,32个long型。
- 下面时fd_set相关操作的宏定义
- void FD_ZERO(fd_set *fdset) 将fd_set的内存区域清空
- void FD_SET(int fd,fd_set *fdset) 添加一个文件描述符到fd_set中
- void FD_CLR(int fd,fd_set *fdset) 在fd_set中删除一个文件描述符
- int FD_ISSET(int fd,fd_set *fdset) 判断一个文件描述符是否在fd_set中
- fd_set的实现原理
- select 阻塞检查fd_set中是否有文件描述符数据更新(下面是参数介绍)
- int maxfd 在fd_set中在该值下的文件描述符会被监听
- fd_set *readfds 监听是否有可读数据的fd_set(这些如果fd_set如果没有可填NULL)
- fd_set *readfds 监听是否有可写的fd_set
- fd_set *readfds 异常fd_set
- struct timeval *timeout 设置超时返回
- epoll 此函数需要后续查找学习
- poll 这个函数也可以用于多路复用,具体使用省略
网络扩展
相关函数介绍
下面的函数有些没有做详细的介绍,要知道详情可以使用man查询
- gethostbyname 可以根据域名获取IP地址
- setsockopt 设置套接字的属性,具体函数和相关参数设置参照图片存放处
网络超时优化
一般来说,套接字等待接收数据,如果没有数据会一直阻塞,如果对此方法不满意可以采取下面的解决办法
- 采用多路复用解决
- 使用setsockopt设置设置接收超时
- 设置定时器,捕捉SIGALRM信号参考代码如下
void handler(int sinno){return ;}
struct sigaction act;
sigaction(SIGALRM,NULL,&act);
act.sa_handler=handler;
act.sa_flags &=~SA_RESTART;
sigaction(SIGALRM,&act,NULL);
alarm(5);
if(recv()<0)[]
网络连接保持
- 每个一段时间,一方发送一点数据,对方给出特定的应答,如果超出设定次数大小的时间内没有应答,则认为异常
- 通过改变套接字的属性来实现,具体看图片
广播,组播
- 广播的步骤
- 广播发送
- 创建用户数据报套接字
- 设置套接字属性,缺省创建的套接字不允许广播
- 接收方地址指定为广播地址,指定端口信息
- 发送数据包
- 广播接收
- 创建用户数据报套接字
- 绑定本机IP地址和端口(绑定的端口必须和发送方指定的端口相同)
- 等待接收数据
- 广播发送
- 组播的步骤
- 组播发送
- 创建用户数据报套接字
- 接收方地址为组播地址(一般组播地址为D类地址)
- 指定端口信息
- 发送数据包
- 组播接收
- 创建用户数据报套接字
- 加入组播组
- 绑定本机IP地址和端口(绑定的端口必须和发送方指定的端口相同)
- 等待接收数据
- 组播发送
// 加入组播代码
struct ip_mreq mreq;
bzero(&mreq,sizeof(mreq));
mreq.imr_multiaddr.s_addr=inet_addr(group_address);
mreq.imr_interface.s_addr=htol(INADDR_ANY);
setsockopt(fd,IPPORT_IP,IP_ADD_MEMBERSHIP,&mreq,sizeof(mreq));
unix域套接字
unix域套接字,在易用性和效率上都有不错的效果,使用上面和TCP,UDP都差不多,不过改了域类型和bind的时候绑定地址有点特殊
unix域套接字在使用方法和TCP和UDP没什么区别,不过域类型改为AF_LOCAL或AF_UNIX,不过在绑定地址的时候,要注意使用sockaddr_un类型, 里面有两个参数一个是域类型(sun_family)AF_UNIX,一个是文件的绝对路径(sun_path),但是文件必须不存在,因为绑定成功后文件会存在在内存中
Sqlite3
请查找相关sqlite的手册,下面给出代码的示范,只包括简单使用
int scallback(void *data,int argc,char **argv,char **azColName){
sqliteR *tdata=(sqliteR *)data;
int i=0;
int len=0;
char buf[NDATAL/4];
if(!tdata){
return 0;
}
len=tdata->length;
bzero(tdata->data[tdata->length], sizeof(tdata->data[0]));
for(;i<argc;i++){
bzero(buf, sizeof(buf));
snprintf( buf,NDATAL/4-1,"%s:%s",azColName[i],argv[i]);
strncat(tdata->data[len], buf, NDATAL/4);
}
tdata->length+=1;
return 0;
}
void test(){
Perror(sqlite3_open("/Volumes/Other/Code/dictionary/inline dictionary/inline dictionary/data.db", &db), "sqlite3 open");
sqliteR data;
data.length=0;
char *zErrMsg=0;
char *sql="select username,passWord from User limit 5";
int rc;
rc=sqlite3_exec(db,sql, scallback, (void *)&data, &zErrMsg);
printf("%d",rc);
if( rc != SQLITE_OK ){
fprintf(stderr, "SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}else{
fprintf(stdout, "Operation done successfully\n");
}
sqlite3_close(db);
}