线程池
线程池的特点就是尽量减少创建和销毁线程的开销,缺点是不利于保护共享资源。现在公司用的多都是封装好的线程池,比如qt框架的QThread::QThread(QObject *parent = Q_NULLPTR)。
这一方面带来好的好处就是少了很多造轮子的过程,qt是我见过帮助文档最好的框架了,但是手撕线程池的也能让自己对其内部实现更加清晰。
在Linux下线程和进程其实是同一种东西。
**struct task;的结构体,fork()和pthread_create()虽然是不同的函数,但是在内部实现都调用了一个do_fork的函数加入到链表中。**当调用fork()的时候,子进程是几乎完整得拷贝了用户态的所有数据,堆栈之类的。以及大部分内核态的数据,除了未决信号集屏蔽,和文件fd。所需要的开销是很大的。这个task中可能存储了进程的调度,以及父进程的id信息。
pthread_create()调用了do_fork以及一些特定的参数,就会创建一个轻量级的线程,一起共享线程之间的数据。
其中进程开销最大的就是1个CPU下,上下文切换,每秒少则数十次,多则数千次的上下文。而进程除了上下文切换浪费时间,进程之间交互数据也需要特殊的手段,包括共享内存,QSharedMemory(const QString &key, QObject *parent = Q_NULLPTR);管道,本地套接字等。
为了得到多条代码执行流而复制整个内存,负担过重!
1.读取配置文件配置信息
//head.h头文件声明
#ifndef __HEAD_H__
#define __HEAD_H__
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <dirent.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netdb.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <sys/time.h>
#include <pthread.h>
#include <setjmp.h>
#include <sys/time.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/msg.h>
#include <sys/uio.h>
#include <arpa/inet.h>
#include <sys/shm.h>
#include <sys/epoll.h>
#define ARGS_CHECK(argc, val) \
if(argc != val) { \
printf("error args\n"); \
return -1; \
}
#define ERROR_CHECK(ret, retVal, funcName) { \
if(ret == retVal) { \
perror(funcName); \
return -1; \
} \
}
#define THREAD_ERROR_CHECK(ret, funcName) \
do { \
if (0 != ret) { \
printf("%s : %s\n", funcName, strerror(ret)); \
} \
}while(0)
typedef struct conf1
{
char ip[17];//暂时想不到好办法
struct conf1* next;
}Conf_t,*pConf_t;
typedef struct confNode
{
int size;
pConf_t phead;//保存文件配置项头部节点
pConf_t ptail;//保存文件配置项尾部节点
}confNode_t,*pconfNode_t;
//截取字符串前的空格和后面的空格,只保留合法字符串配置数据
void Ltrim(char *string);
void Rtrim(char *string);
pConf_t load(const char *pconfName, pConf_t* conf);
#endif
首先读取配置文件,位于同级目录下的tcp.conf
#ip为本机IP地址,port为指定端口,clientnum为创建子进程的个数
2 ip = 192.168.98.130
3 port = 5555
4 clientnum = 5
读取tcp.conf配置文件配置项,由于坚持不用c++代码,所以调用部分写的比较Low一点,原来光是600行的代码,就删减到200行不到了。
readConf.c
#include "../include/head.h"
void Rtrim(char *string)
{
size_t len = 0;
if (string == NULL)
return;
len = strlen(string);
while (len > 0 && string[len - 1] == ' ') //位置换一下
string[--len] = 0;
return;
}
void Ltrim(char *string)
{
size_t len = 0;
len = strlen(string);
char *p_tmp = string;
if ((*p_tmp) != ' ') //不是以空格开头
return;
//找第一个不为空格的
while ((*p_tmp) != '\0')
{
if ((*p_tmp) == ' ')
p_tmp++;
else
break;
}
if ((*p_tmp) == '\0') //全是空格
{
*string = '\0';
return;
}
char *p_tmp2 = string;
while ((*p_tmp) != '\0')
{
(*p_tmp2) = (*p_tmp);
p_tmp++;
p_tmp2++;
}
(*p_tmp2) = '\0';
return;
}
pConf_t load(const char *pconfName, pConf_t* conf)
{
//pConf_t *pcur;
int sum = 0;
pconfNode_t pcur = (pconfNode_t)calloc(1, sizeof(confNode_t));
FILE *fp;
fp = fopen(pconfName, "r");
if (fp == NULL)
return NULL;
else
{
printf("成功打开配置文件\n");
}
//每一行配置文件读出来都放这里
char linebuf[501];
while (!feof(fp))
{
//为空或者第一个字符为空格都要忽略
if (fgets(linebuf, 500, fp) == NULL)
continue;
if (linebuf[0] == 0)
continue;
//处理注释行
if (*linebuf == ';' || *linebuf == ' ' || *linebuf == '#' || *linebuf == '\t' || *linebuf == '\n')
continue;
lblprocstring:
//屁股后边若有换行,回车,空格等都截取掉,goto循环截取
if (strlen(linebuf) > 0)
{
if (linebuf[strlen(linebuf) - 1] == 10 || linebuf[strlen(linebuf) - 1] == 13 || linebuf[strlen(linebuf) - 1] == 32)
{
linebuf[strlen(linebuf) - 1] = 0;
goto lblprocstring;
}
}
if (linebuf[0] == 0)
continue;
if (*linebuf == '[') //[开头的也不处理
continue;
//==0肯定就是没读到数据
char *ptmp = strchr(linebuf, '=');
//走到=停下来,注意编写配置文件的格式
//需要截取右边的
if (ptmp != NULL)
{
strcpy((*conf)->ip,ptmp + 1);
//拷贝出来
Rtrim((*conf)->ip);
Ltrim((*conf)->ip);
}
pConf_t cf = (pConf_t)calloc(1, sizeof(Conf_t));
strcpy(cf->ip, (*conf)->ip);
//把读取到的数据,存储到动态分配的cf节点中
//这里不采用head为空的方式,不习惯。
//如果是第一次插入节点
if (pcur->size == 0)
{
pcur->phead = cf;
pcur->ptail = cf;
pcur->size++;
}
else
{
pcur->ptail->next = cf;
pcur->ptail = cf;
pcur->size++;
}
}
pConf_t p;
p = pcur->phead;
while (pcur->phead)
{
printf("%s\n", pcur->phead->ip);
//打印测试
pcur->phead = pcur->phead->next;
}
return p;//把指针返回回去。本来想做个传入传出的,太麻烦了,算了
}
测试结果如上str为我需要创建的子线程个数,也从配置文件中读取。
2.初始化任务和任务队列数据
//workque.h
#include "../include/head.h"
typedef struct
{
int clientFd;//存储客户端的fd,代表着每一个任务
pNode_t next;
}Node_t,*pNode_t;
typedef struct
{
int size;//存储任务节点的个数
pNode_t pHead;//任务队列的头节点
pNode_t Ptail;//任务队列的尾节点
pthread_cond_t cond;//条件变量
pthread_mutex_t mutex;//互斥量
int flag;//flag用于令线程结束的标志位
}Que_t,*pQue_t;
//workque.c
#include "../include/head.h"
#include "../include/workque.h"
int queInit(pQue_t pQue)
{
//初始化
pthread_mutex_init(&pQue->mutex,NULL);
pthread_cond_init(&pQue->cond,NULL);
pQue->pHead = NULL;
pQue->Ptail = NULL;
pQue->size = 0;
}
int queInsert(pQue_t pQue,pNode_t pnew)//插入任务
{
//从任务队列里面插入新的任务节点
if(pQue == NULL)
{
perror("pQue is NULL");
return -1;
}
if(pQue->size == 0)
{
pQue->pHead = pnew;
pQue->Ptail = pnew;
pQue->size++;
}
else
{
pQue->Ptail->next = pnew;
pQue->Ptail = pnew;
//尾插法,移动到下一个新节点
pQue->size++;
}
return 0;
}
int queGet(pQue_t pQue,pNode_t* pget)
{
if(pQue->size == 0)
{
perror("pQue is NULL not Get");
return -1;
}
//取出头节点的任务,先进先出
*pget = pQue->pHead;
pQue->pHead = pQue->pHead->next;
pQue->size--;
return 0;
}
3.初始化线程池的结构和任务队列
//thread_pool.h
#ifndef __THREADPOOL_H__
#define __THREADPOOL_H__
#include "head.h"
#include "workque.h"
typedef struct {
int len;
char buf[1000];//约定的私有协议
}Train_t;
//线程池的数据结构
typedef struct{
int threadNum;
pthread_t * pthid;
Que_t Que;
}threadPool_t, *pThreadPool_t;
int threadPoolInit(pThreadPool_t pPool, int threadNum)//初始化线程池
int threadPoolStart(pThreadPool_t pPool);//启动线程池
int transFile(int clienFd);//子线程需要处理的任务——发送文件
int tcpInit(char *ip, char *port, int *sockFd);//tcp初始化
int epollAddFd(int fd, int epfd);//epoll将文件描述符纳入监听红黑树上
#endif
//thread_pool.c
#include"../include/workque.h"
#include "../include/thread_Pool.h"
#include "../include/head.h"
//信号处理函数,用于进程退出的
void cleanFunc(void *p)
{
pQue_t pQue = (pQue_t)p;
//解锁
pthread_mutex_unlock(&pQue->mutex);
}
void *threadFunc(void *p)
{
pQue_t pQue = (pQue_t)p;
pNode_t pCur;
int getsuccess = 1;
while(1)
{
if(pQue->flag == 0)
{
pthread_exit(NULL);
}
pthread_mutex_lock(&pQue->mutex);
//使用线程终止清理函数清理锁资源
pthread_cleanup_push(cleanFunc, pQue);
if(0 == pQue->size)
{
//等待队列里面有任务
pthread_cond_wait(&pQue->cond, &pQue->mutex);
}
getsuccess = queGet(pQue, &pCur);
//如果pthread_cond_wait被pthread_cond_signal激发,就会取出一个节点
pthread_mutex_unlock(&pQue->mutex);
if(0 == getsuccess)
{
transFile(pCur->clientFd);//调用该函数,处理自己的任务
free(pCur);
pCur = NULL;
}
pthread_cleanup_pop(1);
}
pthread_exit(NULL);
}
int threadPoolInit(pThreadPool_t pPool, int threadNum)
{
pPool->threadNum = threadNum;
pPool->pthid = (pthread_t*)calloc(threadNum, sizeof(pthread_t));
queInit(&pPool->Que);
return 0;
}
int threadPoolStart(pThreadPool_t pPool)
{
for(int i = 0; i < pPool->threadNum; ++i)
{
pthread_create(pPool->pthid+i, NULL, threadFunc, &pPool->Que);
}
return 0;
}
3.1子线程任务处理函数
#define _GNU_SOURCE
#include"../include/workque.h"
#include "../include/thread_Pool.h"
#include "../include/head.h"
#include<fcntl.h>
void sigfunc(int signum)
{
printf("sig is comming\n");
}
int transFile(int clienFd)
{
signal(SIGPIPE, sigfunc);
//使用私有协议发送数据,人为的规定发送数据的边界
Train_t train;
printf("xxxx\n");
memset(&train, 0, sizeof(train));
int fd = open("file", O_RDWR);
//存储文件名长度
train.len = 4;
strcpy(train.buf, "file");
//发送文件名
send(clienFd, &train, 4 + train.len, 0);
struct stat fileInfo;
bzero(&fileInfo, sizeof(fileInfo));
fstat(fd, &fileInfo);
train.len = sizeof(fileInfo.st_size);
memcpy(train.buf, &fileInfo.st_size, train.len);
//发送文件的总长度
send(clienFd, &train, 4 + train.len, 0);
printf("filesize = %ld\n", fileInfo.st_size);
int sfd[2];
pipe(sfd);
//splice需要借助管道发送数据,一次最多可以发送65535个
int recvLen = 0;
while(recvLen < fileInfo.st_size){
int ret = splice(fd, 0, sfd[1], 0, 4096, 0);
ret = splice(sfd[0], 0, clienFd, 0, ret, 0);
recvLen += ret;
}
return 0;
}
4.tcpinit()和epoll.c初始化
//tcpinit.c
//功能:用于返回一个sfd用于监听的文件描述符,为参数3,传入传出
//随后续将其纳入epoll红黑树的监听,等待就绪到wait双向链表上
#include"../include/workque.h"
#include "../include/thread_Pool.h"
#include "../include/head.h"
int tcpInit(char *ip, char *port, int *sockFd)
{
int sfd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(sfd, -1, "socket");
struct sockaddr_in serAddr;
memset(&serAddr, 0, sizeof(serAddr));
serAddr.sin_family = AF_INET;
serAddr.sin_addr.s_addr = inet_addr(ip);
serAddr.sin_port = htons(atoi(port));
//设置套接口地址可重用
int reuse = 1; // 变量大于0,设置的选项生效
setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
int ret = 0;
ret = bind(sfd, (struct sockaddr*)&serAddr, sizeof(serAddr));
ERROR_CHECK(ret, -1, "bind");
ret = listen(sfd, 10);
ERROR_CHECK(ret, -1, "listen");
*sockFd = sfd;
return 0;
}
//epollAddFd.c
#include"../include/workque.h"
#include "../include/thread_Pool.h"
#include "../include/head.h"
int epollAddFd(int fd, int epfd)
{
struct epoll_event event;
memset(&event, 0, sizeof(event));
event.events = EPOLLIN;
event.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
return 0;
}
5.主函数逻辑流程及下载完成线程退出
#include "../include/head.h"
#include "../include/workque.h"
#include "../include/thread_Pool.h"
int exitPipe[2];
void sigFunc(int sig)
{
printf("触发信号函数,给子进程管道写入flag\n");
write(exitPipe[1],&sig,4);
}
int main()
{
pConf_t conf =(pConf_t) calloc(1, sizeof(Conf_t));
pConf_t phead;
phead = load("tcp.conf",&conf);
//打开配置文件,读取配置项
char ip[20],port[5];
char num[4];
int ret = 0;
int str = atoi(phead->next->next->ip);
printf("ip = %s\tport = %s\n",phead->ip,phead->next->ip);
printf("str : %d\n",str);
pipe(exitPipe);
if(fork())
{
close(exitPipe[0]);
signal(SIGUSR1,sigFunc);
wait(NULL);
}
threadPool_t pool;
threadPoolInit(&pool, str);
//2.启动线程池
threadPoolStart(&pool);
//3.创建tcp监听套接字
int sfd = 0;
tcpInit(phead->ip, phead->next->ip, &sfd);
//4.创建epoll
int epfd = epoll_create(1);
epollAddFd(sfd, epfd);
epollAddFd(exitPipe[0],epfd);
//将管道读端纳入监听队列
struct epoll_event evs[2];
int newFd = 0;
int sums = 0;
while(1){
sums = epoll_wait(epfd, evs, 2, -1);
for(int i = 0; i < sums; ++i)
{
if(evs[i].data.fd == sfd)
{
newFd = accept(sfd, NULL, NULL);
printf("成功建立连接\n");
//创建任务节点,newFd交给节点
pNode_t pNew = (pNode_t)calloc(1, sizeof(Node_t));
pthread_mutex_lock(&pool.Que.mutex);
pNew->clientFd = newFd;
pool.Que.flag = 1;
//把节点插入到任务队列里面
queInsert(&pool.Que, pNew);
//任务队列里面有数据了,唤醒等待的子线程
pthread_cond_signal(&pool.Que.cond);
pthread_mutex_unlock(&pool.Que.mutex);
}
else if(evs[i].data.fd == exitPipe[0])
{
for(int a = 0;a<str ;a++)
{
//循环遍历所有子线程
pool.Que.flag = 0;
}
for(int a = 0 ;a < str; a++)
{
pthread_join(pool.pthid[a],NULL);
}
}
}
}
return 0;
}
后续等待完善中……
待实现功能:
- 文件的秒上传功能。通过比对数据库的文件内容哈希值。
- 数据库用户表。用户的注册,登录,修改密码。
- 数据库文件表,虚拟文件系统
- cd进入本用户的目录
- ls查看自己目录下属于自己的文件
- 断点续传功能
- 2.日志记录 (1) 服务器日志记录客户端请求信息及客户端连接时间。 (2) 服务器日志记录客户端操作记录及操作时间
- mmap 将大文件映射入内存,进行网络传递。当发现文件大于 100M,就使用 mmap 方式映