跟我学NETBSD内核源代码

到百度贴吧首页
新闻   网页   贴吧   知道   MP3   图片   视频   百科
    吧内搜索 | 帮助

跟我学NETBSD内核源代码

1楼

点名要求今天狂风过来学习。

2楼

先来个简单的:
#define LIBKERN_INLINE
#include <lib/libkern/libkern.h>

unsigned int
min(a, b)
unsigned int a, b;
{
return (a < b ? a : b);
}

这个函数是什么作用,我想不用我多说了吧?
这是一个非常简单的函数,简单到了极点,但它透露出来的信息不是那么简单。
#define LIBKERN_INLINE
这个define的作用,是告诉其它源代码,这是内核级(KERN)的一个库函数(LIB),它是INLINE的。什么叫INLINE函数?查书去。
为什么要用INLINE函数,而不用宏呢?比如 #define min((a), (b)) ((a)<(b)?(a):(b)),这样是不是更好,速度更快?
不是的,宏定义有一个致命的缺点:编译器不帮你检查参数类型!如果你传递了一个错误类型的参数给一个宏,它还是会以为是正常的。
而定义成函数就好多了,编译器会检查的。
但定义成函数又有个缺点:调用函数,要压栈,保护现场,函数执行完之后又要恢复现场,出栈。花费的时间太多,对于内核级的函数来说,这是不可容忍的。
所以定义成INLINE函数。至于是如何实现的,这比较复杂,内核有它的办法,而我们,如果想用INLINE函数,现在有了好的选择:标准C99已经定义了INLINE,具体请参考它。
这个函数告诉我们,如果有一个很简单的功能,请用内联函数(即INLINE FUNCTION)来实现它,而不是用宏,更不要直接写代码。

3楼

那是不是所有函数都用INLINE来实现更好?
当然不是。内联函数的定义,是有限制的。比如有多个循环(for,while),比如递归函数,都不可以声明成内联函数。
如果一个比较大的函数,它的执行时间已经大大超过了调用它的时间,那么定义成内联函数已经失去了它的意义,反而会让程序变大(内联函数就是在每一个调用它的地方直接展开代码,这样调用多少次,程序里就会有多少个这样的代码,体积会变大)。

所以,内联函数,仅仅是在,一个很小功能的函数实现的时候用。它的作用,是很好地替代宏。

4楼

min函数如此简单,max函数呢?当然跟它一样简单,只不过return 那里要改一下顺序。
大家注意到没有,这个min函数只能比较unsigned int参数。如果是long类型的呢?
那就得实现一个lmin函数,如果是double类型的呢?那就得实现dmin函数。
不过为什么NETBSD里没有dmin函数呢?其实,内核级的编程中,很少用到double类型。最多会用到long类型。

5楼

这里C语言就没有C++方便了。C++可以实现运算符重载,或者用模板,方便地用一个函数进行不同类型数据的比较。不过它的内部,仍然是这样一种实现方法。C语言让你直接了解事情的本质。

6楼

下面让我们看一个inline函数的例子,很简单的:

#include <stdio.h>

inline void fun_a(int a)
{
 if(a==1)printf("1\n");
 else printf("not 1\n");
}

int main()
{
 fun_a(10000);
}
在mingw+gcc3.2.3下编译通过。
这就是inline函数。你会了吗?

7楼

再来个复杂点的:

#include <sys/types.h>
#include <sys/systm.h>

struct queue {//定义一个队列结构,队列是什么?看书去!有单向的,双向的……而这个程序,定义的是双向的,有next(下一个),有prev(前一个)
struct queue *q_next, *q_prev;
};

/*
 * insert an element into a queue 插队啦
 */

void
_insque(v1, v2)
void *v1;
void *v2;//为什么要用void *类型?呵呵,留个作业
{
struct queue *elem = v1, *head = v2;
struct queue *next;
//搞清楚赋值的顺序与关系。是谁插在谁的后面?
next = head->q_next;
elem->q_next = next;
head->q_next = elem;
elem->q_prev = head;
next->q_prev = elem;
}

/*
 * remove an element from a queue删掉队列中的一个元素。注意,这里没有清空它占用的内存,为什么?
 */

void
_remque(v)
void *v;
{
struct queue *elem = v;
struct queue *next, *prev;

next = elem->q_next;
prev = elem->q_prev;
next->q_prev = prev;
prev->q_next = next;
elem->q_prev = 0;
}

8楼

继续昨天。
为什么要用VOID *类型为参数呢?
void *是一种很特殊的类型,特殊在它可以接受任何类型的指针,比如int *, long *,甚至其它指针,如struct _some_struct *等。
这样其它函数在调用这个函数的时候,就不用做类型转换了,可以直接把某指针作为参数传递给这个函数。
但是void *类型的参数不能直接使用,因为编译器不知道你要用的内存有多大。char *s, 
*s解引用的是1个字节,int *s, *s解引用的是sizeof(int)个字节。而void *s, *s解引用的是多少字节?sizeof(void)???不知道。
所以在使用void *参数之前,必需进行类型转换。
本函数中,就是转换成了struct queue *,利用的是隐性类型转换。

9楼

接下来讲一个简单的函数
#include <sys/cdefs.h>
#include <lib/libkern/libkern.h>

#undef bzero /* in case of LIBSA_USE_MEMSET */

void
bzero(dstv, length)
void *dstv;
size_t length;
{
u_char *dst = dstv;

while (length-- > 0) {
*dst++ = 0;
}
}

10楼

#include <sys/cdefs.h>
#include <lib/libkern/libkern.h>
bzero的作用是把一段内存里的内容全部清零。
而memset是把一段内存中的内容全部写成某个数。
它们的作用是如此相似(实际上bzero就是用来代替memset最常用的情形--清零)
void *
memset(dstv, c, length)
void *dstv;
int c;
size_t length;
{
u_char *dst = dstv;

while (length-- > 0) {
*dst++ = c;
}
return dstv;
}

11楼

那为什么要设计bzero来代替memset的这种情形呢?
大家注意到没有,memset的后两个参数,一个是int c,一个是size_t length.
而在绝大多数的编译器和系统里,int和size_t是一样大小的。
因此如果你不小心传送错了参数,编译器不会警告你!
比如你想把int *s 的前20个字节都清零,
你可能会memset(s, 0, 20),
但也可能会memset(s,20, 0).如果是这样,就完全错了。可编译器无动于衷。
而程序员一般不会记忆这种参数顺序的,特别是两种类型很像的情况下,很容易搞混。
但要改写memset也不容易,毕竟已经用得很多了。
于是bzero诞生了。

这个例子告诉我们,设计函数的时候,应该多考虑一些。如果能利用编译器警告的,尽量用。

当然有些函数你没办法设计不同类型的参数,比如swap(int a, int b),那怎么办呢?C语言设计函数有一个办法,约定俗成法。

12楼

比如strcat这个函数,是把一个字符串连到另外一个字符串后面,
当然你也可以这样理解,在一个字符串后面添加另外一个字符串。

C语言怎么理解呢?它约定后一种理解方法。
比如
char *
strcat(s, append)
char *s;//第一个参数是目的字符串,即要增加(修改的)的
const char *append;//第二个只是用来读的,因此声明成const,避免修改
{
char *t = s;

for (; *t; ++t)
;
while ((*t++ = *append++) != '\0')
;
return (s);
}
再看strcpy

char *
strcpy(to, from)
char *to;//要修改的
const char *from;//要读的,声明成const
{
char *save = to;

for (; (*to = *from) != '\0'; ++from, ++to);
return(save);
}

再看sprintf
sprintf(char *buf/*要修改的*/, const char *fmt/*只读的*/, ...);
看出来了吗?都是要修改的是第一个参数,读的放在后面。
这就是所谓的C语言约定俗成的定义函数参数的方法。这样会大大减少大家记忆参数的数量,也就大大减少了出错的可能性。
但有一些函数因为历史原因,并没有遵循这样的方法。也有些人自己写函数,不遵循这样的方法。
在这里,我建议大家遵循这样的方法,不仅方便别人,更重要的是,方便你自己。跟大家接轨,你才能发展得更好。

13楼

留一个作业,有哪些函数,遵循了这样的方法?有哪些函数,没有遵循这样的方法?
提示:字符串函数和文件读写函数中,这两种情况都有。大家好好找找。

61.131.58.*

14楼

感谢assiss大哥,小弟我一定认真尽力去学习

--------
今天狂风

61.131.58.*

15楼

void
_remque(v)
void *v;

ANSI 不是规定写成void _remque(void *v)吗?

61.131.58.*

16楼

fputc(),fputs()等函数就没遵循"要修改的是第一个参数,读的放在后面"

17楼

15:
ANSI C99倒没有硬性规定一定要这样写,只不过推荐这样写,我也觉得这样写比较好

不过NETBSD那帮家伙受K & R 的影响实在太大,呵呵,他们的内核代码中,基本上都是K & R的写法。区别对待就行。

16:
文件系统的函数遵循的是另外一套约定俗成的方案,
即把FILE *的参数都放在最后,
 int fputc (int c, FILE *stream)
 int fputs (const char *s, FILE *stream)
 size_t fread (void *data, size_t size, size_t count, FILE *stream)
 size_t fwrite (const void *data, size_t size, size_t count, FILE *stream)
但是
 int fprintf (FILE *stream, const char *template, ...)
显然就开始模糊了,它只能遵循前一种(因为有可变参数)。
不过
int fseek (FILE *stream, long int offset, int whence)
这样的函数就让人不知所措了。按FILE函数的约定,FILE *应该放在最后,但它没有理由地放在了前面。

每一套函数可能都有它自己的约定俗成的方案,一个良好的函数设计应该遵循统一的约定,虽然C语言没有硬性规定它。

18楼

很多人都对FILE函数的设计不是很满意,参数比较乱。
如果历史能够重来,应该把FILE函数重新设计一遍,FILE *参数都统一放在前面。

STRING函数我原来记得有一个不遵循“修改的放在前面”这样的约定的,但没找到,可能记错了?

61.131.58.*

19楼

swab()函数
61.131.58.*

20楼

#include <stdio.h>
#include <string.h>

char source[11]="0123456789";
char Result[11];

int main()
{
 swab(source,Result,strlen(source));
 printf("\nSource:%s\tResult:%s",source,Result);
 return 0;
}
----------------------
顺便问一下,为什么我将char source[11]="0123456789";char Result[11];放在主函数中输出结果就会有乱码(XP sp2+Dev-C++4.9.9.2):
Source:0123456789 Result:1032547698纤?鮳0123456789

21楼

因为你的Result没有初始化。它的内容都是一些随机的东西。
而swab只负责写10个字符,它没有顺手帮你写上一个'\0',呵呵。
在swab语句后面写上一个Result[10]=0;再试试

61.131.58.*

22楼

原来是这样,试过了,明白

23楼

昨天没来,竟然就到第二页了。看来现在C语言吧还是比较兴旺的。
可惜我的时间越来越少了。唉。

今天结合一两个小函数,讲一些设计有指针的函数的注意事项

1、bcmp
int
bcmp(b1, b2, length)
const void *b1, *b2;
size_t length;//这种函数的定义法是K & R法,前面提过了。比较奇怪吧。我觉得大家还是用ANSI C推荐的写法比较好。当然这种写法也不错,我有时候也快被感染了。
//注意参数的类型与顺序,为什么用VOID *类型前面也讲过了。这里提一提length的类型size_t.见下面。
{
const char *p1 = b1, *p2 = b2;//NETBSD内核有个特点,就是无论你传过来的参数是什么样的,我在函数内部再定义一个变量来接受它。这里就用const char *类型隐式转换void *类型。
//像这些并非递归调用的函数,多出来一两个局部变量对程序性能并没有多么大的损失,相反整个程序会变得更清晰,更容易理解,更容易修改。

if (length == 0)
return(0);//如果比较的长度是0,那就不用比较了。返回值的情况见下面分析。
do
if (*p1++ != *p2++)//有人问了,为什么这里不测试一下p1和p2是不是NULL呢?C语言就是C语言,它假定程序员知道一切他应该知道的事。程序员可能不知道length的长度(但至少应该知道length>=0),但一定得知道p1跟p2是不是NULL.函数不会代替程序员应有的思考。
break;
while (--length);
return(length);//返回值的情况见下面
}

今天主要讲两个东西。
一个是size_t类型,一个是返回值的约定。

