C语言服务器编程必备常识

入门

包含了正确的头文件只能编译通过,没链接正确的库链接会报错。

一些常用的库gcc会自动链接。

库的缺省路径/lib  /usr/lib /usr/local/lib

不知道某个函数在那个库可以nm -o /lib *.so | grep 函数名

man sin 会列出包含的头文件和链接的库名。

man 2 sin 2表示系统调用,3表示c库函数

一旦子进程被创建,父子进程一起从fork处被创建。

创建子进程为了争夺资源。

重定向用dup2函数

kill -l查看信号种类

pthread_mutex不跨进程,ipc中的信号量跨进程,但linux不支持无名信号量。

信号灯的主要用途是保护临界资源。

多进程访问共享内存,用信号量同步。

alarm(5)5秒后向自己发送SIGALARM信号,缺省处理是结束进程,不自定义就会结束进程。

通过对信号集加减信号,确定信号屏蔽字。

在信号处理程序被调用时,操作系统建立的新信号屏蔽字包括正在被递送的信号,如果此时这个信号再次发生,将阻塞到前一个处理完,多次发生不排队只处理一次。

sa_mask会被加到信号屏蔽字中。

netstat -an|grep A |grep ESTABLISHED | grep B,查看ip为A的服务器是否在端口B建立了连接

由于我们的连接都是常连接,故可以按照客户端与服务器端建立的连接端口进行判断。

IP协议是网络层协议,主要发送数据包。

UDP基于IP协议,用在传输层。

TCP协议建立在IP协议之上,可靠的、按顺序发送的。

TCP连接三次握手:

客户机向服务器发包。

服务器给客户机回包。

客户机收到包,向服务器发送确认信息完成连接。

服务器收到确认信息也完成连接。

ioctl可以控制所有文件描述符的情况。

循环服务器:

UDP服务器,UDP是非面向连接的,没有一个客户机可以老是占着服务器。

TCP循环服务器一次只能处理一个,close后才能处理下一个。

TCP并发服务器:

fork子进程来处理。

创建子进程消耗资源。

并发服务器:

多路IO复用。

当我们创建一个正常的TCP套接字的时候,我们只处理内容,不负责TCP头部和ip头部,自己创建头部使用setsockopt。

网络程序一般是多进程加上多线程。

g++参数-pg产生gprof性能信息,gprof好像是g++自带的

(gdb)make使你能不退出gdb就能产生就重新产生可执行文件 , shell 不退出gdb就执行shell

file  a.out可以在gdb模式下载入程序。

服务器端:

socket->bind->listen【建立监听队列,监听socket】->【返回连接socket】accept【从监听队列取请求,成功时返回新的socket,与传入的第一参数不同,标识被接受的这个连接】

客户端:

socket->connect

进阶

linux中代替数据库管理文件可能会出现文件数超过linux可管理数的问题。

tcpip协议族:

上层协议使用下层协议提供的服务。

应用层的东西最后需要在内核中实现,会需要应用空间和内核空间的切换。

IP数据太长要分片。

IP协议的核心是数据报路由。

路由表、跳转、自动更新。

socket含义ip:port。

称其为socket地址。

字节序是按字节考虑的,和位无关。

cpu累加器一次装载4字节(32位机)。

网络上传的一定是大端字节序,各主机按自身情况转化。

java虚拟机采用大端字节序。

不同输入调用两次函数,如果发现后面结果覆盖前面结果,说明函数不可重入。

函数内部如果用静态变量存储结果,就不可重入。

将一个地址和socket绑定称为给socket命名。

0-1023端口普通用户不能使用,有默认用途。

accept只是从监听队列中取出连接,不论连接处于何种状态。

connect(fd,..)一旦连接建立成功,fd就唯一标识了这个连接,客户端就可以读写fd和服务器通信。

对socket执行close只减少连接数,fork会使引用数加1。

无论如何都要终止连接用shutdown。

read和write同样适用于socket。

用于TCP数据流的是send recv。

UDP数据是recvfrom sendto。

没有进程读管道的时候【例如close(fd[0])了】还往管道写数据将引发SIGPIPE。

把STDOUT_FILENO关闭,dup(连接socket),这时dup返回最小可用描述符1【返回的文件描述符和原有描述符指向相同文件】,此时printf回返回给客户端,而不是打印在屏上。

sendfile将真实文件传给socket。

