`
tiandirensoon
  • 浏览: 595711 次
文章分类
社区版块
存档分类
最新评论

服务器端开发经验总结 Linux C语言

 
阅读更多

简介

在进行服务器端开发的时候需要考虑一些算法和性能问题,经过了几年的开发,对这方面有了一些经验,现在写下来跟大家分享和讨论。

我主要是在Linux下进行C语言的开发,所以后面的实现都是基于Linux操作系统并用C语言来讲解。其它平台和语言需要考虑的问题是类似的只不过可能是实现细节上有一些差异,我尽量减少这些差异吧。注意一下讲解的所有内容都是基于32位系统的开发!

服务器程序开发核心是稳定,在稳定的前提下需要考虑效率。其中主要的公共模块是内存池和线程池。因为服务器程序一般都会长时间的运行,而且频繁的进行创建和释放内存的操作,这时如果使用系统的malloc和free方法,则会使系统中产生很多内存碎片,从而影响效率和稳定性。内存池的主要思想是先调用系统的malloc开辟一个很大的内存,然后对这个大内存进行管理,程序中要使用内存时,内存池分配内存,程序要释放内存时,只是通知内存池,而不真正释放内存。线程池,原理类似,并发处理一些任务的时候,需要使用很多线程,我们可以先创建很多线程,然后每次有任务需要处理,则找到一个空闲的线程让它来处理任务,处理完成后线程挂起。这样省去了每次一个任务都创建线程的时间开销。

预备知识

下面介绍一下在进行库的封装时用到的一些技巧。因为我们是使用C语言进行开发,所以为了使结构和效率得到很好的平衡,需要一些技巧来进行封装,从而使程序在保证C语言效率的前提下足够的模块化。

container_of 宏

首先介绍一下这个宏,这个宏是我在linux内核中看到的,它用于通过一个结构中的某个成员的指针,推算出整个结构的指针,先举一个例子吧。例如有如下一个结构:

 1: struct my_struct
 2: {
 3: int a;
 4: int b;
 5: char c;
 6: short d;
 7: };

看看下面的代码:

 1: int
 2: main ()
 3: {
 4: struct my_struct mys; // 创建结构对象
 5: struct my_struct* pmys; // 声明一个指向我们的结构的指针
 6: char* pc = &(mys.c); // pc指向结构中c成员
 7: int* pb = &(mys.b); // pb指向结构中b成员
 8: pmys = container_of (pc, struct my_struct, c); // pmys实际上指向mys结构
 9: pmys = container_of (pb, struct my_struct, b); // pmys还是指向mys结构
 10: // 上面两行代码与 pmys = &mys; 的功能一样
 11: ...
 12: }

上面的代码说明,通过调用container_of可以通过某个结构中某个成员的指针(如pc或者pb),获得整个结构的指针。
这个宏需要三个参数,第一个参数是结构中某个成员的指针,第二个参数是结构的类型,第三个参数是,第一个参数中的成员指针指向的成员在结构的声明中的成名名称。这个宏定义如下:

 1: #define offsetof(TYPE, MEMBER)((size_t) &((TYPE *)0)->MEMBER)
 2:
 3: #ifdef WIN32
 4: // WIN32下(不支持类型安全检查)
 5: #define container_of(ptr, type, member) /
 6: ((type*)(ptr)-offsetof(type, member))
 7:
 8: #else
 9: // linux下
 10: #define container_of(ptr, type, member)({/
 11: const typeof( ((type *)0)->member) *_mptr = (ptr); /
 12: (type *)((char*)_mptr-offsetof(type, member));})
 13:
 14: #endif /* */

在上面的实现中,首先定义了一个offsetof宏,这个宏用于计算结构中的某个成员的地址相对于这个结构的起始地址的偏移。WIN32下可以使用系统自带offset宏。

container_of宏实际上是用结构中某个成员的地址减去这个成员相对于结构起始的偏移,就得到了结构的地址也就是指向这个结构的指针。linux下实现的时候:const typeof( ((type *)0)->member)* _mptr = (ptr);这句实际上是在做类型安全检查。保证给宏的第一个参数成员指针的类型就是指向这个成员类型的指针。typeof是gcc扩展,用于获得某个表达式的类型。在WIN32下我没有找到typeof的代替品,所以没有做类型安全检查。

有了container_of宏,我们可以做些什么工作呢?举个例子,如果我们写一个链表,教科书上一般将链表中的数据区域当作一个int。实际上可能是一个复杂的结构,而我作为编写链表的人,不知道使用者会在链表中存储什么数据。通常的做法是,数据区域就是一个void*,这样使用者可以用这个指针存储任何对象了。如果使用者要存储自己的数据,可以创建自定义结构的对象,然后用这个指针指向这个结构。链表中的每一个元素除了数据区占用的内存外还占用了一个void*.在服务器端开发时,数据量是很大的,这样就浪费了很多的void*。我们可以通过如下的方法来解决这个问题。

定义链表中的元素结构:

 1: struct link_item
 2: {
 3: struct link_item* next;
 4: };

而提供的操作链表的方法都是基于struct link_item* 指针的方法。用户在使用的时候声明自己的链表元素结构:

 1: struct user_link_item
 2: {
 3: struct link_item lk_item;
 4: int my_data1;
 5: short my_data2;
 6: // ...
 7: };

这个结构中拥有struct link_item类型的一个成员lk_item。我们在使用链表的时候,总是将lk_item的地址传递给链表的方法。获取数据的时候获得的也是指向struct link_item的指针,但是我们可以使用container_of宏用链表方法返回的struct link_item的指针推算出struct user_link_item的指针,这样作为用户就可以获得存储在链表中的数据了。这样的实现中具体元素的内存也是外部使用者来分配的。

这种方法有些类似c++的继承机制。遇到继承的问题可以考虑使用这种方法。

引用计数器

在开发服务器程序的时候,我们经常是多线程的并行处理。对于要处理的数据就有一个线程安全的问题。最简单的线程安全处理是原子操作。每一次处理都是使用一条指令完成。其次是使用线程锁,进行线程同步。对于一个存储在某个数据结构中的数据来说,可能会在某个线程进行读取操作,而在另外的一个线程这个数据被删除了。我们可以模仿WIN32的COM处理这个问题时使用的方法。增加一个引用计数器,每次要对数据进行访问的时候都要递增这个数据的引用计数器,当使用完成的时候,递减这个数据的引用计数器。当引用计数器的值被递减为0的时候,这个数据就会真正的被删除。当然,引用计数器的递增和递减操作需要是线程安全的,完全可以使用原子操作来实现。

所有需要保证线程安全的数据对象,我们都可以通过上面讲到的使用container_of宏的实现的继承方式来实现。

通讯协议中变长字段的结构化处理

服务器端通讯通常会设计一套协议。在协议中通常是如下形式:

|4字节表示后面的字节数|1字节表示某个标志|1字节表示某个标志|2字节表示某个标志|n个字节表示内容|

上面的这中协议,我们如何来将它转换为一个结构呢?看看如下定义的结构:

 1: // 错误的结构
 2: struct wrong_struct
 3: {
 4: long size;
 5: char f1;
 6: char f2;
 7: short f3;
 8: char* content;
 9: };

上面的结构明显是错误的,主要是content成员。content是一个指向字符串的指针它覆盖了协议中“n个字节表示内容”中的最前面的4个字节。如何解决这个问题呢?很简单:

 1: struct right_struct
 2: {
 3: long size;
 4: char f1;
 5: char f2;
 6: short f3;
 7: char content[1];
 8: };

我们将content声明为只有一个元素的字符串数组。字符串数组又是字符串指针,我们可以使用content成员来引用“n个字节表示内容”了。

当然这设计通讯协议的时候一定要考虑到字节对齐,关于字节对齐,这里就不再详细介绍了,可以参考一些C/C++的资料。

简介

在进行服务器端开发的时候需要考虑一些算法和性能问题,经过了几年的开发,对这方面有了一些经验,现在写下来跟大家分享和讨论。

我主要是在Linux下进行C语言的开发,所以后面的实现都是基于Linux操作系统并用C语言来讲解。其它平台和语言需要考虑的问题是类似的只不过可能是实现细节上有一些差异,我尽量减少这些差异吧。注意一下讲解的所有内容都是基于32位系统的开发!

服务器程序开发核心是稳定,在稳定的前提下需要考虑效率。其中主要的公共模块是内存池和线程池。因为服务器程序一般都会长时间的运行,而且频繁的进行创建和释放内存的操作,这时如果使用系统的malloc和free方法,则会使系统中产生很多内存碎片,从而影响效率和稳定性。内存池的主要思想是先调用系统的malloc开辟一个很大的内存,然后对这个大内存进行管理,程序中要使用内存时,内存池分配内存,程序要释放内存时,只是通知内存池,而不真正释放内存。线程池,原理类似,并发处理一些任务的时候,需要使用很多线程,我们可以先创建很多线程,然后每次有任务需要处理,则找到一个空闲的线程让它来处理任务,处理完成后线程挂起。这样省去了每次一个任务都创建线程的时间开销。

预备知识

下面介绍一下在进行库的封装时用到的一些技巧。因为我们是使用C语言进行开发,所以为了使结构和效率得到很好的平衡,需要一些技巧来进行封装,从而使程序在保证C语言效率的前提下足够的模块化。

container_of 宏

首先介绍一下这个宏,这个宏是我在linux内核中看到的,它用于通过一个结构中的某个成员的指针,推算出整个结构的指针,先举一个例子吧。例如有如下一个结构:

 1: struct my_struct
 2: {
 3: int a;
 4: int b;
 5: char c;
 6: short d;
 7: };

看看下面的代码:

 1: int
 2: main ()
 3: {
 4: struct my_struct mys; // 创建结构对象
 5: struct my_struct* pmys; // 声明一个指向我们的结构的指针
 6: char* pc = &(mys.c); // pc指向结构中c成员
 7: int* pb = &(mys.b); // pb指向结构中b成员
 8: pmys = container_of (pc, struct my_struct, c); // pmys实际上指向mys结构
 9: pmys = container_of (pb, struct my_struct, b); // pmys还是指向mys结构
 10: // 上面两行代码与 pmys = &mys; 的功能一样
 11: ...
 12: }

上面的代码说明,通过调用container_of可以通过某个结构中某个成员的指针(如pc或者pb),获得整个结构的指针。
这个宏需要三个参数,第一个参数是结构中某个成员的指针,第二个参数是结构的类型,第三个参数是,第一个参数中的成员指针指向的成员在结构的声明中的成名名称。这个宏定义如下:

 1: #define offsetof(TYPE, MEMBER)((size_t) &((TYPE *)0)->MEMBER)
 2:
 3: #ifdef WIN32
 4: // WIN32下(不支持类型安全检查)
 5: #define container_of(ptr, type, member) /
 6: ((type*)(ptr)-offsetof(type, member))
 7:
 8: #else
 9: // linux下
 10: #define container_of(ptr, type, member)({/
 11: const typeof( ((type *)0)->member) *_mptr = (ptr); /
 12: (type *)((char*)_mptr-offsetof(type, member));})
 13:
 14: #endif /* */

在上面的实现中,首先定义了一个offsetof宏,这个宏用于计算结构中的某个成员的地址相对于这个结构的起始地址的偏移。WIN32下可以使用系统自带offset宏。

container_of宏实际上是用结构中某个成员的地址减去这个成员相对于结构起始的偏移,就得到了结构的地址也就是指向这个结构的指针。linux下实现的时候:const typeof( ((type *)0)->member)* _mptr = (ptr);这句实际上是在做类型安全检查。保证给宏的第一个参数成员指针的类型就是指向这个成员类型的指针。typeof是gcc扩展,用于获得某个表达式的类型。在WIN32下我没有找到typeof的代替品,所以没有做类型安全检查。

有了container_of宏,我们可以做些什么工作呢?举个例子,如果我们写一个链表,教科书上一般将链表中的数据区域当作一个int。实际上可能是一个复杂的结构,而我作为编写链表的人,不知道使用者会在链表中存储什么数据。通常的做法是,数据区域就是一个void*,这样使用者可以用这个指针存储任何对象了。如果使用者要存储自己的数据,可以创建自定义结构的对象,然后用这个指针指向这个结构。链表中的每一个元素除了数据区占用的内存外还占用了一个void*.在服务器端开发时,数据量是很大的,这样就浪费了很多的void*。我们可以通过如下的方法来解决这个问题。

定义链表中的元素结构:

 1: struct link_item
 2: {
 3: struct link_item* next;
 4: };

而提供的操作链表的方法都是基于struct link_item* 指针的方法。用户在使用的时候声明自己的链表元素结构:

 1: struct user_link_item
 2: {
 3: struct link_item lk_item;
 4: int my_data1;
 5: short my_data2;
 6: // ...
 7: };

这个结构中拥有struct link_item类型的一个成员lk_item。我们在使用链表的时候,总是将lk_item的地址传递给链表的方法。获取数据的时候获得的也是指向struct link_item的指针,但是我们可以使用container_of宏用链表方法返回的struct link_item的指针推算出struct user_link_item的指针,这样作为用户就可以获得存储在链表中的数据了。这样的实现中具体元素的内存也是外部使用者来分配的。

这种方法有些类似c++的继承机制。遇到继承的问题可以考虑使用这种方法。

引用计数器

在开发服务器程序的时候,我们经常是多线程的并行处理。对于要处理的数据就有一个线程安全的问题。最简单的线程安全处理是原子操作。每一次处理都是使用一条指令完成。其次是使用线程锁,进行线程同步。对于一个存储在某个数据结构中的数据来说,可能会在某个线程进行读取操作,而在另外的一个线程这个数据被删除了。我们可以模仿WIN32的COM处理这个问题时使用的方法。增加一个引用计数器,每次要对数据进行访问的时候都要递增这个数据的引用计数器,当使用完成的时候,递减这个数据的引用计数器。当引用计数器的值被递减为0的时候,这个数据就会真正的被删除。当然,引用计数器的递增和递减操作需要是线程安全的,完全可以使用原子操作来实现。

所有需要保证线程安全的数据对象,我们都可以通过上面讲到的使用container_of宏的实现的继承方式来实现。

通讯协议中变长字段的结构化处理

服务器端通讯通常会设计一套协议。在协议中通常是如下形式:

|4字节表示后面的字节数|1字节表示某个标志|1字节表示某个标志|2字节表示某个标志|n个字节表示内容|

上面的这中协议,我们如何来将它转换为一个结构呢?看看如下定义的结构:

 1: // 错误的结构
 2: struct wrong_struct
 3: {
 4: long size;
 5: char f1;
 6: char f2;
 7: short f3;
 8: char* content;
 9: };

上面的结构明显是错误的,主要是content成员。content是一个指向字符串的指针它覆盖了协议中“n个字节表示内容”中的最前面的4个字节。如何解决这个问题呢?很简单:

 1: struct right_struct
 2: {
 3: long size;
 4: char f1;
 5: char f2;
 6: short f3;
 7: char content[1];
 8: };

我们将content声明为只有一个元素的字符串数组。字符串数组又是字符串指针,我们可以使用content成员来引用“n个字节表示内容”了。

当然这设计通讯协议的时候一定要考虑到字节对齐,关于字节对齐,这里就不再详细介绍了,可以参考一些C/C++的资料。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics