别出心裁的Linux系统调用学习法
操作系统与系统调用
操作系统(Operating System,简称OS)是计算机中最重要的系统软件,是这样的一组系统程序的集成:这些系统程序在用户对计算机的使用中,即在用户程序中和用户操作中,负责完成所有与硬件因素相关的(硬件相关)和任何用户共需的(应用无关)基本使用工作,并解决这些基本使用工作中的效率和安全问题,为使用户(操作和上层程序)能方便、高效、安全地使用计算机系统,而从最底层统一提供所有通用的帮助和管理。
硬件相关:
- 涉及物理地址、设备接口寄存器、设备接口缓冲区
- 代码量大,需硬件知识
- 需随硬件的变化而变化
应用无关:
- 所有应用、用户共需
- 工作过程雷同
- 与应用无直接关系
我把操作系统完成的「硬件相关、应用无关」的工作比喻成两个角色:
- 管家婆
- 服务生
操作系统通过三抽象概念完成了「管家婆」的功能:
- 通过「文件」对I/O设备进行了抽象
- 通过「虚存」对主存和I/O设备进行了抽象
- 通过「进程」对CPU、主存和I/O设备进行了抽象
如果说概念是任何学习中的重中之重,那么《操作系统》课上最重要的概念就是「文件、虚存和进程」,这几个概念没学好,《操作系统》这门课就完了。当然通信、并发、并行、异步、调度、多道等概念在《操作系统》课程学习中也很重要。
一般来说,操作系统通过三个服务完成了「服务生」的概念:
- GUI:为小白用户提供服务,你只会用鼠标就可以使用操作系统
- Shell: 为高级用户提供服务,你要记忆系统命令,更多通过键盘使用操作系统
- 系统调用:为专业用户程序员提供服务,你可以创建自己的工具让大家更好的使用操作系统
系统调用(System Call)是操作系统为在用户态运行的进程与硬件设备进行交互提供的一组接口。当用户进程需要发生系统调用时,CPU 通过软中断切换到内核态开始执行内核系统调用函数。我们可以有三种方法使用系统调用:
- 通过 int 指令陷入:通过软中断指令int 0x80 来陷入内核态
- 使用 syscall 直接调用, glibc没有封装某个系统调用时可以
- 通过 glibc 提供的API调用,最方便的方法
下文我们主要使用第三种方法来使用Linux系统调用,并且不区分的使用「系统调用」和「API」。比如打开文件系统调用sys_open
对应的是 API是open
,我们会说open
是系统调用,也就是说manpages中的第二节的API我们就叫「系统调用」。所以系统调用你当成一般函数来学习就可以了,通过参数和返回值关注一下功能。
Linux下学习编程的3*3
Linux 系统调用学习方法
程序员技术练级攻略中提出程序员进阶过程中通过实践来学习系统编程。
如何学习命令我写了一篇「别出心裁的Linux命令学习法」,核心是问题驱动,像使用Google/Baidu一样使用man -k key1| grep key2| grep key3| ...
进行搜索,通过实践不断的学习新的命令。这个学习方法对你的英文要求有点高,建议用扇贝背单词、DKY背单词小组学习来提高自己的英文实用水平。
我们把这种方法推广到Linux系统调用的学习,通过Linux系统来学习Linux编程。我们要充分利用好man pages。如下图,man pages分为九节,每节的内容大家要熟悉:
我们想要了解系统调用的相关信息,我们搜一下看看:intro(2)
和syscalls(2)
好像是我们要找的东东。我们用man
看看它们能提什么信息。我们先在shell中输入man 2 intro
:
intro(2)
直接让我们找syscalls(2)
,我们在shell中输入man 2 syscalls
:
syscalls(2)
介绍了什么是系统调用,并提供了Linux系统调用的列表。从上图我们看出Linux Kernel 3.1 大约有400个系统调用。当然,我们不需要掌握每一个系统调用,根据20/80定律,我们掌握大约80个核心系统调用就可以解决80%的问题了,文末会给出一些核心系统调用。
我们通过以下3个步骤来学习。
- 分析程序
首先分析现有的Linux命令如who/cp/ls/pwd...,这些命令都在/bin,/usr/bin,/usr/local/bin目录中,通过「别出心裁的Linux命令学习法」中的方法了解它的功能及实现原理。能写出实现该命令的伪代码。 - 学习系统调用
看程序都用到哪些系统调用,以及每个系统调用的功能(参数,返回值,错误码...)和使用方法(相关头文件,库文件,相关系统调用...)。 - 编程实现
利用学到的原理和系统调用,自己编程实现原来程序所实现的功能。
以上3步可以通过下面3个问题来实现:
- 它能做什么?
- 它是如何实现的?
- 能不能自己编写一个?
系统调用学习示例1-who
我们通过解决三个问题来示范如何学习系统调用:
who
命令能做什么who
命令是如何实现的?- 能不能自己编写一个
who
命令?
问题1. who
命令能做什么
一个Linux命令的功能我们可以通过whatis
或man -f
来查看,当然最好的方法是自己通过使用来体验一下:
通过上图我们看到who
命令用来查看谁登录了系统(show who is logged on ),每一行代表一个巳经登录的用户,第1列是用户名,第2列是终端名,第3列是登录时间。
通过whatis who
或man -f who
直接运行命令,可以了解who的大致功能,要进一步了解who
的用法,需要借助联机帮助manpages。我们输入man 1 who
:
所Linux命令的manpages都有相同的基本格式,从第1行可以知道这是关于哪个命令的帮助,还可以知道这个帮助是位于哪一节的。在这个例子中,从第1行的内容who(l),可以知道这是who
命令的帮助,它的小节编号是1。Linux的manpages分为很多节,如第1小节中是关于Linux命令的帮助,第2小节中是关于系统调用的帮助,第5小节中是关于配置文件的帮助。
上面who
的在帮助文档中:
- 名字(NAME)部分包含命令的名字以及对这个命令的简短说明。我们可以通过
whatis who
或man -f who
来获取这部分内容。 - 概要(SYNOPSIS)部分给出了命令的用法说明,包括命令格式、参数(arguments)和选项(Option)列表。选项指的是一个短线后面紧跟着一个或多个英文字母,如-a、-Bc,命令的选项影响该命令所进行的操作。在帮助文档中,方括号([-a])表示该选项不是一个必须的部分。帮助中指出
who
的写法可以是who
或者who -a
,或者who -
加上AbdhHlmMpqrstTu
这些字母的任意组合,在命令的末尾还可以有一个文件参数。
从帮助中可以知道who命令还有其他几种形式:- whoami
- who am i
- who mom likes
从联机帮助中还可以获得上述形式的进一步帮助。
- 描述(DESCRIPTION)部分是关于命令功能的详细阐述,根据命令和平台的不同,描述的内容也不同,有的简洁、精确,有的包含了大量的例子。不管怎么样,它描述了命令的所有功能,而且是这个命令的权威性解释。
- 选项(OPTIONS)部分给出了命令行中每一个选项的说明。
- 参阅(SEE ALSO)部分包含与这个命令相关的其他主题。
到这,即使以前没有用过who
命令,也知道它的功能了。
问题2. who
命令是如何实现的?
前面看到who命令可以显示出当前系统中已经登录的用户信息,联机帮助manpages中描述了who
的功能和用法,现在的问题是:who
是如何来实现这些功能的?
你可能会认为,像who这样的系统程序一定会用到一些特殊的系统调用,需要高级管理员的权限,要编写这样的程序得要花很多钱来购买系统开发工具,包括光盘、参考书等。实际上,所需的资料都在系统中,你要知道的仅仅是如何找到这些资料。
我们首先关注manpages,上面who
的帮肋文档中第43行提供了一个重要信息:
If FILE is no specified, use /var/run/utmp,/var/log/wtmp as FILE is common.
/var/run/utmp
是个什么东东?
我们通过ls /var/run/utmp
可以查年有没有这个东西。我们看到有这个文件.
我们通过file /var/run/utmp
可以查年这是个什么东西(文件类型)。file
告诉我们是个数据(data)文件。这里用了一个命令行技巧:!$
代表了上一条命信令的参数。我们刚运行过ls /var/run/utmp
,!$
就代表/var/run/utmp
,file !$
就等价于file /var/run/utmp
.
我们通过cat /var/run/utmp
查看utmp文件的内容,发现是乱码,但也能看出有who
中的信息。
我们确定了/var/run/utmp
是个二进制文件,可以通过od -tx1 /var/run/utmp
来一个字节一个字节的查看其内容,还挺有规律的,我们可以想象utmp是一条记录,一条记录组成的文件。
命令和结果如下图所示:
我们对utmp大致有个了解和判断了,但还不精确。
我们在「别出心裁的Linux命令学习法」,学到可以像使用Google/Baidu一样使用man -k key1| grep key2| grep key3| ...
在manpages中搜索你想要的信息。我们试试man -k utmp
utmp(5)是个有意思的结构,证实了我们关于「utmp是一条记录,一条记录组成的文件」的猜想。
我们用man 5 utmp
看一下:
一切尽在不言中,我们要实现who
命令的东西都在这了。
who
的联机帮助说who
要读utmp这个文件,进一步,从以上的说明可以知道utmp这个文件里面保存的是结构体数组,数组元素是utmp类型的结构,utmp结构保存了登录记录。它包含9个成员变量,ut_user 数组保存登录名,ut_line 数组保存设备名,也就是用户的终端类型,ut_time 保存登录时间。 utmp这个结构所包含的其他成员没有被who命令所用到。
我们在utmp.h
中应该能找到utmp类型的定义,Linux中的头文件都在/usr/include
目录里。
实践中我们找不到。我们可以通过grep -nr "struct utmp" /usr/include
查找struct utmp在哪个头文件中定义。结果如下图所示:
通过联机帮助manpages来学习Linux就像在网络上寻找信息一样,经常能从某一个帮助主题中找到相关信息,链接到其他有用或有趣的主题。这个过程正是「做中学(Leaning by Doing)」的要义。
问题3. 能不能自己编写一个who
命令?
我们前面说过写程序要写三种代码:
- 伪代码:确定你解决了问题没
- 产品代码: 用计算机语言解决问题,伪代码是产品代码最好的注释
- 测试代码:是不是正确的解决了问题
对于问题3,我们已经解决了,文件中的结构数组存放登录用户的信息,所以直接的想法就是把记录一个一个地读出并显示出来。我们可以写出实现who
命令的伪代码,中英文都行,可能的话最好用英文,我们给出中文的例子:
打开utmp 文件
读取utmp中的每一条记录
显示记录中的相关信息
关闭utmp文件
简单吧?我们下面用C语言写出产品代码。
为了能够顺利实现who
命令,需要经常从联机帮助manpages中获取信息。我们不用写 测试代码,只要把程序的输出与系统who
命令的输出做比较就行了。通过分析可以确认,在编写 who
程序时只有两件事情是要做的:
- 从文件中读取数据结构
- 将结构中的信息以合适的形式显示出来
如果你的C语言基础好,利用C库中的fopen,fread,fclose
就可以实现。我们要学习系统调用,有什么系统调用可用呢?我们想读文件(read a file),不知道用什么系统调用没关系,我们用man -k read | grep file | grep 2
搜一下:
read(2)好象是我们需要的,其他的看起来都不像,所以进一步用man 2 read
看read(2)的帮助:
NAME节解释了这个这个系统调用的功能,
这个系统调用可以将文件中一定数目的字节(参数count)读入一个缓冲区(参数buf),因为每次都要读入一个数据结构,所以要用sizeof(struct utmp)来指定每次读人的宇节数。read函数需要一个文件描述符作为输人参数,如何得到文件描述符呢?我们关注一下上面read(2)的帮助的「SEE ALSO」部分。你如果还记得C语言中的读文件的fopen,fread,fclose
的组合模式,你就会想到open(2)正是我们需要的。我们用man 2 open
看一下open(2)的帮助:
查看open(2)的联机帮助,从open(2)中又可以找到对close(2)的引用,通过阅读联机帮助,可以知道以上3个系统调用(open(2),read(2),close(2))都是进行文件操作所必需的。
大家可以看到,上面的帮助文档都是英文的。对大部分同学来说,一看是英文的,再简单都没信心看下去。实在不行,你可以通过sudo apt-get install manpages-zh
安装中文版帮助文档:
中文帮助文档与英文不是一一对应的,少了一些内容,并且翻译的太差了。在我看来,还没英文版的看着舒服。这与买一些烂的翻译版图书一样,还不如看原版。
我们现在实现一下who
命令,源程序文件名who1.c:
#include <stdio.h>
#include <stdlib.h>
#include <utmp.h>
#include <fcntl.h>
#include <unistd.h>
int show_info( struct utmp *utbufp )
{
printf("%-8.8s", utbufp->ut_name);
printf(" ");
printf("%-8.8s", utbufp->ut_line);
printf(" ");
printf("%10ld", utbufp->ut_time);
printf("\n");
return 0;
}
int main()
{
struct utmp current_record;
int utmpfd;
int reclen = sizeof(current_record);
//打开utmp 文件
if ( (utmpfd = open(UTMP_FILE, O_RDONLY)) == -1 ){
perror( UTMP_FILE );
exit(1);
}
//读取utmp中的每一条记录
while ( read(utmpfd, ¤t_record, reclen) == reclen )
//显示记录中的相关信息
show_info(¤t_record);
//关闭utmp文件
close(utmpfd);
return 0;
}
这段代码应用了前面学到的内容,在while循环内从文件中逐条地把数据读取出来,存放在记录current_record中,然后调用函数show_info把登录信息显示出来,当文件中已经没有数据时,循环结束,最后关闭文件返回。这里调用了函数perror,这是一个系统函数,使用这个函数来处理系统报错。
我们用gcc who1.c -o who1
编译一下程序,运行结果如下:
我们自己编写的who1已经可以工作了,它能正确显示出用户名、终端名、登录时间,但跟系统的who
命令比起来还不完善,至少在两处有问题需要改进的:
- 消除空白记录
- 正确显示登录时间
针对这两个问题,我们继续编写who的第2个版本,解决问题的方法还是通过阅读联机帮助manpages和头文件utmp.h。
消除空白记录
系统所带的who
只列出已登录用户的信息,而刚才编写的who1除了会列出已登录的用户,还会显示其他的信息,而这些都来自于utmp文件。实际上utmp包含所有终端的信息,甚至那些尚未被用到的终端的信息也会存放在utmp中,所以要修改刚才的程序,做到能够区分出哪些终端对应活动的用户。如何区分呢?
一种简单的思路是过滤掉那些用户名为空的记录,但这样做是有问题的,如刚才的输出中,用户名为LOGIN的那一行对应的是控制台,而不是一个真实的用户。最好有一种方法能够指出某一条记录确实对应着已登录的用户。
在utmp. h中,有以下内容:
utmp结构中有一个成员ut_type,当它的值为7(USER_PROCESS)时,表示这是一个巳经登录的用户。根据这一点,对原来的程序做以下修改,就可以消除空白行:
show_info( struct utmp * utbufp ){
if ( utbufp-〉ut_type != USER_PROCESS ) /* users only */
return;
printf ( " % - 8. 8s", utbufp ->ut_name) ; /* the username */
}
以可读的方式显示登录时间
接下来要处理的是时间显示的问题,要把时间以易于理解的形式显示。还是需要借助联机帮助manpages和头文件utmp.h。
如果你C语言基础比较好,对time.h比较熟悉,这是一个很简单的问题。如果不知道怎么办也没关系,我们用man -k key1|grep key2|...
来搜索。现在要解决的问题是转换(convert,transform)时间格式,我们用man - k time | grep convert
和man - k time | grep transform
搜一下:
who1中的时间是time_t数据类型,用一个整数来表示,它的数值是从1970年1月1日0时开始所经过的秒数。ctime(3)将表示时间的整数值转换成人们日常所使用的时间形式。
关于程序设计中的时间,可以参考漫谈程序设计中的时间。
ctime(3)函数要输人一个指针,返回的时间字符串类似于以下格式:
Wed Jun 30 21:49:08 2016
注意:并不是所有的字符串内容都需要,需要的是其中用标识出来的部分,接下来就很容易处理了,将ctime返回的字符串从第4个字符开始,输出12个字符:
printf("% 12.12s",ctime(&t) + 4)
我们可以编写who2.c了:
#include <stdio.h>
#include <unistd.h>
#include <utmp.h>
#include <fcntl.h>
#include <time.h>
void showtime(long);
void show_info(struct utmp *);
int main()
{
struct utmp utbuf; /* read info into here */
int utmpfd; /* read from this descriptor */
if ( (utmpfd = open(UTMP_FILE, O_RDONLY)) == -1 ){
perror(UTMP_FILE);
exit(1);
}
while( read(utmpfd, &utbuf, sizeof(utbuf)) == sizeof(utbuf) )
show_info( &utbuf );
close(utmpfd);
return 0;
}
void show_info( struct utmp *utbufp )
{
if ( utbufp->ut_type != USER_PROCESS )
return;
printf("%-8.8s", utbufp->ut_name); /* the logname */
printf(" "); /* a space */
printf("%-8.8s", utbufp->ut_line); /* the tty */
printf(" "); /* a space */
showtime( utbufp->ut_time ); /* display time */
printf("\n"); /* newline */
}
void showtime( long timeval )
{
char *cp; /* to hold address of time */
cp = ctime(&timeval); /* convert time to string */
/* string looks like */
/* Mon Feb 4 00:46:40 EST 2016 */
/* 0123456789012345. */
printf("%12.12s", cp+4 ); /* pick 12 chars from pos 4 */
}
至此就完成了who
命令的编写。
系统调用学习示例2-cp
我们再通过cp
命令示范如何学习系统调用,同样有三个问题:
cp
命令能做什么cp
命令是如何实现的?- 能不能自己编写一个
cp
命令?
问题1. cp
命令能做什么
cp
能够复制文件,典型的用法是:
cp source-file target-file
如果target-file所指定的文件不存在,cp
就创建这个文件,如果已经存在就覆盖,target-file的内容与source-file相同。
问题2. cp
命令是如何实现的?
了解cp
的功能,实现cp
很简单,伪代码:
打开source-file
创建target-file
从source-file读出一段数据
把这段数据写入target-file
关闭source-file
关闭target-file
问题3. 能不能自己编写一个cp
命令?
现在的问题是如何创建文件?如何写文件?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#define BUFFERSIZE 4096
#define COPYMODE 0644
void oops(char *, char *);
int main(int argc, char *argv[])
{
int in_fd, out_fd, n_chars;
char buf[BUFFERSIZE];
if (argc != 3) {
fprintf(stderr, "usage: %s source destination\n", *argv);
exit(1);
}
if ((in_fd = open(argv[1], O_RDONLY)) == -1)
oops("Cannot open ", argv[1]);
if ((out_fd = creat(argv[2], COPYMODE)) == -1)
oops("Cannot creat", argv[2]);
while ((n_chars = read(in_fd, buf, BUFFERSIZE)) > 0)
if (write(out_fd, buf, n_chars) != n_chars)
oops("Write error to ", argv[2]);
if (n_chars == -1)
oops("Read error from ", argv[1]);
if (close(in_fd) == -1 || close(out_fd) == -1)
oops("Error closing files", "");
}
void oops(char *s1, char *s2)
{
fprintf(stderr, "Error: %s ", s1);
perror(s2);
exit(1);
}
这里要注意main函数的两个参数:
- argc记录了用户在运行程序的命令行中输入的参数的个数。
- arg[]指向的数组中至少有一个字符指针,即arg[0].它通常指向程序中的可执行文件的文件名。
总结
通过上面的例子,我们学习了Linux中学习Linxu系统编程的方法:
- 仔细研究manpages
- 问题驱动,使用
man -k key1|grep key2|...
在manpages中搜索你要的内容 - 阅读.h文件: 可以通过
grep -nr XXXX /usr/incldue
查找相关的宏定义,结构体定义,类型定义等 - 解决一个问题要多个系统调用,可以参考manpages的
SEE ALSO
部分来得到相关系统调用的信息
更好的答案
我们上面通过man -k key1|grep key2|...
推导出实现who
命令,cp
命令所需要的系统调用,这个过程对学习来讲意义很大。其实我们可以使用strace who
来查看实际上调用了哪些系统调用。十种不好的学习方式中说「只看问题答案而不去自己解题」是个不好的学习方式:
看答案的时候感觉什么都会,自己一碰到题目立刻抓瞎,这种情况怕是谁都遭遇过罢。
看答案的过程是一个单向输入的过程,而输入结果怎么样,只有靠输出才能验证。所谓厚积薄发,其实在很多方面都适用,想要自己独立解答出题目,就必须对相关概念以及概念背后的知识理解到一定深度才可能成功。看懂答案仅仅是掌握的第一步,止步于此会让所有的以为理解变成最终遗忘。
与看答案的坏方式类似的还有一种坏方式,就是仅思考解题思路而不动手解题。中学时候一同学成绩不错,做课后习题很少动手,只是在大脑里模拟解题,感觉思路没有问题后就在题目旁边标注一个 “易”,然后跳过继续往后做其它题目。这种方式貌似很高效,最后却在考试中惨败,很多本以为思路很明确的地方在实际解题时却发现完全不是自己想象的那样。所以勤动手永远是学习中最重要的几点之一。
常用的系统调用
通过上面的方法,我们可以学习Linux的核心系统调用,学习路径参考下图:
通过「做中学(Learning by doing)」进行Linux系统编程学习是个很好的方法。有了上面的学习方法,剩下的就是时间问题了。
按我的经验,学Linux系统调用一两 个月够了,按某个培训机构的说法,你可以月薪11k了:
参考资料
- Unix/Linux编程实践教程(Understanding UNIX/LINUX Programming)
- 深入理解计算机系统(英文版,官网2E,官网3E)
- Unix系统编程(Unix Systems Programming)
- 计算机系统要素(The Elements of Computing Systems)
- Unix编程环境(The Unix Programming Environment)
操作系统教程
欢迎关注“rocedu”微信公众号(手机上长按二维码)
做中教,做中学,实践中共同进步!
版权声明:自由转载-非商用-非衍生-保持署名| Creative Commons BY-NC-ND 3.0
如果你觉得本文对你有帮助,请点一下左下角的“好文要顶”和“收藏该文”