momo zone

调核人的blog

Monthly Archives: 十月 2010

关于scullc 中的一些问题

不知道作者怎么想的,scullc的一些算法和scull不一样,除了使用了slab 还有就是scullc_follow 和scull_follow的差别。前者返回一个scullc_dev 后者返回一个scull_qset这两个不是一个层面的东西嘛 ,结果 scullc_dev 充当了两个角色,scullc设备结构和量子集,不过这样用下来也没有问题。因为scullc.h 中的scullc_dev是这样定义的:

struct scullc_dev {
void **data;
struct scullc_dev *next;  /* next listitem */
int vmas;                 /* active mappings */
int quantum;              /* the current allocation size */
int qset;                 /* the current array size */
size_t size;              /* 32-bit will suffice */
struct semaphore sem;     /* Mutual exclusion */
struct cdev cdev;
};

注意红色,看来作者真是故意的,但我不了解这要搞的目的。我对比了一下scull和scullc 的效率,确实后者好很多!

localhost:~ # time dd if=/media/disk-2/chrono_cross_1.flv of=/dev/scullc
99840+1 records in
99840+1 records out
51118215 bytes (51 MB) copied, 0.106118 s, 482 MB/s
real 0m0.109s
user 0m0.028s
sys  0m0.079s
localhost:~ # time dd if=/media/disk-2/chrono_cross_1.flv of=/dev/scull
99840+1 records in
99840+1 records out
51118215 bytes (51 MB) copied, 0.265509 s, 193 MB/s
real 0m0.269s
user 0m0.022s
sys  0m0.218s

差不多快了3倍 !

但有两点ldd3 中没有说明

1. 其实kmalloc 也是基于slab 伙伴系统来分配内存的,不同的是每次kmalloc的大小不同, 因此是从不同的slab队列中分配。直接使用内核cache的话在分配和回收的时候都是固定大小,这样可以少执行一些代码,所以开销小,速度快。

2. 用于kmalloc可分配(每次调用)的内存大小范围在32~131027(128k)字节,并且由于它用slab分配器来分配内存的,所以,得到的内存大小可能比你申请的要大一些(它向上取2的N次幂整数)。而且如果开启了CONFIG_LARGE_ALLOCS选项,这个值可以更大,可以达到了32M。

关于0号(idle)进程

1. idle是什么

简单的说idle是一个进程,其pid号为 0。其前身是系统创建的第一个进程,也是唯一一个没有通过fork()产生的进程。在smp系统中,每个处理器单元有独立的一个运行队列,而每个运行队列上又有一个idle进程,即有多少处理器单元,就有多少idle进程。系统的空闲时间,其实就是指idle进程的”运行时间”。既然是idle是进程,那我们来看看idle是如何被创建,又具体做了哪些事情?

2. idle的创建

我们知道系统是从BIOS加电自检,载入MBR中的引导程序(LILO/GRUB),再加载linux内核开始运行的,一直到指定shell开始运行告一段落,这时用户开始操作Linux。而大致是在vmlinux的入口startup_32(head.S)中为pid号为0的原始进程设置了执行环境,然后原是进程开始执行start_kernel()完成Linux内核的初始化工作。包括初始化页表,初始化中断向量表,初始化系统时间等。继而调用 fork(),创建第一个用户进程:

kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);

这个进程就是着名的pid为1的init进程,它会继续完成剩下的初始化工作,然后execve(/sbin/init), 成为系统中的其他所有进程的祖先。关于init我们这次先不研究,回过头来看pid=0的进程,在创建了init进程后,pid=0的进程调用 cpu_idle()演变成了idle进程。

current_thread_info()->status |= TS_POLLING;

在 smp系统中,除了上面刚才我们讲的主处理器(执行初始化工作的处理器)上idle进程的创建,还有从处理器(被主处理器activate的处理器)上的idle进程,他们又是怎么创建的呢?接着看init进程,init在演变成/sbin/init之前,会执行一部分初始化工作,其中一个就是 smp_prepare_cpus(),初始化SMP处理器,在这过程中会在处理每个从处理器时调用

task = copy_process(CLONE_VM, 0, idle_regs(&regs), 0, NULL, NULL, 0);

init_idle(task, cpu);

即从init中复制出一个进程,并把它初始化为idle进程(pid仍然为0)。从处理器上的idle进程会进行一些Activate工作,然后执行cpu_idle()。

整个过程简单的说就是,原始进程(pid=0)创建init进程(pid=1),然后演化成idle进程(pid=0)。init进程为每个从处理器(运行队列)创建出一个idle进程(pid=0),然后演化成/sbin/init。

3. idle的运行时机

idle 进程优先级为MAX_PRIO,即最低优先级。早先版本中,idle是参与调度的,所以将其优先级设为最低,当没有其他进程可以运行时,才会调度执行idle。而目前的版本中idle并不在运行队列中参与调度,而是在运行队列结构中含idle指针,指向idle进程,在调度器发现运行队列为空的时候运行,调入运行。

4. idle的workload

从上面的分析我们可以看出,idle在系统没有其他就绪的进程可执行的时候才会被调度。不管是主处理器,还是从处理器,最后都是执行的cpu_idle()函数。所以我们来看看cpu_idle做了什么事情。

因为idle进程中并不执行什么有意义的任务,所以通常考虑的是两点:1.节能,2.低退出延迟。

其核心代码如下:

void cpu_idle(void)  {
int cpu = smp_processor_id();
current_thread_info()->status |= TS_POLLING;    /* endless idle loop with no priority at all */
while (1)
{tick_nohz_stop_sched_tick(1);
      while (!need_resched())
            {check_pgt_cache();      
             rmb();
             if (rcu_pending(cpu))
                rcu_check_callbacks(cpu, 0);
             if (cpu_is_offline(cpu))       
                play_dead();    
             local_irq_disable();      
             __get_cpu_var(irq_stat).idle_timestamp = jiffies;      /* Don’t trace irqs off for idle */      
             stop_critical_timings();      
             pm_idle();      
             start_critical_timings();     
            }     
      tick_nohz_restart_sched_tick();     
      preempt_enable_no_resched();     
      schedule();     
      preempt_disable();   
 }  
}

循环判断need_resched以降低退出延迟,用idle()来节能。

默认的idle实现是hlt指令,hlt指令使CPU处于暂停状态,等待硬件中断发生的时候恢复,从而达到节能的目的。即从处理器C0态变到C1态(见 ACPI标准)。这也是早些年windows平台上各种”处理器降温”工具的主要手段。当然idle也可以是在别的ACPI或者APM模块中定义的,甚至是自定义的一个idle(比如说nop)。

小结:

1.idle是一个进程,其pid为0, 对应的command 是swapper (为什么是这个名字,据说是因为一些历史原因)。

2.主处理器上的idle由原始进程(pid=0)演变而来。从处理器上的idle由init进程fork得到,但是它们的pid都为0。

3.Idle进程为最低优先级,且不参与调度,只是在运行队列为空的时候才被调度。

4.Idle循环等待need_resched置位。默认使用hlt节能。

LLD3 example for kernel 2.6.20+

之前自己尝试着修改了一下,花了不少功夫,不过今天google 了一下发现竟然有站点在更新git版本,好像是美国波特兰州立大学计算机科学系的一个资源服务器。话说这方面国内大学做的真的很不够。

http://svcs.cs.pdx.edu/gitweb?p=ldd-examples.git;a=tree

 

update:

github 的一个更新版本:https://github.com/martinezjavier/ldd3

有多个分支,目前支持到2.6.37 。但实际上3.1 编译起来也没有问题。

ldd3 关于内核计数和定时器的调试

我终于体会到写一段糟糕的内核代码是多么容易把系统搞死(锁死)。

首先看这个:

while(time_before(jiffies,ji))

cpu_relax();

或者

mdelay(3000);

这将使运行该段代码的cpu循环执行nop空指令,而该cpu在期间将不能做任何事情,被锁死。复杂的情况是上面还讲在超线程cpu上仍然可以执行其他代码甚至执行调度,不过我手里没有这样的cpu 没办法验证了呵呵。还有就是可抢占内核也可以避免被锁死,和超线程cpu的情景有点相似。

我先在amd 的一款双核cpu上用2.6.34-debug内核试验了一下:执行这段代码的cpu id 和X window(不确定还没有搞明白具体哪个进程) 等其他进程所在cpu id不一致的情况下就不会死锁(其实是gui等无响应了,用ps -eo psr,pid,comm 可以查看cpu绑定状态),如果恰好在另一cpu id 执行就没有任何状况。

而我在core duo E6300 2.6.35上调试时就是另外一个情况,即使手动用taskset -c 指定cpu 仍然不会有任何死锁的状况,难道我自己编译的2.6.35 可抢占,而2.6.34-debug 不能抢占 ?看了一下kernel config 发现确实是这样,CONFIG_PREEMPT就是用来设置内核是否可以抢占的标志。

然后看另一个问题(琢磨了很长时间):

关于内核定时器实验代码jit模块中的jit_timer_fn 定时器超时事件函数:

void jit_timer_fn(unsigned long arg)
{
struct jit_data *data = (struct jit_data *)arg;
unsigned long j = jiffies;
printk(KERN_INFO “timer out,loop:%d,jiff:%9lu\n”,data->loops,j);
data->buf += sprintf(data->buf, “%9li  %3li     %i    %6i   %i   %s\n”,
j, j – data->prevjiffies, in_interrupt() ? 1 : 0,
current->pid, smp_processor_id(), current->comm);
if (–data->loops) {
data->timer.expires += tdelay;
data->prevjiffies = j;
add_timer(&data->timer);
} else {
wake_up_interruptible(&data->wait);
}
}

这段代码的结果用cat读出来很令人意外

Oct 13 21:29:39 linux-hm12 kernel: [ 9502.654843] go sleep
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.664131] timer out,loop:5,jiff:  9202664
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.674130] timer out,loop:4,jiff:  9202674
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.684130] timer out,loop:3,jiff:  9202684
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.694131] timer out,loop:2,jiff:  9202694
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.704130] timer out,loop:1,jiff:  9202704
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.704153] waken
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.704177] go sleep
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.714005] timer out,loop:5,jiff:  9202714
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.724005] timer out,loop:4,jiff:  9202724
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.734007] timer out,loop:3,jiff:  9202734
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.744004] timer out,loop:2,jiff:  9202744
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.754005] timer out,loop:1,jiff:  9202754
Oct 13 21:29:39 linux-hm12 kernel: [ 9502.754011] waken

localhost:~/ldd3/misc-modules # cat /proc/jitimer
time   delta  inirq    pid   cpu command
9202654    0     0     17541   1   cat
9202664   10     1         0   1   swapper
9202674   10     1         0   1   swapper
9202684   10     1         0   1   swapper
9202694   10     1         0   1   swapper
9202704   10     1         0   1   swapper

使用cat读取时共2次延迟输出(启动定时器并睡眠),而不是预想的1次,而且是结果是在第一次延迟和第二次延迟中间一次全部输出。后者的原因是因为/proc使用了输出缓冲。

反复debug我确定是在return后内核又自动执行了jit_timer() 。 主要到p191 这样说:

请确保每次从/proc/jitbusy 中读取至多一行(或几行)。用来注册/proc文件的简化内核机制会反复调用read方法,以填充用户请求的数据缓冲区……读取/proc/jitbusy的推荐命令是dd bs=20 </proc/jitbusy。

我用了dd if=/proc/jitimer bs=1000 count=1 这样输出是正常了!! 以后慎用cat ,误人子弟。

这里还有一个问题就是对于输出结果中的pid 0 以及对应的command的问题,其实这个是idle 进程,前面的文章有介绍。至于为什么输出结果有pid0 是因为定时器超时函数执行的上下文不属于注册该定时器的进程,而是属于内核进程。如果此时内核空闲就会载入pid0 进程。如果在这个时候发生了定时器超时,那么上下文也就是pid0了。哈哈,可以想到这样就能看到某个时刻内核正在调度哪个线程 ~~

验证一下,让cpu忙一点,找个视频播放一下,结果如下:

localhost:~/ldd3/misc-modules # taskset -c 1 dd if=/proc/jitimer bs=1000 count=1
time   delta  inirq    pid   cpu command
11902610    0     0     25684   1   dd
11903612  1002     1         7   1   ksoftirqd/1
11904614  1002     1      2894   1   Xorg
11905616  1002     1     20109   1   gnome-system-mo
11906618  1002     1         0   1   swapper
11907620  1002     1         7   1   ksoftirqd/1

ps 进程状态解释

系统维护的时候难免会遇到进程的状态的查询和管理,到底什么是R,有的是S,有的还是S+呢?一直有些混沌的问题,今天细细的来总结一下:
ps是用来报告系统中程序执行状况的命令这个是无可厚非的,linux进程的状态:
D    不可中断睡眠 (通常是在IO操作) 收到信号不唤醒和不可运行, 进程必须等待直到有中断发生
R   正在运行或可运行(在运行队列排队中)
S   可中断睡眠 (休眠中, 受阻, 在等待某个条件的形成或接受到信号)
T   已停止的 进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行
W   正在换页(2.6.内核之前有效)
X   死进程 (未开启)
Z   僵尸进程  进程已终止, 但进程描述符存在, 直到父进程调用wait4()系统调用后释放BSD风格的
<   高优先级(not nice to other users)
N   低优先级(nice to other users)
L   页面锁定在内存(实时和定制的IO)
s   一个信息头
l   多线程(使用 CLONE_THREAD,像NPTL的pthreads的那样)
+   在前台进程组
例如:
[test@pan ~]$ ps -aux
USER     PID    %CPU    %MEM    VSZ  RSS     TTY   STAT  START       TIME     COMMAND
root         1      0.0        0.0       1672  516       ?        S       Apr21      0:00    init [5]
root         2      0.0        0.0           0    0       ?        S       Apr21      0:00    [migration/0]
root         3      0.0        0.0           0    0       ?        SN      Apr21      0:00    [ksoftirqd/0]
root         4      0.0        0.0           0    0       ?        S       Apr21      0:00    [migration/1]
root         5      0.0        0.0          0    0        ?        SN      Apr21      0:00    [ksoftirqd/1]
root         6      0.0        0.0          0    0        ?        S       Apr21      0:00    [migration/2]
其中这个参数来列出所有的信息以提供自己检查程序的问题!在上面的程序列出当中,说明如下:
USER:说明该程序是属于哪一个人的;
PID:该程序的代号;
%CPU:代表该程序使用了多少 CPU 资源;
%MEM:代表该程序使用了多少的 RAM ;
VSZ, RSS:占去的 ram 的大小( bytes );
TTY:是否为登入者执行的程序?若为 tty1-tty6 则为本机登入者,若为 pts/?? 则为远程登入者执行的程序
STAT:该程序的状态
START:该程序开始的日期;
TIME:该程序运行的时间?
COMMAND:该程序的内容啦!

ps命令一般和grep搭配使用,列出某个特定进程的状态
ps -aux |grep Mega或者ps -ef |grep Mega  (检查进程名包含Mega的进程运行状况)

man ps的解释:
linux ps(process status) 命令详解
功能说明:报告程序状况。
语  法:ps [-aAcdefHjlmNVwy][acefghLnrsSTuvxX][-C <指令名称>][-g <群组名称>][-G <群组识别码>][-p <程序识别码>][p <程序识别码>][-s <阶段作业>][-t <终端机编号>][t <终端机编号>][-u <用户识别码>][-U <用户识别码>][U <用户名称>][-<程序识别码>][–cols <每列字符数>][– columns <每列字符数>][–cumulative][– elect][–forest][–headers][–help] [–info][–lines <显示列数>][–no-headers][–group <群组名称>][-Group <群组识别码>][–pid <程序识别码>][–rows <显示列数>][–sid <阶段作业>][–tty <终端机编号>][–user <用户名称>][–User <用户识别码>][–version][–width <每列字符数>]

补充说明:ps是用来报告程序执行状况的指令,您可以搭配kill指令随时中断,删除不必要的程序。
参  数:
-a  显示所有终端机下执行的程序,除了阶段作业领导者之外。
a   显示现行终端机下的所有程序,包括其他用户的程序。
-A  显示所有程序。
-c  显示CLS和PRI栏位。
c  列出程序时,显示每个程序真正的指令名称,而不包含路径,参数或常驻服务的标示。
-C  <指令名称>  指定执行指令的名称,并列出该指令的程序的状况。
-d  显示所有程序,但不包括阶段作业领导者的程序。
-e  此参数的效果和指定”A”参数相同。
e   列出程序时,显示每个程序所使用的环境变量。
-f  显示UID,PPIP,C与STIME栏位。
f   用ASCII字符显示树状结构,表达程序间的相互关系。
-g<群组名称>  此参数的效果和指定”-G”参数相同,当亦能使用阶段作业领导者的名称来指定。
g   显示现行终端机下的所有程序,包括群组领导者的程序。
-G<群组识别码>  列出属于该群组的程序的状况,也可使用群组名称来指定。
h   不显示标题列。
-H  显示树状结构,表示程序间的相互关系。
-j或j  采用工作控制的格式显示程序状况。
-l或l  采用详细的格式来显示程序状况。
L    列出栏位的相关信息。
-m或m  显示所有的执行绪。
n  以数字来表示USER和WCHAN栏位。
-N  显示所有的程序,除了执行ps指令终端机下的程序之外。
-p<程序识别码>  指定程序识别码,并列出该程序的状况。
p<程序识别码>  此参数的效果和指定”-p”参数相同,只在列表格式方面稍有差异。
r  只列出现行终端机正在执行中的程序。
-s<阶段作业>  指定阶段作业的程序识别码,并列出隶属该阶段作业的程序的状况。
s  采用程序信号的格式显示程序状况。
S  列出程序时,包括已中断的子程序资料。
-t<终端机编号>  指定终端机编号,并列出属于该终端机的程序的状况。
t<终端机编号>  此参数的效果和指定”-t”参数相同,只在列表格式方面稍有差异。
-T  显示现行终端机下的所有程序。
-u< 用户识别码>  此参数的效果和指定”-U”参数相同。
u  以用户为主的格式来显示程序状况。
-U<用户识别码>  列出属于该用户的程序的状况,也可使用用户名称来指定。
U<用户名称>  列出属于该用户的程序的状况。
v  采用虚拟内存的格式显示程序状况。
-V或V  显示版本信息。
-w或w  采用宽阔的格式来显示程序状况。
x  显示所有程序,不以终端机来区分。
X  采用旧式的Linux i386登陆格式显示程序状况。
-y  配合参数”-l”使用时,不显示F(flag)栏位,并以RSS栏位取代ADDR栏位 。
-<程序识别码>  此参数的效果和指定”p”参数相同。
–cols<每列字符数>  设置每列的最大字符数。
–columns<每列字符数>  此参数的效果和指定”–cols”参数相同。
–cumulative  此参数的效果和指定”S”参数相同。
–deselect  此参数的效果和指定”-N”参数相同。
–forest  此参数的效果和指定”f”参数相同。
–headers  重复显示标题列。
–help  在线帮助。
–info  显示排错信息。
–lines<显示列数>  设置显示画面的列数。
–no-headers  此参数的效果和指定”h”参数相同,只在列表格式方面稍有差异。
–group<群组名称>  此参数的效果和指定”-G”参数相同。
–Group<群组识别码>  此参数的效果和指定”-G”参数相同。
–pid<程序识别码>  此参数的效果和指定”-p”参数相同。
–rows<显示列数>  此参数的效果和指定”–lines”参数相同。
–sid<阶段作业>  此参数的效果和指定”-s”参数相同。
–tty<终端机编号>  此参数的效果和指定”-t”参数相同。
–user<用户名称>  此参数的效果和指定”-U”参数相同。
–User<用户识别码>  此参数的效果和指定”-U”参数相同。
–version  此参数的效果和指定”-V”参数相同。
–widty<每列字符数>  此参数的效果和指定”-cols”参数相同。