splice用于在两个文件描述符间移动数据,零拷贝,用于socket和管道之间互相定向。

tee用于两个管道之间复制数据。

IO处理单元是一个专门的接入服务器,它实现负载均衡。

请求队列是系统内部各单元之间通信方式的抽象,一般实现为池。

阻塞和非阻塞是对文件描述符而言的。

非阻塞IO一般和IO通知机制一起使用,如IO复用或SIGIO信号。

IO复用本身是阻塞的,提高效率是因为同时监听多个事件。

同步就是协同步调,按预定的先后次序进行运行。

处理客户连接就是读写描述符,就是IO,所以IO单元被定义为接入服务器。

并发不适用于计算密集型,因为任务切换会降低效率,适用于IO密集型,如经常读写文件、访问数据库。

池就是预先静态分配资源,到时可以快速使用。

避免了对内核的频繁访问。

提升性能方法:

池、避免数据复制、上下文切换【线程数大于cpu数时】和锁。

读写锁可以减少锁的粒度适用于读多写少的情况。

epoll需要使用一个额外的描述符维护事件表。

EPOLLONESHOT确保只有一个线程处理某个socket。

sigaction结构体中的sa_mask设置信号掩码,确切的说是在进程原有信号掩码的基础上增加信号掩码,以指定哪些信号不能发送给本进程。

sigset_t 每个元素的每个位表示一个信号,所以相同的信号只能表示一次。

子进程有和父进程相同的信号掩码,但挂起信号集【发送但是被阻塞的信号】为空,就是说阻塞的信号是不可能发给子进程的。

应用程序使用信号集sigset_t前,应调用sigemptyset或sigfillset一次,否则信号集初始状态不清楚。

最小时间堆可以达到定时器的效果。

waitpid用NOHANG就是非阻塞的了。

socketpair创建全双工管道的系统调用。

对信号量的操作成为P(传递,进入临界区)V(释放,退出临界区)。

最简单的二进制信号量,只有0和1.用一个普通变量模拟是不行的,因为检测和减1无法原子完成。

linux上的线程使用clone系统调用创建的进程模拟的。

目前可以实现跨进程的线程同步

被pthread_cancel的线程可以决定是否允许被取消以及如何取消。

销毁一个已经加锁的互斥量将导致不可知的后果。

互斥量属性设置中可以设置跨进程共享互斥量。

pthread_mutexattr_setpshared()

加锁-》pthread_cond_wait暗含解锁,确保能检测到条件变量的任何变化。

有些函数不可重入主要是因为内部使用了静态变量。

多线程程序中的一个线程调用fork,只复制调fork的那个线程。

互斥量的状态也继承,此时容易出现死锁。

所有线程共享信号处理函数,共享进程的信号。

所以需要专门线程处理所有信号。

进程池:

典型的是3-10个。

线程池中的线程数量应该和cpu数量差不多。

通信【通信:

传递数据】父子进程间可以使用管道,多线程间使用一个全局数据即可。

pthread_create当线程函数是类的成员函数时,必须为静态函数【确保没对象时也可以使用】,由于静态成员函数只能访问静态成员,要访问动态成员需要函数内部用单例或将类的对象作为参数传给函数。

SA_RESTART被信号中断的系统调用再信号处理结束后继续执行。

将线程池或进程池中个数减少为1,便于调试逻辑。

然后逐步增加数量,看同步。

进程类[i].fd 通过给不同i的fd传递数据,调用不同的进程工作。

m_sub_process[i].pid=fork()【fork了maxnum次】。

线程池:

线程函数一起都启动,启动后进入while(!stop)循环,不断的锁队列,取任务。

POSIX线程

只有互斥量的主人能够解锁它。

线程的堆栈受限。

线程结束方式要么从线程函数return,要么调用pthread_exit,进入终止态,直到被分离或被连接。

创建不需要连接的线程应该使用detachstate属性建立线程使其自动分离。

pthread_join会阻塞调用者,直到被join的线程结束,join返回被连接的线程也分离,所以只能被join一次,下一次就错误了。

使用条件变量时必须保证如果有线程等待,则该线程等待后必然会收到信号(if/while)

条件变量可以使线程处于等待状态而不消耗资源。

条件变量必须跟一个互斥变量一起使用,因为条件变量就是共享的全局数据?