在C语言的基本数据类型里,没有size_t这个东西。
那它是什么东西呢?其实在很多系统里,size_t跟int一样,都是4个字节32位的一个整形变量。为什么要定义这样的一个东西呢,看起来似乎是多余的。
这是因为标准C没有规定int , long等类型具体是多少位,它们是跟机器与系统相关的。如DOS下int只有16位,而windows下却是32位,在其它的系统里,它可能是64位,或只有8位等。这一切都是可能。
如果你只在一个系统里写程序,似乎不用管它的平台相关性。但C语言的一个强大的特点,就是跟UNIX/LINUX息息相关,而UNIX/LINUX现在基本上可以运行在各种平台上,一个优秀的程序员在写程序的时候,不能不考虑这个程序的可移植性。如果直接用int , long等类型,到时候修改就太多了。于是有其它的一些标准规定了一些相对稳定的类型,size_t就是其中一种。
size_t类型的作用就是用来表示客观世界中一些可数的数量,因此它的值一般是>=0的。它最典型的应用,就是表示字符串的大小和文件的大小。大家可以在字符串与文件的各种函数里见到它的身影。
大家在使用size_t类型的时候,无需费心,直接跟int ,long等类型一样使用。只是要注意,一般情况下,size_t类型是非负值的。
跟size_t类似的,还有一个time_t类型,用来衡量时间的。大家可以在时间相关的函数中见到它的大量使用。

24楼

返回值的约定:

C语言标准里没有规定返回值一定是什么,这得看各人的爱好。
但标准函数库有一些约定的方案。
比如,返回0基本是表示顺利执行,没什么错误,或者表示比较的两个数(两种类型)相等(或相同),即最正常的情况。
而返回-1或其它非零值,基本是表示有错误发生。在UNIX下要结合errno等来判断发生了什么错误,或者表示比较的两个数不等(返回正数表示第一个参数大,返回负数表示第一个参数小)。

所以大家在看到下面的语句的时候,基本是应该知道是什么意思了:
if((errnum=some_func(some_vari))==0)
{
//如果正常执行这个函数,就应该到这里执行一些语句
}
else//发生错误或其它异常,得到这里来进行处理
{
//UNIX/LINUX下常需要结合errno,perror等全局变量、函数等进行判断到底发生了什么错误
//或者结合上面的errnum。具体得根据不同的函数具体分析。
}


当然,不一定所有的函数都是这样的。比如某些字符串函数,返回NULL(通常情况下NULL与0是相等的,但不一定永远相等,所以该判断NULL的地方,不能用0代替)表示有错。又比如某些文件读写函数,返回读写的字节数(通常返回-1表示出错。因为你不可能写入或读取-1字节)。

25楼

没心思再举其它例子了。也是大同小异。
给狂风留个作业:
找一些size_t, time_t应用的例子。
找一些返回值是0表示正常,和返回0表示不正常的函数例子。

26楼

那个bcmp函数是不是对任何类型都能做比较呢?
例如int型的使用方法是不是:
int main()
{ int a,b;
 scanf("%d,%d",&a,&b);
 if(!bcmp(&a,&b,sizeof(int))) printf("a==b");
 else printf("a!=b");
 return 0;
}

而对于字符串,就是比较前length个字符是否相等?

210.77.5.*

27楼

是的。它设计就是用来比较任意类型的数据的。不然也不会用VOID*了。

28楼

以前用过time_t,time_t做为一个长整数型来代表OS的time && date,可用ctime()函数将其转换为字符串。
我了解的就这一点点,具体如何转换就不清楚了。
int main()
{ time_t t;
 time(&t);
 printf("%ld\t%s",t,ctime(&t));
 return 0;
}

29楼

呵呵,风儿好。等我插个嘴...
可以把它看成是比较两个指针指向的内存段。

今天狂风>>>那个bcmp函数是不是对任何类型都能做比较呢?

要注意一点,这里比较的是值的相同,有时候和逻辑上的相同是不一样的。
有些复合数据,譬如含有指针成员的 struct,这样比较是没有意义的。

30楼

呵呵,这个我知道。

发表回复

内 容:
用户名:
  
©2010 Baidu 贴吧协议  意见反馈