ldd3 scull 调试总结

前段时间仔细折腾了scull这个“设备”,这段代码有很多细节书中并未详解,只有反复调试才能发现其中端倪:

1.首先在每个文件操作回调函数首部中加入printk,另外调试的时候起初用的是echo>/dev/scull 和cat /dev/scull 发现这样会使write,read的调用过程很模糊,后来想到用dd 因为他的调用过程我还算明白点。

2.关于scull_read函数,使用dd if=/dev/scull of=/dev/null bs=100000 如果bs超过单个quantum(量子,4k大小),那么scull_read 将多次被内核调用,每次调用读取单个quantum的数据到用户空间。这个一开始没想明白,盯着/var/log/message的调试信息琢磨了很长时间。过程是这样: 每次调用read的时候都要对传入的count进行修正:

if (*f_pos + count > dev->size)
count = dev->size – *f_pos;

这样将会使count获得总的剩余字节数。 然后还会进行第二次修正

if (count > quantum – q_pos){
count = quantum – q_pos;

这样count就是一个量子内的剩余字节数。所以count实际上最大只能是一个量子(4k)大小。 最后read函数返回count+读取指针偏移值,内核发现返回值小于调用时的count值,那么内核会继续调用read直到所有字节都读取完毕。

3.再一个是关于信号量的问题。 if (down_interruptible(&dev->sem)) 我在这个条件中加入了printk不过怎么都不会有message出现,后来终于想明白了,因为如果发现已上锁进程就会去休眠,而不会去执行if条件,除非睡眠失败。这里观察信号量作用的方法是用两个dd进程输出大文件到设备,会发现两个进程交替锁定该段代码(频度并不高,受内核调度策略的影响)。

4.关于几种信号量加锁函数的区别可以用down_trylock()比较一下,把scull_open中的down_interruptible()替换掉。并且把if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) 注释掉(因为在read前会调用open但该条件语句会阻止读操作下加锁!!)。最后要在down_trylock 和up之间加上

set_current_state(TASK_INTERRUPTIBLE);                schedule();

这样会把当前进程强制睡眠(也可以用内核api去做)。

ok 执行第一个dd去写:

dd if=main.c of=/dev/scull

可以看到内核消息:

Oct 10 16:06:55 linux-hm12 kernel: [ 6760.654095] scull_open

而且dd 不会返回,进入睡眠

localhost:~ # ps fl |grep dd0     0 14622  3759  20   0   2272   536 scull_ S+   pts/1      0:00  \_ dd if=main.c of=/dev/scull

接着执行第二个dd去读:

# dd if=/dev/scull of=/dev/null bs=10000

结果是:

dd: opening `/dev/scull’: Unknown error 512

内核消息是:

Oct 10 16:17:56 linux-hm12 kernel: [ 7421.095061] scull_open

Oct 10 16:17:56 linux-hm12 kernel: [ 7421.095065] in open down_interr

可见第二个open上锁失败,进入if语句。

OSS v4.2 安装分析

官方提供了rpm包,主要包含如下部分:

二进制工具

配置档模版

c源码,头文件,动态库

辅助脚本

文档

该rpm在复制文件之后会执行/usr/lib/oss/build/install.sh脚本,该脚本流程如下

1.设置OSSLIBDIR,oss工作目录。

2.检查ext3模块的REGPARM参数和辅助工具ossvermagic -r 的执行结果来判断内核是不是2.6.20或更新 ,这些版本的模块将使用CONFIG_REGPARM指定的模式传递参数,进一步正确链接工作目录下object和modules目录符号。

3.判断/lib/modules/$(uname -r)/kernel/oss 下是否有旧的oss内核模块,如果有就关掉oss系统(soundoff),并清除旧模块。

4.判断/usr/lib/oss/build/下是否有旧的oss源代码,如果有就全部清除掉。

5.检查gcc工具链和库文件,以及内核源文件。

6.编译osscore模块,并用ld链接后产生ko文件到内核模块路径。

7.编译modules 目录下的其他模块,并用ld链接后产生ko文件到内核模块路径。

8.生成各模块对应的设定档到conf目录。

9.安装/etc/init.d 启动脚本。

10. 编译flash支持动态库

11.安装hal脚本,安装hal fdi文件 (热插拔设备被内核识别后加入/sys目录,并被hal的 fdi match,然后根据append的脚本来添加设备文件,并加入链接文件)

上面的安装脚本执行完后还要执行/usr/lib/oss/scripts/remove_drv.sh ,它将系统的声音系统从alsa替换为ossv4

如果要恢复alsa系统则要执行/usr/lib/oss/scripts/restore_drv.sh

linux音频系统的那点事

这几年linux 音频架构层面上演着一出又一出的闹剧,你方唱罢我方登场。每次发行版更新时声称某某架构解决了某某问题,提供了某某特性,表面看起来是在解决问题,其实则是混乱不堪,让一般用户无所适从。音频这块可以说是就像是泡久了的面条一样找不到头绪。

从原本的单纯声卡驱动OSS ,到重新发明轮子的alsa 。这期间的一系列变化以及alsa的不足导致了大量应用程序不得不反复修改来对付这一系列的糟糕变化。OSS的单纯(仅仅用来驱动硬件,不涉及混合器和复杂的API)是基于早期类似声霸卡这样的玩意,这是一套完备独立的音频硬件,具备了硬件混音器。 后来intel 搞出来了AC97 ,以及后来的HDA ,一切变得复杂起来了(当然主要是成本也低了):声卡变成了软声卡,或者与其说是声卡不如说是几个音频D/A+一堆底层API(在windows的AC97或HDA驱动中),也就是说这些低端玩意都不具备硬件混音器,这些功能都抛给软件去做了。当然也就有了相对比较大的延迟,并且特效也要软件去做了。

OSS是朴素的,设计也是遵循经典的linux 设备驱动模型,看到/dev出现了一个个音频设备,一切都不是那么迷茫。ls >/dev/dsp 是发出的噪音让人意识到“让这个玩意发音太容易了”。 到了alsa的时候就没那么幸运了,设备开始变得看不到摸不着,驱动开始像保姆一样既在内核里面插一腿又在应用层环境里自做主张而且还试图模仿(模拟)OSS。更糟的是音质的问题十分严重。而且混音器工作得并不好。

然后就是八仙过海各显神通的时代了,为了解决多音频流问题,各个桌面环境都提供了一系列的声音服务(aRts和ESD),这些服务都是各自为战,用尽各种曲线救国的方法掩盖linux音频系统的种种缺陷。这些设计连同alsa自身的很多方面都严重背离了linux系统设计的原则。

这里有篇文章详细介绍了linux音频架构的混乱,后一篇则是近段时间写,讲了一下这方面的近况,并毫不避讳的对OSS V4 大大赞赏。

Linux音频系列之一:ALSA是垃圾,OSS王道!

翻译自
http://insanecoding.blogspot.com/2007/05/sorry-state-of-sound-in-linux.html被墙,请用www.sneakme.net。一个有广度和深度的评论。我觉得我们真的应该思考一下这个问题。
– 事情从什么时候开始变得糟糕的。。。
– ALSA怎么了,为什么突然开始使用这么个文档不良的东西。。。。
– 我们应该支持闭源吗?不过,这篇文章是两年以前写的,现在,事情已经变了。另一篇博文:
http://insanecoding.blogspot.com/2009/06/state-of-sound-in-linux-not-so-sorry.html
分析了当前Linux声音系统的现状。翻译随后奉上。Linux声音系统的可悲境地
作者:Insane Coding
日期:2007-05-17

我们先谈谈背景吧。

回到过去的日子里,如果你有一台PC,那么,只有一种卡可以称为“声卡”,它叫做Sound Blaster16,通称“声霸16”。如果你玩当时流行的游戏,你会发现它们很多都只支持这种声卡。其他声卡公司要想赢得一点点声卡市场份额,就必须提供“声霸”模拟器。有时候,这种模拟器很bug,但是,你要是不买来,你也根本不知道它到底有没有bug。所以,如果你真的想有好的音效,你只能老老实实买“声霸16”。

回到Linux刚刚问世的时候。那时候,PC机大多数都是386,当然,也只需要那一种声卡。可以理解,在早期Linux中,“声霸”的支持那是相当的完美阿!不仅如此,因为Linux的API很好很强大,而且大多数的Linux声音软件都对应这一种API,所以,其他声卡的厂商都用同样的API提供Linux驱动程序。从此,这种Linux系统的“声霸”API被称为“Linux soundAPI”,各个厂商的声卡驱动器都被整合在了同一个软件包中,这个包后来称为“Open Sound System”,简称OSS。

因为OSS是面向Unix的,而且很好很强大,其他UNIX操作系统那时也有了声音支持的需求,OSS因此也被移植到这些系统上了。今天,OSS在几乎所有的UNIX系统上都能运行(不包括Mac OS X,但包括Linux, *BSD, Solaris以及AIX和HP-UX等等)

现在,站在开发人员的立场上,如果你想编一个简单的程序,包括声音支持,而你又想让你的程序在各种UNIX系统上都能跑,你的选择很简单:只要用OSS编写代码就行了,一切都很好,很容易移植。

但是,OSS有一个致命的缺点,即:混音。比如,你想同时听音乐和新闻,新闻音量很大而音乐很安静,那么调整音量就很费劲了。但是,当OSS最初被设计的时候,它就把“混音”的工作转移给了声卡,当时的声卡允许多路输出混合。但是,更多新的声卡决定跟随modem的脚步:把混音工作交给软件。从此以后,声卡的各个功能都必须用软件来书写,而“混音”常常被忽视,而它实际上也不总是那么简单。从此,很多新的声卡在OSS下都具备混音功能。

因此,出现了两个新的声音系统:aRts和ESD,分别被KDE和Enlightenment/GNOME广泛应用。它们具有新的API,在声音被送往OSS之前就完成了混音。它们都期待新的程序使用它们的API。现在,我调研了aRts和ESD两者:aRts看上去很容易使用,甚至比OSS还要易用,我用5分钟就用aRts书写了一个声音播放器。ESD看上去比aRts提供更多的功能,但是比aRts复杂的多。如果你用上述两者之一写了程序,你可以运行多个实例,每个实例都用它们之一播放声音,你能听到所有的声音。

aRts和ESD的问题在于,它们是两个,所以,虽然大多数KDE和GTK程序都能使用其中一个或另一个,但是你不能同时使用它们两个。你只能一次使用一个,这是因为OSS的混音问题。甚至,它们之一也不能与另一个直接使用OSS的程序一起工作。为了解决这个问题,库封装器出现了。

第一个是Simple DirectMedia Layer (SDL),它封装了OSS,aRts,ESD甚至Windows和Mac OSX的声音系统。因为它在哪里都能工作,在UNIX上可以使用任何一种声音系统,听起来好像很容易移植。不幸的是,SDL只提供一种回调式的接口。虽然这对很多程序来说都不错,但是有时候会惹麻烦,比如在一些可以即时生成声音的程序中,不仅很难正确地编程,甚至会使得一些声音根本无法同步。

另一个是libao也封装了OSS,aRts,ESD和一些UNIX特有的声音引擎。libao的API也很易用,和aRts很像,我不用花什么事件就弄出了一个libao的程序。不幸的是,libao好久没有更新了。而且他的一些封装器也很buggy。libao只支持阻塞音频,也对即时生成声音的程序造成了麻烦,强迫程序使用线程,希望其中一个运行的时候另一个不陷入长眠,还要用信号量和互斥锁。

因为libao总的概念是好的,所以MPlayer团队使用了libao,消除了很多局限性,更新了,修正了bug,增加了更多的声音封装器,甚至包括两个Windows的和一个Mac OSX的,把新的库称为libao2。用MPlayer,你可以选择声音封装器,只要用-ao命令行选项就可以选择使用哪个系统了,也可以通过配置文件,永久地设置。我甚至拿了一些libao2DirectSound的代码,用于一个Windows应用程序,一个我认识的微软程序员看了看这个代码,说这个代码很好很强大,它展示了其封装器代码的质量。不幸的是,libao2与MPlayer结合紧密,里面包括各种各样的MPlayer库的调用,这对于将其应用于其他程序而言,是一个很大的不利因素。也许一些MPlayer或者库的开发人员可以聚在一起,把它们两者分开,把特需的特性作为插件,这样我们应用程序开发人员可以真正拥有一款可移植的声音库了。这也可能让MPlayer在更加古怪的驱动程序上播放声音,因为一个完全不懂MPlayer的开发人员也可以来修正这个bug。

当程序员们正在为他们宝贵的程序库而担忧的时候,OSS的开发人员们决定走闭源路线,为付费用户提供一些额外的功能(一般用户很少会用到)。这引起一片哗然,一些Linux开发人员决定创造另一个解决方案。

但是,他们既没有从核心开始重写OSS,也没有使用封装器(当然,用封装器解决不了混音问题),也没有基于已有的开源的OSS继续写,而是因为一些荒诞的理由,决定重写一个完全不同的API。这被称为Advanced Linux SoundArchitecture,简称ALSA。这个新的API很肥硕,而且基本没有文档,难以置信地复杂,与OSS完全不同。这就意味着,现有的驱动程序不得不为ALSA完全重新编写,甚至应用程序也要重写(或者让一些封装器,比如SDL和libao,支持ALSA)。

ALSA的优势是它具有一个软件混音器(但不总是能工作)。但是,显而易见,应用程序不会一夜之间全都转而使用ALSA。而且,ALSA不可移植,那些要支持BSD和Solaris的人还是要用OSS。也就是说,想要支持ALSA,就必须同时支持OSS和ALSA。因为OSS也能在Linux上用,我们为什么还要这么做呢?意识到这个,ALSA的开发人员在ALSA中加入了OSS模拟器。因此,明智的程序员只需要为OSS编程。但是,一个让人尴尬的地方是,使用ALSA的OSS模拟器比直接使用ALSA效果更好。很多SDL和libao的用户都说,用ALSA的OSS模拟器的话,声音的裂缝比起直接用ALSA来说更少。

但是,由于某些很傻的原因,ALSA的OSS模拟器不支持混音,结果直接使得ALSA的优势荡然无存。我这里有两块同时支持ALSA和OSS的声卡,结果,在OSS下工作的更好。更让我震惊的是,我发现声卡中具备硬件混音器,它并没有被ALSA的OSS模拟器利用,这就使得用ALSA的OSS程序根本没有混音。但是,因为某些原因,我却听到了大量的宣传,说我不得不把我的所有程序改成ALSA的,因为OSS被废弃了(英语叫deprecated)。我真的怀疑到底是不是因为ALSA的存在而使得FreeBSD突然废弃了OSS。一个可移植的框架,怎么可能因为一个鼠目寸光的操作系统不知道今后怎么做,而被废止呢?使用不可移植的API才是真正应该被废止的!我很聪明地把所有支持ALSA的宣传重定向到/dev/null中,但是,真正让我恼火的是,我的笔记本的内置声卡只支持ALSA。对于这些宣传,我更郁闷的是,如果ALSA更加得寸进尺怎么办?现在很多Linux发行版自带了SDL却没有OSS绑定,这已经很烦了,还能更糟糕一点吗?

现在,我的一个朋友最近决定改用Linux,因为他意识到了Linux在很多对他重要的方面都比Windows好的多。因为他的笔记本很新,它唯一能找到的驱动是ALSA的。现在,因为这个原因,他的笔记本声音一直很糟糕。音量控制很烂,强迫它回到Windows,那样能让声音正常一些。

这周初,另一个朋友告诉我,闭源的OSS自从上一个开源的OSS被加入Linux内核一来,一直在被更新,现在可以免费从它的网站上下载,安装,而且比ALSA更好。它甚至提供ALSA模拟器。这听起来很吸引人。我登录OSS的网站,把OSS安装到我朋友的笔记本上看看能不能解决问题。现在,你看,你听,声音清澈,音量控制细致得多,音量还能调到比ALSA更大[这真的是feature吗?不是bug?不会失真?译者注]我开始研究闭源的OSS,发现,它有一个软件混合器,甚至软件重采样都工作得很好。我然后把它安装到我自己的笔记本上,声音好多了。因为我目前只有一个程序是仅支持ALSA的,我不太在意,但是让我郁闷的是,这个程序的上一个版本还支持OSS呢。

一切都说明,ALSA就是个垃圾,从根上就是个坏主意。如果你想在Linux下得到好的声音支持,最好的,有时候也是唯一的选择,就是用闭源的OSS。用它,你总能够获得混音支持(甚至可以用ALSA做不了的硬件混音),支持很多UNIX操作系统,还有良好的音量控制。它还增加了一些改进的API,使得程序员可以做更多的高级音频功能。它还支持spdif,甚至你可以直接把AC3音频格式直接送入OSS,而不必先解码。

真正的问题是,它是闭源的,这使得很多人望而却步。但是,就像最好的Linux视频驱动是nVidia的一样,最好的音频驱动是闭源的,也不是什么奇怪的事情。因为我永远要在我的笔记本上得到最好的应用,而不是在乎什么奇怪的意见,只要闭源驱动工作得好,我就会使用闭源驱动。但是,对于我的发行版的打包着来所,它们就不得不避讳闭源软件了。虽然,对于nVidia和ATI的情况而言,程序员用OpenGL或X11写的程序也能被闭源驱动支持,程序员因此不必太在意用哪个来写。但是,对于OSS来说,如果发行版使用了完全不兼容的接口,而且提供SDL而不提供OSS,我们就不能使用闭源驱动了,我们的自由也没了。我们也就不得不使用垃圾,牺牲了好的可移植性。

我们必须鉴定我们的立场,把ALSA的宣传掐死在摇篮里,然后无条件地开发大量的不支持ALSA的应用程序,然后告诉ALSA的卫道士们让他们觉悟。我们必须让发行版们继续支持OSS,如果它们真的想要一个新的声音系统,它应该使用和旧系统相同的API,使用和其他UNIX一样的本地声音系统。

现在,我希望OSS再次开源,也许我们应该直接与4Front公司讨论,或者我们重新创造一个和闭源驱动具备同等质量的新的声音系统。但是,无论如何,我们都不能把我们所有的精力都浪费在那个破烂、不兼容、不可用的垃圾上了。

http://4front-tech.com/hannublog/?p=5
看看这个链接,你就更了解最新的闭源OSS了,也就了解为什么我们应该用它了。

综上,如果你遇到声音问题,你应该试试官方的OSS,让大家知道,OSS才是王道,ALSA是垃圾。也记得告诉你的发行版,ALSA是垃圾,它们不应该移除OSS。如果我们愿意,我们就应该有使用闭源驱动的自由。告诉应用程序的开发着说你需要OSS支持,告诉那些想要ALSA的你的应用程序的用户说ALSA是垃圾,并告诉它们,如果遇到问题,请用闭源OSS驱动。我们也应该考虑与4Front交涉,让它们重新将OSS开源,或者更新原有的开源OSS实现(或者干脆重写)。我们也应该吧libao2弄得更像一个库。如果我们采取这些措施,我认为,Linux的声音的形势,以及其他UNIX操作系统的形势,都会好得多。如果我们采取了以上必要的措施,UNIX声音程序就不再会沦为声音程序社区的笑柄。

Linux音频系列之二:不是那么可悲了

现在,Linux的声音现状不是那么可悲了这篇可以说是上一篇文章的对应。
这里还有一些问题值得思考:
* 延迟究竟是什么引起的?真的是封装的层次越多,延迟就越大吗?看看这篇:
* 最好看看原文后面的评论,也指出了原文的一些问题。
http://0pointer.de/blog/projects/pulse-glitch-free.html原标题:State of sound in Linux not so sorry after all.
原文:http://insanecoding.blogspot.com/2009/06/state-of-sound-in-linux-not-so-sorry.html
原作者:INSANE CODING大约两年前,我写了一篇文章,题为“Linux声音系统的可悲境地”(链接:http://insanecoding.blogspot.com/2007/05/sorry-state-of-sound-in-linux.html ) ,希望通过它让一些Linux下的声音问题修正。现在,两年过去了,很多东西都改变了,今天终于到了回顾Linux声音的现状的时间了。上一篇文章的总结(怕你没看过):
* Linux下的声音系统的历史很有意思,以前,硬件缺乏的“混音”功能大多数由软件来补充。
* 人们创造了很多“声音服务器”以便解决“混音”问题。
* 人们创造了很多“库”来解决后端太多的问题。
* 在Linux内核源代码中,ALSA替代了OSS v3,试图解决一些已有的问题。
* 闭源的OSS很好很强大。
* Linux发行版渐渐开始移除基于OSS的程序,以更好地向ALSA转移
* 一般的声音软件开发人员更喜欢简单的API。
* 可移植性是好东西
* 在某些情景下,用户会遭遇问题。

现在,很多东西都变了。比如:
* OSS再次开源和自由了。
* PulseAudio广为流传。
* 现有的库改进了。
* 新的Linux发行版发布了,有些已有的发行版试图重新设计它的整个声音软件栈,以改进用户体验。
* 人们读了我的上一篇文章,开始有了更多的认识,在某些方面,比以前更懂得开张圣听,察纳雅言。
* 我个人也更加深入地研究了这些问题,以便提供更详细的信息。

让我们拉近距离,观察一下OSS和ALSA现在的优势与不足。不是五年以前,不是去年,不是上个月,而是今天的它们。

首先,ALSA。
ALSA有三个组件组成。第一部分是内核中的驱动程序。提供了API,以便另外两部分与之通信。第二部分是给声音开发者的API,这允许开发人员创造与ALSA通信的程序。第三部分是混音组件,它可以放在另外两个组件之间,让多个使用ALSA API的程序同时输出声音。

为了便于理解,这里给出一幅图:

注意,图是我自己画的。我的艺术细胞很烂,我从来没有获过什么艺术奖。而且,这图也不是100%绝对准确的,但是已经足以让普通用户来了解其背后的大意了。

开发人员,想要在程序中输出声音,可以用以下任意一种方法:
* 通过ALSA API,直接输出到ALSA的内核API(当混音关闭时)
* 通过ALSA API,输出到“混音器”,混音器再输出到内核API(当混音关闭时)
* 用OSS v3的API直接输出到ALSA的内核API。
* 使用封装过的API,它们再利用上述3中方法输出。

可以看出,ALSA很灵活,具备OSSv3所不具备的混音功能,但是仍然为旧程序提供遗留的OSSv3支持。也允许关闭混音,以免某些情况下混音会造成声音质量下降,或者在某些时候引入用户所不希望的延迟。

两点很明确:ALSA提供可选的内核外的混音器,但ALSA的OSS遗留API不具备混音器。

其劣势也很明显,ALSA当初设计的目的是为了在比传统的“声音服务器”更低的层次,以较直接的方法,解决混音问题,

ALSA的明显优势是自由、开源、有混音器、可以支持多块声卡(这些OSSv3都不具备)。而且ALSA包含在内核中,而且迎合了新老程序的需求。

ALSA另一个不太明显的劣势是,它只支持Linux、FreeBSD、Solaris、Mac OS X和Windows上都没有。同样,一般的开发人员也觉得ALSA的本地API太难用了,但这值得商榷。

现在,看看今天的OSS。OSS现在的最新版是4,它与OSSv3完全不同。
与OSSv3的闭源不同,OSSv4是开源的,以GPL、3条款BSD和CDDL协议发布。
十年前,OSS在内核之内;新的OSSv4被踢出内核以外。因此,普通用户难以尝试之。旧的OSSv3缺乏混音,不支持多声卡,OSSv4不再是这样了。很多讨论OSS,测试OSS,把OSS与ALSA比较的人,很不幸,用的是十年前的旧的OSS,因此,它们得出的结论都不是今天的现实。

这里有一幅OSSv4的图:

想要开发基于OSSv4的程序的开发人员应该按照以下某种方法做:
* 通过OSS API直接输出到内核,有混音
* 通过ALSA API输出到OSS API,有混音
* 通过封装库的API,间接利用上述某种方法

不像ALSA,当你用OSSv4的时候,最终用户总是能够得到混音。而且由于混音在内核内部,它不会引起像ALSA那么严重的延迟。

OSSv4提供了它自己的ALSA模拟层,但它非常糟糕,我还没有发现一个ALSA程序能够在OSS的ALSA模拟层上正常地跑。但是,这不是个大问题,正如我刚才所说,ALSA自身的声音开发API就可以输出到OSS,这就达到了ALSA与OSS的兼容。你可以在这个链接中获得更多信息:http://insanecoding.blogspot.com/2009/05/perfect-sound-with-oss-version-4.html

ALSA自身的库也能做到这一点。它的结构如下:

如你所见,它既能输出到OSS后端,又能输出到ALSA内核后端(下文还会继续讨论其他后端)。

由于基于OSS和ALSA程序都可以使用OSS或者ALSA内核后端,它们两者的差别也就很微小了(注意:我这里说的不是OSSv3),据我的研究和测试,它们的差别并不明显。

OSS总是提供混音;ALSA不是。
OSS的混音质量高于ALSA,因为OSS使用了更加精准的混音算法。
OSS的延迟小于ALSA,因为一切都在内核中运行。[译注:有人不同意这一点,认为延迟和缓冲区大小有关,而不是和程序运行于内核态、用户态有关。见本文开头的链接]
ALSA允许操作系统进入待机然后回复,然后原本正在播放的声音继续播放;OSS则不可以,要求应用程序重新开始播放。
OSS对于某些声卡来说是必须的,因为ALSA要么没有相应的驱动程序,要么质量很烂。
ALSA对于某些声卡来说是必须的,因为OSS要么没有相应的驱动程序,要么质量很烂。
ALSA被包含进Linux内核,很容易得到;OSS(v4)则没有。

现在,问题是,普通用户属于上述的哪一类呢?如果用户的声卡只能在其中一种系统上工作,很显然,他们就应该使用那个能够正常工作的。当然,用户也可以两者都试试,看看哪个工作得比另一个更好。

如果用户真的需要让他的程序在Linux待机、回复之后还能正常播放声音,那么,ALSA是(目前)唯一的选择。我个人不觉得这是个问题,我更怀疑到底有多少Linux用户使用“待机”功能。Linux的待机功能很不怎么样,因为总有一些乱七八糟的硬件,比如网卡和显卡,把事情弄得更糟糕。

如果用户不想惹麻烦,ALSA是很明显的选择,因为它就包含在Linux内核里面。所以,用户用最新的ALSA比用最新的OSS更简单。但是,处理这些情况,应该是Linux发行版的工作。对于最终用户,切换ALSA和OSS应该是无缝而透明的。之后继续讨论该话题。

我们还发现,涉及到混音时,如果要选择一个混音质量和延迟都更好的选项,那么只要上述种种问题仍然存在,OSS都是更好的选择。但是,混音质量仅仅在音量更大的情况下变得明显,或者一些极个别情况下。而延迟问题仅仅在玩大型游戏的时候才明显,听音乐,看电影的时候都不是问题。

等等,以上都是后端的问题。应用程序开发人员的API问题怎么样?

很多人都喜欢对各个API指指点点(我也喜欢,尤其是在我的上一篇文章中)。但是它们真的没有抓住本质。首先,下图是一般的声音封装API的结构图:

应用程序利用封装器——如OpenAL、SDL或libao——进行输出,然后,声音传到高层或低层的后端,而用户不用关心这一细节。

由于后端因操作系统声音接口而异,封装器允许用户编写一个程序,在Windows、Mac OS X、Linux上都能运行,而且更简单。

Adobe的人说这是一种问题(见 http://blogs.adobe.com/penguin.swf/2007/05/welcome_to_the_jungle.html ),使得在Linux下无法输出声音。没有比这种言论更远离事实的了。这种图(见以上连接)很误导人。OpenAL、SDL、libao、GStreamer、NAS、Allegro很多也都在Windows下出现,但我却从来没听说有人抱怨。

我也可以画一个类似的Windows下的图:

上图也是不完整的。还有XAudio和其他封装器,还有一些Windows特有的封装器,我只是忘了名字而已。

这也完全没有给任何人惹来麻烦,也不应该是个问题。

对于使用而言,库有如下几种:
OpenAL – 强大,难用,擅长“3D音频”。我参考了一些示例,只用了一两个小时,就做了很多事情,给我的程序加上了声音。
SDL – 简单,使用回调API,如果这种风格恰好和你的程序的风格是一致的,那么这是个不错的选择。我个人用了半小时就能给我的程序加入声音,但我不觉得这满足所有人的需求。
libao – 非常简单,难以置信地容易使用,但是,如果你的程序是非阻塞的,这就是个问题了。我只用了几分钟,就给我的程序加入了很多音响效果。我只是觉得,如果你的程序需要给声音单独创造一个线程,这有时候就有点烦了。同样,看你的需求。

我还没有尝试其他封装器,所以我不能评论它们,但是,同样的理念在每个里面都有体现。

当然,在Linux平台下有真正的OSS和ALSAAPI。那么既然还有更可移植、更易用的封装库,为什么还会有人去直接使用OSS和ALSA呢?这一般都是对的,没有理由直接使用OSS和ALSAAPI来输出声音。某些情况下,使用封装API会引起额外延迟,你可能不期望。或者你并不需要封装的API提供的更多高级功能。

下面是对OSS和ALSA API的总结:
OSSv3 – 易用,很多开发者都喜欢它,存在于各种UNIX,除了Mac OS X以外。我只用了10分钟就给我的程序加入了声音。
OSSv4 – 基本上和v3兼容,甚至更易用,存在于除了Mac OS X以外的任何UNIX上,在Linux上使用ALSA后端,具备重新采样功能,还有内置AC3解码器,我做了几个声音程序,每个也只要10分钟。
ALSA -难以使用,我所问过的大多数程序员都不喜欢它,文档烂,除了Linux以外哪里都没有它。但是有些程序员喜欢它,他们觉得ALSA提供了比OSSAPI更高的灵活性。我个人花了3个小时才找到文档的头绪,并把声音加到程序中。然后,我发现声音只在我开发用的机器上可以工作,我又花了一个小时,看文档,改代码,才让程序在两台机器上都能工作。最后,我发布了我的程序,又发现一些人抱怨我的程序在他们的机器上没有声音,又收到了几个别的开发人员发给我的补丁。每个补丁都能让我的程序在他的机器上工作,但又让程序在我的某台机器无法工作。现在,一年已经过去了,我们的程序,在浪费了好多程序员好多个小时之后,终于可以在所有的机器上工作了。不过,我真的不信任它。我们作为开发人员,不应该忙于这种问题。当然,你可以反对我,甚至引用例子说你是怎么找到文档,快速实现声音,然后在所有的机器上经所有人测试都能够无瑕地工作。我也许只是太笨了。

我本以为OSS与ALSA之争是最终用户的事,也就是它们被迫使用某种框架。现在我知道,这与开发人员也息息相关。现在,主要问题很困难:如果我要利用OSSv4所提供的所有的额外特性,我就必须使用OSS后端。而用户则根本不关心到底用哪个,除非它们使用能够利用这些特性的某些程序。

对于封装的API,我也用几个程序得到了一些有意思的结果:
App -> libao -> OSS API -> OSS Back-end – Good sound, low latency.
App -> libao -> OSS API -> ALSA Back-end – Good sound, minor latency.
App -> libao -> ALSA API -> OSS Back-end – Good sound, low latency.
App -> libao -> ALSA API -> ALSA Back-end – Bad sound, horrible latency.
App -> SDL -> OSS API -> OSS Back-end – Good sound, really low latency.
App -> SDL -> OSS API -> ALSA Back-end – Good sound, minor latency.
App -> SDL -> ALSA API -> OSS Back-end – Good sound, low latency.
App -> SDL -> ALSA API -> ALSA Back-end – Good sound, minor latency.
App -> OpenAL -> OSS API -> OSS Back-end – Great sound, really low latency.
App -> OpenAL -> OSS API -> ALSA Back-end – Adequate sound, bad latency.
App -> OpenAL -> ALSA API -> OSS Back-end – Bad sound, bad latency.
App -> OpenAL -> ALSA API -> ALSA Back-end – Adequate sound, bad latency.
App -> OSS API -> OSS Back-end – Great sound, really low latency.
App -> OSS API -> ALSA Back-end – Good sound, minor latency.
App -> ALSA API -> OSS Back-end – Great sound, low latency.
App -> ALSA API -> ALSA Back-end – Good sound, bad latency.

如果你觉得看懂上表很难,这里是总结:
* OSS后端总是产生高质量的声音,除了通过OpenAL->ALSA输出到OSS以外。
* ALSA使用OSS API,声音质量一般更好,而且延迟也更低(一般是因为它避免了所有的混音)
* 要获得更好的声音,用OSS相关技术都更好。

等等。“声音服务器”应该处于什么位置呢?

“声音服务器”最初创造的时候是为了解决OSSv3的混音问题。当代的声音服务器栈看上去是这样:

很显然,这些“声音服务器”什么也不干,纯粹增加延迟,应该完全扔掉。KDE4不再使用aRts声音服务器了,而是使用一个封装API,称为Phonon,它可以处理很多种后端(而其中某些后端也可以输出到声音服务器)。

但是,如上所述,ALSA的混音质量不如OSS高。而且ALSA缺乏一些很好的功能,比如可以为每个不同的应用程序设置对应的音量控制。

现在,你可以关掉ALSA低质量的混音器,而是让应用程序在输出声音之前,在内部通过改变声波,完成音量控制,但是这对开发人员来说不是很友好。

为此,Fedora和Ubuntu都引入了一个所谓的“最先进的”声音服务器,称为PulseAudio。

如果你还记得这幅图:

可以看出,ALSA的API也可以输出到PulseAudio。这就意味着,用ALSA的API写的程序可以输出到PulseAudio,并可以无缝地使用PulseAudio的高质量混音器,而不用修改原有程序。PulseAudio也可以通过网络把声音发送到另一个远端的PulseAudio服务器上。PulseAudio的栈看上去是这样:

可以看出,它非常复杂。而100%精确描述PulseAudio的细节的图将更复杂。

由于PulseAudio是如此的先进,绝大多数的封装API都能输出到PulseAudio。Fedora和Ubuntu也预装且为最终用户将其配置好了。在某些情况下,它也可以接受为其他声音服务器书写的声音,如ESD,而不必在其上运行真正的ESD。这也就意味着,现在的很多程序发出的声音,在到达声卡之前,要经过很多很多层次。

有人认为,PulseAudio是我们新的救世主,为任何API书写的声音都可以输出到它上面,而且混音质量很好很强大。

例外的是,很多游戏玩家都抱怨PulseAudio增加了“奇大无比”的延迟,对于一些高端游戏来说,非常明显。用户不愿意在看到敌人和谐靠大家之后足足3秒钟才听到和谐靠大家声。别听别人胡扯,声音服务器根本没用,尤其是在这么肥硕和复杂的结构下还想为游戏提供可以接受的低延迟。

与PulseAudio的龌龊相比,看看这个:

请你考虑考虑混音、逐程序音量控制、应用程序兼容性和其他特性,你觉得哪个声音栈更好?

没错,不要忘记应用程序。经常有人告诉我说它们的程序是为某个特定的API写的,因此他们要么用ALSA后端,要么用OSS后端。但是,正如我上文所述,任何一个API都可以输出到另一个后端上去,而它本身可以什么也不做。如果设置正确(见 http://insanecoding.blogspot.com/2009/05/perfect-sound-with-oss-version-4.html ),你就算用新版的OSS播放Flash,也不会听不到声音。

那么,我们现在的情况是怎么样的呢?
最大的问题就是,我发现发行版根本就不考虑要不要让用户的选择更容易。Debian和基于它的发行版提供一个“Linux sound base”包,允许用户选择使用OSS后端还是ALSA后端,而它本身什么也不做。以下是这样的软件包应该提供的功能:
* 当用户选择OSS时,它应该安装最新的OSS包,也要安装ALSA的“ALSA API -> OSS后端”的接口,并自动设置正确。
* 尽量少地配置一个安装好的OpenAL库,让它使用OSS后端,最好SDL、libao和其他封装库也这样配置
* 安装新应用程序或者封装库的时候,根据以上设置,配置它们,让它们也用OSS。
* 如果用户选择ALSA,则类似地配置,只不过反过来。

这样的设置将允许用户很容易地选择这两个后端。如果用户的声卡在发行版默认配置的后端上无法工作,这会很有用。如果用户关心,这也将允许用户客观地测试哪个后端更好,而使用他们认为好的那一个。用户应该获得这样的权利。我个人认为OSS更好,但是,如果用户不喜欢默认的后端,我们应该允许用户选择。

我现在不断听到有人说:“但是,但是,OSS已经被踢出内核了,它也许再也不会被合并进来。”

让我们客观地分析这个问题。它有没有被包含进内核,真的很重要吗?就算KVM是内核的一部分,而VirtualBox不是,我们不是照样可以使用VirtualBox吗?就算KDE和GNOME都不是内核的一部分,我们不也可以使用它们吗?

最后,真正重要的是发行版的支持,而不是到底谁是内置的。谁关心内置不内置呢?唯一的区别在于,内核的开发人员不会去关心不包含在内核里的东西。但是这正是各大发行版正在做的工作,保证内核模块和相关的软件包在每个新的内核发布之后可以正常使用。

不管怎么样,总结几点:

我相信OSS比ALSA好,你倒是不见得同意。最好OSS和ALSA能够共享它们的驱动程序,而不是只支持某些声卡而不是另一些。

OSS应该支持系统挂起,以及其他比不过ALSA的特性,即使这些特性微不足道。这里有个建议:为什么Ubuntu雇用OSS的作者,并让它把最终用户最近遇到的一些问题弄得更友好一些呢?他现在正在找工作呢。(见 http://4front-tech.com/hannublog/?p=23 )然后再指派一些人改进现有的音量控制,让它对新的OSSv4更加友好,或者让HAL之类的东西默认就认识OSSv4。

问题应该直截了当地解决,而不是想PulseAudio那样绕弯,这个垃圾真该扔掉。如果用户真的需要远程音频,他应该很容易地把/dev/dsp映射到NFS文件系统中,然后用这种方法输出到OSS。网络透明性应该在文件系统级别完成,这就是UNIX的设计理念(即:一切都是文件),而不是让那些非UNIX的技巧在今天占领声音领域。

发行版们真的应该联合起来。虽然最近Draco Linux(见 http://www.dracolinux.org/ )出现了,且只包含OSS的发行版;而Arch Linux( http://www.archlinux.org/ )似乎把OSSv4视为头等公民,展示给用户,给用户选择,但是我已经告诉了它们,它们的ALSA兼容性部门没有给最终用户设置正确,这不好。而Arch Linux要求用户修改每个应用程序/库的配置文件。

由于OSS的操作系统抽象层,它的可移植性很好,与整个UNIX世界更加相关,而不像ALSA。FreeBSD则是采取自己的措施避免了OSS的抽象层,但还是基本上兼容的。如果愿意,用户可以在FreeBSD上安装官方版的OSSv4。

最后,Linux上的声音真的不需要那么可悲。发行版只要联合起来,阻止那些蔓延的指手画脚、宣传和“惧惑疑”论调,这些论调不是离题万里,就是根本错误。让我们不要被Adobe公司、PulseAudio宣传机器以及其他人或组织冲昏了头脑。让我们客观一点,让我们应用最好的解决方案,不要以庸人自居,也不要以五十步笑百步。

页面缓存——内存与文件的那些事儿

页面缓存——内存与文件的那些事儿

原文标题:Page Cache, the Affair Between Memory and Files
原文地址:http://duartes.org/gustavo/blog/

上次我们考察了内核如何为一个用户进程管理虚拟内存,但是没有涉及文件及I/O。这次我们的讨论将涵盖非常重要且常被误解的文件与内存间关系的问题,以及它对系统性能的影响。

提到文件,操作系统必须解决两个重要的问题。首先是硬盘驱动器的存取速度缓慢得令人头疼(相对于内存而言),尤其是磁盘的寻道性能。第二个是要满足‘一次性加载文件内容到物理内存并在程序间共享’的需求。如果你使用进程浏览器翻看Windows进程,就会发现大约15MB的共享DLL被加载进了每一个进程。我目前的Windows系统就运行了100个进程,如果没有共享机制,那将消耗大约1.5GB的物理内存仅仅用于存放公用DLL。这可不怎么好。同样的,几乎所有的Linux程序都需要ld.so和libc,以及其它的公用函数库。

令人愉快的是,这两个问题可以被一石二鸟的解决:页面缓存(page cache),内核用它保存与页面同等大小的文件数据块。为了展示页面缓存,我需要祭出一个名叫render的Linux程序,它会打开一个scene.dat文件,每次读取其中的512字节,并将这些内容保存到一个建立在堆上的内存块中。首次的读取是这样的:

在读取了12KB以后,render的堆以及相关的页帧情况如下:

这看起来很简单,但还有很多事情会发生。首先,即使这个程序只调用了常规的read函数,此时也会有三个 4KB的页帧存储在页面缓存当中,它们持有scene.dat的一部分数据。尽管有时这令人惊讶,但的确所有的常规文件I/O都是通过页面缓存来进行的。在x86 Linux里,内核将文件看作是4KB大小的数据块的序列。即使你只从文件读取一个字节,包含此字节的整个4KB数据块都会被读取,并放入到页面缓存当中。这样做是有道理的,因为磁盘的持续性数据吞吐量很不错,而且一般说来,程序对于文件中某区域的读取都不止几个字节。页面缓存知道每一个4KB数据块在文件中的对应位置,如上图所示的#0, #1等等。与Linux的页面缓存类似,Windows使用256KB的views。

不幸的是,在一个普通的文件读取操作中,内核必须复制页面缓存的内容到一个用户缓冲区中,这不仅消耗CPU时间,伤害了CPU cache的性能,还因为存储了重复信息而浪费物理内存。如上面每张图所示,scene.dat的内容被保存了两遍,而且程序的每个实例都会保存一份。至此,我们缓和了磁盘延迟的问题,但却在其余的每个问题上惨败。内存映射文件(memory-mapped files)将引领我们走出混乱:

当你使用文件映射的时候,内核将你的程序的虚拟内存页直接映射到页面缓存上。这将导致一个显著的性能提升:《Windows系统编程》指出常规的文件读取操作运行时性能改善30%以上;《Unix环境高级编程》指出类似的情况也发生在Linux和Solaris系统上。你还可能因此而节省下大量的物理内存,这依赖于你的程序的具体情况。

和以前一样,提到性能,实际测量才是王道,但是内存映射的确值得被程序员们放入工具箱。相关的API也很漂亮,它提供了像访问内存中的字节一样的方式来访问一个文件,不需要你多操心,也不牺牲代码的可读性。回忆一下地址空间、还有那个在Unix类系统上关于mmap的实验,Windows下的CreateFileMapping及其在高级语言中的各种可用封装。当你映射一个文件时,它的内容并不是立刻就被全部放入内存的,而是依赖页故障(page fault)按需读取。在获取了一个包含所需的文件数据的页帧后,对应的故障处理函数会将你的虚拟内存页映射到页面缓存上。如果所需内容不在缓存当中,此过程还将包含磁盘I/O操作。

现在给你出一个流行的测试题。想象一下,在最后一个render程序的实例退出之时,那些保存了scene.dat的页面缓存会被立刻清理吗?人们通常会这样认为,但这是个坏主意。如果你仔细想想,我们经常会在一个程序中创建一个文件,退出,紧接着在第二个程序中使用这个文件。页面缓存必须能处理此类情况。如果你再多想想,内核何必总是要舍弃页面缓存中的内容呢?记住,磁盘比RAM慢5个数量级,因此一个页面缓存的命中(hit)就意味着巨大的胜利。只要还有足够的空闲物理内存,缓存就应该尽可能保持满状态。所以它与特定的进程并不相关,而是一个系统级的资源。如果你一周前运行过render,而此时scene.dat还在缓存当中,那真令人高兴。这就是为什么内核缓存的大小会稳步增加,直到缓存上限。这并非因为操作系统是破烂货,吞噬你的RAM,事实上这是种好的行为,反而释放物理内存才是一种浪费。缓存要利用得越充分越好。

由于使用了页面缓存体系结构,当一个程序调用write()时,相关的字节被简单的复制到页面缓存中,并且将页面标记为脏的(dirty)。磁盘I/O一般不会立刻发生,因此你的程序的执行不会被打断去等待磁盘设备。这样做的缺点是,如果此时计算机死机,那么你写入的数据将不会被记录下来。因此重要的文件,比如数据库事务记录必须被fsync() (但是还要小心磁盘控制器的缓存)。另一方面,读取操作一般会打断你的程序直到准备好所需的数据。内核通常采用积极加载(eager loading)的方式来缓解这个问题。以提前读取(read ahead)为例,内核会预先加载一些页到页面缓存,并期待你的读取操作。通过提示系统即将对文件进行的是顺序还是随机读取操作(参看madvise(), readahead(), Windows缓存提示),你可以帮助内核调整它的积极加载行为。Linux的确会对内存映射文件进行预取,但我不太确定Windows是否也如此。最后需要一提的是,你还可以通过在Linux中使用O_DIRECT或在Windows中使用NO_BUFFERING来绕过页面缓存,有些数据库软件就是这么做的。

一个文件映射可以是私有的(private)或共享的(shared)。这里的区别只有在更改(update)内存中的内容时才会显现出来:在私有映射中,更改并不会被提交到磁盘或对其他进程可见,而这在共享的映射中就会发生。内核使用写时拷贝(copy on write)技术,通过页表项(page table entries),实现私有映射。在下面的例子中,render和另一个叫render3d的程序(我是不是很有创意?)同时私有映射了scene.dat。随后render改写了映射到此文件的虚拟内存区域:

上图所示的只读的页表项并不意味着映射是只读的,它们只是内核耍的小把戏,用于共享物理内存直到可能的最后一刻。你会发现‘私有’一词是多么的不恰当,你只需记住它只在数据发生更改时起作用。此设计所带来的一个结果就是,一个以私有方式映射文件的虚拟内存页可以观察到其他进程对此文件的改动,只要之前对这个内存页进行的都是读取操作。一旦发生过写时拷贝,就不会再观察到其他进程对此文件的改动了。此行为不是内核提供的,而是在x86系统上就会如此。另外,从API的角度来说,这也是合理的。与此相反,共享映射只是简单的映射到页面缓存,仅此而已。对页面的所有更改操作对其他进程都可见,而且最终会执行磁盘操作。最后,如果此共享映射是只读的,那么页故障将触发段错误(segmentation fault)而不是写时拷贝。

被动态加载的函数库通过文件映射机制放入到你的程序的地址空间中。这里没有任何特别之处,同样是采用私有文件映射,跟提供给你调用的常规API别无二致。下面的例子展示了两个运行中的render程序的一部分地址空间,还有物理内存。它将我们之前看到的概念都联系在了一起。

至此我们完成了内存基础知识的三部曲系列。我希望这个系列对您有用,并在您头脑中建立一个好的操作系统模型。

主板芯片组与内存映射

原文标题:Motherboard Chipsets and the Memory Map

原文地址:http://duartes.org/gustavo/blog/

关于PC物理内存的分布我在各种刊物和教科书上看过无数遍了,但感觉从感性上还比不上作者的这篇文章,所以还是忍不住转载一下。


我打算写一组讲述计算机内幕的文章,旨在揭示现代操作系统内核的工作原理。我希望这些文章能对电脑爱好者和程序员有所帮助,特别是对这类话题感兴趣但没有相关知识的人们。讨论的焦点是Linux,Windows,和Intel处理器。钻研系统内幕是我的一个爱好。我曾经编写过不少内核模式的代码,只是最近一段时间不再写了。这第一篇文章讲述了现代Intel主板的布局,CPU如何访问内存,以及系统的内存映射。

作为开始,让我们看看当今的Intel计算机是如何连接各个组件的吧。下图展示了主板上的主要组件:

现代主板的示意图,北桥和南桥构成了芯片组。

当你看图时,请牢记一个至关重要的事实:CPU一点也不知道它连接了什么东西。CPU仅仅通过一组针脚与外界交互,它并不关心外界到底有什么。可能是一个电脑主板,但也可能是烤面包机,网络路由器,植入脑内的设备,或CPU测试工作台。CPU主要通过3种方式与外界交互:内存地址空间,I/O地址空间,还有中断。

眼下,我们只关心主板和内存。安装在主板上的CPU与外界沟通的门户是前端总线(front-side bus),前端总线把CPU与北桥连接起来。每当CPU需要读写内存时,都会使用这条总线。CPU通过一部分管脚来传输想要读写的物理内存地址,同时另一些管脚用于发送将被写入或接收被读出的数据。一个Intel Core 2 QX6600有33个针脚用于传输物理内存地址(可以表示233个地址位置),64个针脚用于接收/发送数据(所以数据在64位通道中传输,也就是8字节的数据块)。这使得CPU可以控制64GB的物理内存(233个地址乘以8字节),尽管大多数的芯片组只能支持8GB的RAM。

现在到了最难理解的部分。我们可能曾经认为内存指的就是RAM,被各式各样的程序读写着。的确,大部分CPU发出的内存请求都被北桥转送给了RAM管理器,但并非全部如此。物理内存地址还可能被用于主板上各种设备间的通信,这种通信方式叫做内存映射I/O。这类设备包括显卡,大多数的PCI卡(比如扫描仪或SCSI卡),以及BIOS中的flash存储器等。

当北桥接收到一个物理内存访问请求时,它需要决定把这个请求转发到哪里:是发给RAM?抑或是显卡?具体发给谁是由内存地址映射表来决定的。映射表知道每一个物理内存地址区域所对应的设备。绝大部分的地址被映射到了RAM,其余地址由映射表来通知芯片组该由哪个设备来响应此地址的访问请求。这些被映射为设备的内存地址形成了一个经典的空洞,位于PC内存的640KB到1MB之间。当内存地址被保留用于显卡和PCI设备时,就会形成更大的空洞。这就是为什么32位的操作系统无法使用全部的4GB RAM。Linux中,/proc/iomem这个文件简明的列举了这些空洞的地址范围。下图展示了Intel PC低端4GB物理内存地址形成的一个典型的内存映射:

Intel系统中,低端4GB内存地址空间的布局。

实际的地址和范围依赖于特定的主板和电脑中接入的设备,但是对于大多数Core 2系统,情形都跟上图非常接近。所有棕色的区域都被设备地址映射走了。记住,这些在主板总线上使用的都是物理地址。在CPU内部(比如我们正在编写和运行的程序),使用的是逻辑地址,必须先由CPU翻译成物理地址以后,才能发布到总线上去访问内存。

这个把逻辑地址翻译成物理地址的规则比较复杂,而且还依赖于当时CPU的运行模式(实模式,32位保护模式,64位保护模式)。不管采用哪种翻译机制,CPU的运行模式决定了有多少物理内存可以被访问。比如,当CPU工作于32位保护模式时,它只可以寻址4GB物理地址空间(当然,也有个例外叫做物理地址扩展,但暂且忽略这个技术吧)。由于顶部的大约1GB物理地址被映射到了主板上的设备,CPU实际能够使用的也就只有大约3GB的RAM(有时甚至更少,我曾用过一台安装了Vista的电脑,它只有2.4GB可用)。如果CPU工作于实模式,那么它将只能寻址1MB的物理地址空间(这是早期的Intel处理器所支持的唯一模式)。如果CPU工作于64位保护模式,则可以寻址64GB的地址空间(虽然很少有芯片组支持这么大的RAM)。处于64位保护模式时,CPU就有可能访问到RAM空间中被主板上的设备映射走了的区域了(即访问空洞下的RAM)。要达到这种效果,就需要使用比系统中所装载的RAM地址区域更高的地址。这种技术叫做回收(reclaiming),而且还需要芯片组的配合。

这些关于内存的知识将为下一篇文章做好铺垫。下次我们会探讨机器的启动过程:从上电开始,直到boot loader准备跳转执行操作系统内核为止。如果你想更深入的学习这些东西,我强烈推荐Intel手册。虽然我列出的都是第一手资料,但Intel手册写得很好很准确。这是一些资料:

l         《Datasheet for Intel G35 Chipset》描述了一个支持Core 2处理器的有代表性的芯片组。这也是本文的主要信息来源。

l         《Datasheet for Intel Core 2 Quad-Core Q6000 Sequence》是一个处理器数据手册。它记载了处理器上每一个管脚的作用(当你把管脚按功能分组后,其实并不算多)。很棒的资料,虽然对有些位的描述比较含糊。

l         《Intel Software Developer’s Manuals》是杰出的文档。它优美的解释了体系结构的各个部分,一点也不会让人感到含糊不清。第一卷和第三卷A部很值得一读(别被“卷”字吓倒,每卷都不长,而且您可以选择性的阅读)。

l         Pádraig Brady建议我链接到Ulrich Drepper的一篇关于内存的优秀文章。确实是个好东西。我本打算把这个链接放到讨论存储器的文章中的,但此处列出的越多越好啦。