一个的错误的演化
一个的错误的演化
OVERVIEW
- 一个的错误的演化
- 一道算法题引出的问题
- 1.问题引出:
- 2.问题再引出:
- 3.未能及时排查问题的原因
- 4.程序的修改与总结
- 6.原则问题
- 项目实践中问题的暴露
频繁遇到的一个问题,多次排查错误最终终于找到问题的根源,现在还原这几个遇到问题的场景:
一道算法题引出的问题
1.问题引出:
- 某场算法竞赛的一道简单题,部分函数功能:将一个int型的数字num,转化为string型的字符串str,并将str进行输出:代码如下
#include<stdio.h>
#include<stdlib.h>
int main() {
char *str;
int num;
scanf("%d", &num);
int len = 0; int x = num; while (x) {x /= 10; len++;}//获取num的长度
int i = len - 1;
while (num) {
str[i] = (num % 10 + '0');
num /= 10;
i--;
}
printf("%s", str);
return 0;
}
在Dev C++5.11中进行C代码的测试,
测试结果如下:
可以看到在终端输入num的数值之后,没有按照预期的将str字符串进行打印,就直接结束了程序的运行。
并且程序在编译和链接时,也没有给出任何的错误信息(这直接导致如果不是很仔细,问题将十分难以发现),问题十分隐蔽:
2.问题再引出:
- 由于竞赛时间紧迫,没时间去排查问题,错误的怀疑是C语言的语法问题,立刻使用C++语言将功能重新实现了一遍,代码如下:
#include<iostream>
using namespace std;
int main() {
string str;
int num; cin >> num;
int len = 0; int x = num; while (x) {x /= 10; len++;}//获取num的长度
int i = len - 1;
while (num) {
str[i] = (num % 10 + '0');
num /= 10;
i--;
}
cout << str << endl;
return 0;
}
在Dev C++5.11中进行C++代码的测试,
测试结果如下:毫无疑问,结果和C语言实现是一致的,在输入num的数值之后,程序没有输出str的值并直接结束了运行,
并且没有给出任何的错误提示信息!
此刻心情开始变得十分复杂,为什么一道这样的简单题写的如此狼狈?
3.未能及时排查问题的原因
未能及时定位问题的原因:
- 使用cmd终端窗口来测试程序,而终端窗口不会给出任何的错误信息提示!
- 在出现终端输入数据后无响应,不会第一时间想到利用更高级的gdb调试工具进行错误定位,而是使用printf函数在程序中测试:导致问题的发现进一步延缓。
- 如果会使用gdb进行调试,就能很快的定位到终端在输入num数值之后没有任何响应的原因,debug调试如图:
非常明显的segmentfault段错误,访问非法的内存空间。发现问题后立即对代码进行修改。
4.程序的修改与总结
- C程序中的问题的本质:在于定义了
char *str
但是没有为其分配内存空间,添加上一行calloc操作为其分配内存空间:
#include<stdio.h>
#include<stdlib.h>
int main() {
char *str;
str = (char *)calloc(100, sizeof(char *));
int num;
scanf("%d", &num);
int len = 0; int x = num; while (x) {x /= 10; len++;}//获取num的长度
int i = len - 1;
while (num) {
str[i] = (num % 10 + '0');
num /= 10;
i--;
}
printf("%s", str);
return 0;
}
- C++程序中的问题也是相同的:
string str;
没有进行初始化,直接使用str[i]
去引用string中的某个字符导致访问非法内存访问。可使用new操作符为string开辟一段内存空间。
可以看到虽然已经使用*string,输出仍然只有一个1,这涉及到string类的底层实现,具体参考:https://blog.csdn.net/qq_28082757/article/details/72782973:
此处可对第14行的输出部分做一个简单的处理,使其能够顺利的将字符串内容进行输出,
#include<iostream>
using namespace std;
int main() {
string *str = new string[100];
int num; cin >> num;
int len = 0; int x = num; while (x) {x /= 10; len++;}//获取num的长度
int i = len - 1;
while (num) {
str[i] = (num % 10 + '0');
num /= 10;
i--;
}
for (int i = 0; i < len; ++i) cout << *(str + i);
return 0;
}
正常的输出了结果:
- 问题总结:这其实是一个很基础很低级的错误,即访问了非法的内存空间,在定义一个字符数组串指针时没有为其分配内存空间,或者未初始化string对象,就直接尝试利用
string[i]
对其进行赋值操作。
6.原则问题
回到算法问题解决的角度,
- 问题1:将一个int类型的num数字,快速转换为string类型的字符串str,真要有必要这么繁琐吗?
这里给出一种快速将num转为str的解决方案,利用sprintf函数直接进行转换,
函数原型:int sprintf(string, const char *format, ...);
#include<iostream>
using namespace std;
int main() {
int num; cin >> num;
char *str = (char *)calloc(100, sizeof(char *));
sprintf(str, "%d", num);
cout << str << endl;
return 0;
}
很快的就将int类型的num数字转为了字符串str,sprintf函数还有其他很多十分有用的应用,例如将ip地址进行拼接。
- 问题2:注意到在求num数字长度的过程中使用了这样一段代码
int len = 0; int x = num; while (x) {x /= 10; len++;}
这样求一个数字的长度太不优雅了。
这里给出另外一种快速求num数字长度的解决方案,利用printf函数的返回值输出数字长度,
#include <stdio.h>
int main(){
int n;
scanf("%d", &n);
printf(" has %d digits!\n",printf("%d",n));//利用prinf函数返回值输出数字的位数
return 0;
}
项目实践中问题的暴露
在项目中也碰到了同样类似的问题,具体参见:使用epoll实现一个echo服务器
#include "head.h"
#include "common.h"
#include "thread_pool.h"
#define QUEUESIZE 100//任务队列大小
#define INS 4//线程数量
#define MAXEVENTS 5//epoll_event的最大数量
#define MAXCLIENTS 2000//最大客户端数量为2000
int epollfd;
int clients[MAXCLIENTS];//每次出现新的套接字时便存储到该数组中 保证临时文件描述符fd在循环中被修改后 操作的对象也不会改变
pthread_mutex_t mutex[MAXCLIENTS];//对用户临时数据上锁
char *data[MAXCLIENTS];//线程进行具体业务操作时 存储每个客户端产生的临时数据
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
void do_work(int fd) {
/* 对fd文件进行的具体逻辑操作 */
int rsize;
char buff[4096] = {0};
DBG(BLUE"<D> : data is ready on %d.\n"NONE, fd);
//1.数据接收recv
if ((rsize = recv(fd, buff, sizeof(buff), 0)) < 0) {
/* 如果接受数据出错 则使用epoll_ctl将fd文件描述符删除 */
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
DBG(RED"<C> : %d is close!\n"NONE, fd);
close(fd);//检测到客户端关闭连接 服务端也关闭连接
return;
}
//2.对数据进行简单的处理 并进行最后的send操作
int ind = strlen(data[fd]);
pthread_mutex_lock(&mutex[fd]);
for (int i = 0; i < rsize; ++i) {
if (buff[i] >= 'A' && buff[i] <= 'Z') {
data[fd][ind++] = buff[i] + 32;//大写转小写
} else if (buff[i] >= 'a' && buff[i] <= 'z') {
data[fd][ind++] = buff[i] - 32;//小写转大写
} else {
data[fd][ind++] = buff[i];//其他字符不处理
if (buff[i] == '\n') {
DBG(GREEN"<END> : \\n recved!\n"NONE);
send(fd, data[fd], ind, 0);
}
}
}
pthread_mutex_unlock(&mutex[fd]);
}
void *thread_run(void *arg) {
pthread_detach(pthread_self());
struct task_queue *taskQueue = (struct task_queue *)arg;
while (1) {
int *fd = task_queue_pop(taskQueue);
do_work(*fd);
}
}
int main(int argc, char *argv[]) {
int opt;
int port;
while ((opt = getopt(argc, argv, "p:")) != -1) {
switch (opt) {
case 'p':
port = atoi(optarg);
break;
default:
fprintf(stderr, "Usage : %s -p port!\n", argv[0]);
exit(1);
}
}
DBG(YELLOW"<D> : Server will listen on port [%d]\n"NONE, port);
//1.创建监听套接字
int server_listen;
if ((server_listen = socket_create(port)) < 0) handle_error("socket_create");
clients[server_listen] = server_listen;//文件描述符作为数组下标 存储新出现的listen_socket文件描述符
DBG(YELLOW"<D> : Server_listen starts.\n"NONE);
//2.初始化任务队列和锁
for (int i = 0; i < MAXCLIENTS; ++i) data[i] = (char *)calloc(1, 4096 + 10);//使用calloc在reacotr thread中开辟内存空间
struct task_queue *taskQueue = (struct task_queue *)calloc(1, sizeof(struct task_queue));
task_queue_init(taskQueue, QUEUESIZE);
DBG(YELLOW"<D> : task queue init successfully.\n"NONE);
for (int i = 0; i < MAXCLIENTS; ++i) pthread_mutex_init(&mutex[i], NULL);
DBG(YELLOW"<D> : mutex init successfully.\n"NONE);
//3.创建多个线程进行工作
pthread_t *tid = (pthread_t *)calloc(INS, sizeof(pthread_t));
for (int i = 0; i < INS; ++i) pthread_create(&tid[i], NULL, thread_run, (void *)taskQueue);
DBG(YELLOW"<D> : threads create successfully.\n"NONE);
//4.利用反应堆模式epoll进行事件分发
int sockfd;
// 4-1 epoll_create
if ((epollfd = epoll_create(1)) < 0) handle_error("epoll_create");//文件描述符被占用完就可能会出错
// 4-2 epoll_ctl将server_listen文件描述符注册到epoll实例中
struct epoll_event events[MAXEVENTS], ev;
ev.data.fd = server_listen;//关注的文件描述符
ev.events = EPOLLIN;//关注的事件、需要注册的事件
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, server_listen, &ev) < 0) handle_error("epoll_ctl");//注册操作
DBG(YELLOW"<D> : server_listen is added to epollfd successfully.\n"NONE);
for (;;) {
// 4-3 epoll_wait开始监听
int nfds = epoll_wait(epollfd, events, MAXEVENTS, -1);//nfds epoll检测到事件发生的个数
if (nfds == -1) handle_error("epoll_wait");//可能被时钟中断 or 其他问题
for (int i = 0; i < nfds; ++i) {
// 4-3-1 对epoll_wait监测到发生的所有事件进行遍历,进行事件分发
int fd = events[i].data.fd;
if (fd == server_listen && (events[i].events & EPOLLIN)) {
/* 返回的fd为server_listen可读 表示已经有客户端进行3次握手了 */
/* events[i].events & EPOLLIN 表示至少有一个可读 */
// 4-3-2将accept到的文件描述符注册到epoll实例中,实现文件监听
if ((sockfd = accept(server_listen, NULL, NULL)) < 0) handle_error("accept");
//如果是实际应用情况,如果出现错误应该想办法处理错误,并恢复实际业务
clients[sockfd] = sockfd;//文件描述符作为数组下标 存储新出现的conn_socket文件描述符
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;//设置为边缘触发模式
make_nonblock(sockfd);
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev) < 0) handle_error("epoll_ctl");//注册操作
} else {
/* 返回的fd不是server_listen可读 是普通的套接字 */
// 4-3-3 将监测到事件event的文件描述符fd加入任务队列中,交给线程池处理
if (events[i].events & EPOLLIN) {
/* 套接字属于就绪状态 有数据输入需要执行 */
task_queue_push(taskQueue, (void *)&clients[fd]);
//不可直接将fd传入 需要保证传入的fd值总是不同,创建clients[]数组保证每次传入的fd值不同
//当把地址作为参数传递给函数后(特别是在循环中),下一次fd的值会不断的被修改(传入的值是会变化的)
} else {
/* 套接字不属于就绪状态出错 将该事件的文件描述符从注册的epoll实例中删除 */
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
}
}
}//for
}//for
return 0;
}
同样出现了segmentfault错误,问题定位到do_work函数中的int ind = strlen(data[fd])
操作,
由于data[fd]
的指针是NULL,对一个空字符指针使用strlen都会报segmentfault错误,本质原因是data[fd]指针没有初始化,立即使用calloc在reacotr thread中为data[fd]
开辟内存空间。
for (int i = 0; i < MAXCLIENTS; ++i) data[i] = (char *)calloc(1, 4096 + 10);
问题成功解决,感悟:任何小问题都可能会引发严重的错误,一定要重视细节问题,更要重视扎实的语言基础。