【条件和锁结合共同保护共享数据】status = pthread_cond_wait(&alarm_cond, &alarm_mutex);

没有条件变量,程序员可用使用轮询某个变量来实现停等-通知同步,但是非常消耗系统资源。

如果确定线程不需要被Join, 则申明为Detached可以节省系统资源

pthread_self获得自身的ID,只能通过ID操作线程。

main是主线程,主线程停止所有线程也停止,main中调用pthread_exit,这样进程就必须等待所有线程结束才能终止。

通过向pthread_t(ID)=pthread_create传递线程函数地址和函数参数来创建线程。

注意当前线程从pthread_create返回前,新创建的线程可能已经运行完毕了。

舀水桶类似一个互斥量:

桶用来保护“舀水”临界区【访问临界资源(共享数据?

)的那段程序是临界区】。

或者将桶理解为:

用来确保一次只能由一个人舀水的不变量。

在访问共享数据的代码段周围加锁互斥量,则一次只能有一个线程进入该代码段。

pthread_mutex_t表示互斥量,不能拷贝,可以拷贝指针。

当调用pthread_mutex_lock时,如果互斥量已经被锁住,线程将被阻塞。

调用pthread_mutex_trylock时不会阻塞,会返回EBASY,可以做其他的事情去。

互斥量的本质是串行执行。

解决死锁的两种方法:

一,规定加锁顺序;

二,trylock如果不行回退,解锁所有已加锁的互斥量

sched_yield()将处理器交给另一个等待处理的线程,如果没有等待处理的线程。

立即返回。

sleep()可以确保其他线程有机会运行。

按照相反的顺序解锁,有助于减少线程做回退操作的可能。

因为同一个线程函数中加锁顺序是一样的。

对于不同的线程函数顺序应该不重要

线程运行于解锁和阻塞之间时,其他线程才能改变共享数据状态。

此时共享状态的改变,本线程是无法知道的。

->需要条件变量。

队列满,队列空,满空就是条件变量。

动态初始化的条件变量需要pthread_cond_destroy来释放。

静态初始化的不必释放。

释放前确保其他线程不使用他。

在阻塞线程之前,条件变量等待操作pthread_cond_wait将解锁互斥量,重新返回线程之前,会再次锁住互斥量。

子线程只在pthread_cond_wait等待的短时间内可以加锁,修改共享数据,然后解锁。

pthread_cond_timedwait的意思就是我在这里等time时间,如果时间内条件变量变了,或者不变,我都要跳出while(谓词)的循环,按情况处理。

pthread_cond_wait和pthread_cond_signal必须同时发生才能成功。

互斥量:

条件变量是 一对多的关系

当线程调用pthread_create时,她所能看到的内存值也是它建立的线程能看到的,之后的线程不一定能看到。

线程解锁互斥量时所看到的的数据,也能被后来直接锁住相同互斥量的线程看到。

解锁后写入的数据,不必被其他线程看见(因为那不是用来同步的数据,没必要所有人看见,同步就应该加锁)。

线程终止,取消,从启动函数返回,pthread_exit时看到的数据,能够被连接该线程的其他线程看到。

终止后写入的数据不会被看到。

线程发信号或广播时看到的内存数据,可以被唤醒线程看到。

之后写入的不会。

线程分配的堆栈和堆空间是私有的,除非传给其他线程指针。

register(寄存器变量)和auto变量(大部分变量默认auto)(static变量的生命期长)中的数据可随时读取,像在同步程序中一样

两个处理器将各自的高速缓存中的数据写入主存的顺序是不一定的,即使写到相应高速缓存的顺序有先后之分。

同一线程写数据也未必按照顺序刷新进内存,这使得其他线程读取结果不对。

锁住互斥量->内存屏障->内存屏障->解锁互斥量

使用线程的方式:

流水线、工作组(工作线程在数据的不同部分操作)、C/S。

pthread_attr_setdetachstate (&_attr, PTHREAD_CREATE_DETACHED);说明在创建线程后,我们不在需要使用线程ID。

不变量(Invariant):

程序所做的一些假设,特别是指变量之间的关系。

判定条件(Predicates):

描述不变量状态的逻辑表达式。

pthread_kill(thdid, SIGTERM)给特定线程发信号

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20201215A0GVZP00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 yunjia_community@tencent.com 删除。

扫码关注云+社区

领取腾讯云代金券