RTC(real time clock)實時時鐘,主要作用是給Linux系統提供時間。RTC因為是電池供電的,所以掉電后時間不丟失。Linux內核把RTC用作“離線”的時間與日期維護器。當Linux內核啟動時,它從RTC中讀取時間與日期,作為基準值。在運行期間內核完全拋開RTC,以軟件的形式維護系統的當前時間與日期,并在需要時將時間回寫RTC芯片。另外如果RTC提供了IRQ中斷并且可以定時,那么RTC還可以作為內核睡眠時喚醒內核的鬧鐘。應用程序可以用RTC提供的周期中斷做一些周期的任務。?linux有兩種rtc驅動的接口,一個是老的接口,專門用在PC機上的。另外一鐘新接口是基于linux設備驅動程序的。這個新的接口創建了一個RTC驅動模型,實現了RTC的大部分基本功能。而底層驅動無須考慮一些功能的實現,只需將自己注冊的RTC核心中,其他工作由RTC核心來完成。下面分析RTC新接口的驅動模型。
一. 驅動模型結構
? ? ? ? 與RTC核心有關的文件有:
? ? ? ? /drivers/rtc/class.c ? ? ? ? ?這個文件向linux設備模型核心注冊了一個類RTC,然后向驅動程序提供了注冊/注銷接口
? ? ? ? /drivers/rtc/rtc-dev.c ? ? ? 這個文件定義了基本的設備文件操作函數,如:open,read等
? ? ? ? /drivers/rtc/interface.c ? ? 顧名思義,這個文件主要提供了用戶程序與RTC驅動的接口函數,用戶程序一般通過ioctl與RTC驅動交互,這里定義了每個ioctl命令需要調用的函數
? ? ? ? /drivers/rtc/rtc-sysfs.c ? ? 與sysfs有關
? ? ? ? /drivers/rtc/rtc-proc.c ? ? ?與proc文件系統有關
? ? ? ? /include/linux/rtc.h ? ? ? ? 定義了與RTC有關的數據結構
RTC驅動模型結構如下圖:
二. 基本數據結構
? 1. struct rtc_device 結構
[cpp]?view plain?copy
struct?rtc_device??
{??
struct?device?dev;??
struct?module?*owner;??
int?id;??
char?name[RTC_DEVICE_NAME_SIZE];??
const?struct?rtc_class_ops?*ops;??
struct?mutex?ops_lock;??
struct?cdev?char_dev;??
unsigned?long?flags;??
unsigned?long?irq_data;??
spinlock_t?irq_lock;??
wait_queue_head_t?irq_queue;??
struct?fasync_struct?*async_queue;??
struct?rtc_task?*irq_task;??
spinlock_t?irq_task_lock;??
int?irq_freq;??
int?max_user_freq;??
#ifdef?CONFIG_RTC_INTF_DEV_UIE_EMUL??
struct?work_struct?uie_task;??
struct?timer_list?uie_timer;??
/*?Those?fields?are?protected?by?rtc->irq_lock?*/??
unsigned?int?oldsecs;??
unsigned?int?uie_irq_active:1;??
unsigned?int?stop_uie_polling:1;??
unsigned?int?uie_task_active:1;??
unsigned?int?uie_timer_active:1;??
#endif??
};??
這個結構是RTC驅動程序的基本數據結構,但是他不像其他核心的基本結構一樣,驅動程序以他為參數調用注冊函數注冊到核心。這個結構是由注冊函數返回給驅動程序的。
? 2. struct rtc_class_ops 結構
[cpp]?view plain?copy
struct?rtc_class_ops?{??
int?(*open)(struct?device?*);??
void?(*release)(struct?device?*);??
int?(*ioctl)(struct?device?*,?unsigned?int,?unsigned?long);??
int?(*read_time)(struct?device?*,?struct?rtc_time?*);??
int?(*set_time)(struct?device?*,?struct?rtc_time?*);??
int?(*read_alarm)(struct?device?*,?struct?rtc_wkalrm?*);??
int?(*set_alarm)(struct?device?*,?struct?rtc_wkalrm?*);??
int?(*proc)(struct?device?*,?struct?seq_file?*);??
int?(*set_mmss)(struct?device?*,?unsigned?long?secs);??
int?(*irq_set_state)(struct?device?*,?int?enabled);??
int?(*irq_set_freq)(struct?device?*,?int?freq);??
int?(*read_callback)(struct?device?*,?int?data);??
int?(*alarm_irq_enable)(struct?device?*,?unsigned?int?enabled);??
int?(*update_irq_enable)(struct?device?*,?unsigned?int?enabled);??
};??
這個結構是RTC驅動程序要實現的基本操作函數,注意這里的操作不是文件操作。驅動程序通過初始化這樣一個結構,將自己實現的函數與RTC核心聯系起來。這里面的大部分函數都要驅動程序來實現。而且這些函數都是操作底層硬件的,屬于最底層的函數。
? 3. struct rtc_time 結構
[cpp]?view plain?copy
struct?rtc_time?{??
int?tm_sec;??
int?tm_min;??
int?tm_hour;??
int?tm_mday;??
int?tm_mon;??
int?tm_year;??
int?tm_wday;??
int?tm_yday;??
int?tm_isdst;??
};??
代表了時間與日期,從RTC設備讀回的時間和日期就保存在這個結構體中
三. class.c?
? 1. 模塊初始化函數:rtc_init
[cpp]?view plain?copy
static?int?__init?rtc_init(void)??
{??
rtc_class?=?class_create(THIS_MODULE,?"rtc");??
if?(IS_ERR(rtc_class))?{??
printk(KERN_ERR?"%s:?couldn't?create?class\n",?__FILE__);??
return?PTR_ERR(rtc_class);??
}??
rtc_class->suspend?=?rtc_suspend;??
rtc_class->resume?=?rtc_resume;??
rtc_dev_init();??
rtc_sysfs_init(rtc_class);??
return?0;??
}??
rtc_init首先調用class_create創建了一個類--rtc。我們知道類是一個設備的高層視圖,他抽象出了底層的實現細節。類的作用就是向用戶空間提供設備的信息,驅動程序不需要直接處理類。然后初始化類結構的相應成員,rtc_suspend,rtc_resume這兩個函數也是在class.c中實現的。接下來調用rtc_dev_init(),這個函數為RTC設備動態分配設備號,保存在rtc_devt中。最后調用rtc_sysfs_init,初始化rtc_class的屬性。
? 2. 為底層驅動提供接口:rtc_device_register,rtc_device_unregister
[cpp]?view plain?copy
struct?rtc_device?*rtc_device_register(const?char?*name,?struct?device?*dev,??
const?struct?rtc_class_ops?*ops,??
struct?module?*owner)??
{??
struct?rtc_device?*rtc;??
int?id,?err;??
if?(idr_pre_get(&rtc_idr,?GFP_KERNEL)?==?0)?{??
err?=?-ENOMEM;??
goto?exit;??
}??
mutex_lock(&idr_lock);??
err?=?idr_get_new(&rtc_idr,?NULL,?&id);??
mutex_unlock(&idr_lock);??
/*--------------------(1)---------------------*/??
if?(err?0)??
goto?exit;??
id?=?id?&?MAX_ID_MASK;??
rtc?=?kzalloc(sizeof(struct?rtc_device),?GFP_KERNEL);??
if?(rtc?==?NULL)?{??
err?=?-ENOMEM;??
goto?exit_idr;??
}??
rtc->id?=?id;??
rtc->ops?=?ops;??
rtc->owner?=?owner;??
rtc->max_user_freq?=?64;??
rtc->dev.parent?=?dev;??
rtc->dev.class?=?rtc_class;??
rtc->dev.release?=?rtc_device_release;??
mutex_init(&rtc->ops_lock);??
spin_lock_init(&rtc->irq_lock);??
spin_lock_init(&rtc->irq_task_lock);??
init_waitqueue_head(&rtc->irq_queue);??
strlcpy(rtc->name,?name,?RTC_DEVICE_NAME_SIZE);??
dev_set_name(&rtc->dev,?"rtc%d",?id);??
/*-------------------(2)--------------------*/??
rtc_dev_prepare(rtc);??
err?=?device_register(&rtc->dev);??
if?(err)??
goto?exit_kfree;??
/*-------------------(3)--------------------*/??
rtc_dev_add_device(rtc);??
rtc_sysfs_add_device(rtc);??
rtc_proc_add_device(rtc);??
dev_info(dev,?"rtc?core:?registered?%s?as?%s\n",??
rtc->name,?dev_name(&rtc->dev));??
/*-------------------(4)--------------------*/??
return?rtc;??
exit_kfree:??
kfree(rtc);??
exit_idr:??
mutex_lock(&idr_lock);??
idr_remove(&rtc_idr,?id);??
mutex_unlock(&idr_lock);??
exit:??
dev_err(dev,?"rtc?core:?unable?to?register?%s,?err?=?%d\n",??
name,?err);??
return?ERR_PTR(err);??
}??
(1):處理一個idr的結構,idr在linux內核中指的就是整數ID管理機制,從本質上來說,idr是一種將整數ID號和特定指針關聯在一起的機制。這個機制最早是在2003年2月加入內核的,當時是作為POSIX定時器的一個補丁。現在,在內核的很多地方都可以找到idr的身影。詳細實現請參照相關內核代碼。這里從內核中獲取一個idr結構,并與id相關聯。
? ? (2):分配了一個rtc_device的結構--rtc,并且初始化了相關的成員:id, rtc_class_ops等等。
? ? (3):首先調用rtc_dev_prepare(在rtc-dev.c中定義)。因為RTC設備本質來講還是字符設備,所以這里初始化了字符設備相關的結構:設備號以及文件操作。然后調用device_register將設備注冊到linux設備模型核心。這樣在模塊加載的時候,udev daemon就會自動為我們創建設備文件rtc(n)。
? ? (4):先后調用rtc_dev_add_device,rtc_sysfs_add_device,rtc_proc_add_device三個函數。rtc_dev_add_device注冊字符設備,rtc_sysfs_add_device只是為設備添加了一個鬧鐘屬性,rtc_proc_add_device?創建proc文件系統接口。
四. rtc-dev.c?
? ? ? ? rtc-dev.c 初始化了一個file_operations結構--rtc_dev_fops,并定義了這些操作函數。
? 1. rtc_dev_fops rtc基本的文件操作
[cpp]?view plain?copy
static?const?struct?file_operations?rtc_dev_fops?=?{??
.owner??????=?THIS_MODULE,??
.llseek?????=?no_llseek,??
.read???????=?rtc_dev_read,??
.poll???????=?rtc_dev_poll,??
.unlocked_ioctl?=?rtc_dev_ioctl,??
.open???????=?rtc_dev_open,??
.release????=?rtc_dev_release,??
.fasync?????=?rtc_dev_fasync,??
;???
2. 函數的實現(以rtc_dev_read為例) ??
[cpp]?view plain?copy
rtc_dev_read(struct?file?*file,?char?__user?*buf,?size_t?count,?loff_t?*ppos)??
{??
struct?rtc_device?*rtc?=?file->private_data;??
DECLARE_WAITQUEUE(wait,?current);??
unsigned?long?data;??
ssize_t?ret;??
if?(count?!=?sizeof(unsigned?int)?&&?count?
return?-EINVAL;??
add_wait_queue(&rtc->irq_queue,?&wait);??
do?{??
__set_current_state(TASK_INTERRUPTIBLE);??
spin_lock_irq(&rtc->irq_lock);??
data?=?rtc->irq_data;??
rtc->irq_data?=?0;??
spin_unlock_irq(&rtc->irq_lock);??
if?(data?!=?0)?{??
ret?=?0;??
break;??
}??
if?(file->f_flags?&?O_NONBLOCK)?{??
ret?=?-EAGAIN;??
break;??
}??
if?(signal_pending(current))?{??
ret?=?-ERESTARTSYS;??
break;??
}??
schedule();??
}?while?(1);??
set_current_state(TASK_RUNNING);??
remove_wait_queue(&rtc->irq_queue,?&wait);??
if?(ret?==?0)?{??
/*?Check?for?any?data?updates?*/??
if?(rtc->ops->read_callback)??
data?=?rtc->ops->read_callback(rtc->dev.parent,??
data);??
if?(sizeof(int)?!=?sizeof(long)?&&??
count?==?sizeof(unsigned?int))??
ret?=?put_user(data,?(unsigned?int?__user?*)buf)??:??
sizeof(unsigned?int);??
else??
ret?=?put_user(data,?(unsigned?long?__user?*)buf)??:??
sizeof(unsigned?long);??
}??
return?ret;??
}??
這里的read不是應用程序用來獲取時間的,而是有其他的作用,他幫助應用程序周期性的完成一些工作。如果要使用這個功能,應用程序首先保證RTC驅動程序提供這樣的功能。這個功能是這樣實現的:進程讀取/dev/rtc(n),進程睡眠直到RTC中斷將他喚醒。我們可以發現,這里的睡眠是ldd3中提到的手工睡眠。這個函數的手工休眠過程如下:首先調用DECLARE_WAITQUEUE(wait, current),聲明一個等待隊列入口,然后調用add_wait_queue將這個入口加入到RTC的irq等待隊列里,然后進入循環。在循環里首先把進程的狀態改成TASK_INTERRUPTIBLE,這樣進程就不能再被調度運行。但是現在進程還在運行,沒有進入睡眠狀態。程序然后讀取RTC里面的irq_data,如果不是零,那么程序跳出這個循環,進程不會睡眠。因為這個irq_data在rtc的中斷處理程序會被賦值,而讀過之后就會清零,所以如果數據不是零的話說明發生過一次中斷。如果是零那么沒有發生中斷,調用schedule,進程會被調度出可運行隊列,從而讓出處理器,真正進入睡眠。跳出循環代表被喚醒,然后將進程狀態改變為可運行,移除等待隊列入口。最后將讀回的數據傳給用戶空間。
五. interface.c?
? ? ? ? interface.c里的所有函數的實現都對應于rtc-dev.c 中ioctl相應的命令。對應關系如下:
RTC_ALM_READ ? ? ? ? ? ? ? ? ? ? rtc_read_alarm ? ? ? ? ? 讀取鬧鐘時間
RTC_ALM_SET ? ? ? ? ? ? ? ? ? ? ?rtc_set_alarm ? ? ? ? ? ?設置鬧鐘時間
RTC_RD_TIME ? ? ? ? ? ? ? ? ? ? ?rtc_read_time ? ? ? ? ? ?讀取時間與日期
RTC_SET_TIME ? ? ? ? ? ? ? ? ? ? rtc_set_time ? ? ? ? ? ? 設置時間與日期
RTC_PIE_ON RTC_PIE_OFF ? ? ? ? ? rtc_irq_set_state ? ? ? ? ? ? ?開關RTC全局中斷的函數
RTC_AIE_ON RTC_AIE_OFF ? ? ? ? ? rtc_alarm_irq_enable ? ? 使能禁止RTC鬧鐘中斷
RTC_UIE_OFF RTC_UIE_ON ? ? ? ? ? rtc_update_irq_enable ? ?使能禁止RTC更新中斷
RTC_IRQP_SET ? ? ? ? ? ? ? ? ? ? rtc_irq_set_freq ? ? ? ? 設置中斷的頻率
? ? ? ?以上就是所有ioctl的命令與實現的對應關系。其中如果不涉及中斷的話,有兩個命令需要我們特別關心一下,就是RTC_RD_TIME與RTC_SET_TIME。因為RTC最基本的功能就是提供時間與日期。這兩個命令恰恰是獲取時間和設置時間。下面分析一下這兩個命令的實現,也就是rtc_set_alarm與rtc_read_time函數的實現:
? 1. rtc_read_time 函數
[cpp]?view plain?copy
int?rtc_read_time(struct?rtc_device?*rtc,?struct?rtc_time?*tm)??
{??
int?err;??
err?=?mutex_lock_interruptible(&rtc->ops_lock);??
if?(err)??
return?err;??
if?(!rtc->ops)??
err?=?-ENODEV;??
else?if?(!rtc->ops->read_time)??
err?=?-EINVAL;??
else?{??
memset(tm,?0,?sizeof(struct?rtc_time));??
err?=?rtc->ops->read_time(rtc->dev.parent,?tm);??
}??
mutex_unlock(&rtc->ops_lock);??
return?err;??
}??
這個函數用了一個信號來保證在同一時刻只有一個進程可以獲取時間。鎖定了這個信號量后,調用rtc->ops里面read函數,這個函數是由具體的驅動程序實現的,操作底層硬件。讀回的時間存放在rtc_time結構里面的。
? 2. rtc_set_time 函數
[cpp]?view plain?copy
int?rtc_set_time(struct?rtc_device?*rtc,?struct?rtc_time?*tm)??
{??
int?err;??
err?=?rtc_valid_tm(tm);??
if?(err?!=?0)??
return?err;??
err?=?mutex_lock_interruptible(&rtc->ops_lock);??
if?(err)??
return?err;??
if?(!rtc->ops)??
err?=?-ENODEV;??
else?if?(rtc->ops->set_time)??
err?=?rtc->ops->set_time(rtc->dev.parent,?tm);??
else?if?(rtc->ops->set_mmss)?{??
unsigned?long?secs;??
err?=?rtc_tm_to_time(tm,?&secs);??
if?(err?==?0)??
err?=?rtc->ops->set_mmss(rtc->dev.parent,?secs);??
}?else??
err?=?-EINVAL;??
mutex_unlock(&rtc->ops_lock);??
return?err;??
}??
這個函數其實和rtc_read_time函數差不多,同樣是鎖定信號量,同樣是調用底層驅動函數。但是這里的設置時間提供了兩個調用:一個是set_time,一個是set_mmss。因為有的RTC硬件只計算秒數,不關心墻鐘時間,所以如果是這樣的RTC,必須實現set_mmss來設置時間。
六. rtc-sysfs.c 部分
? ? ? ? 這個部分主要是有關sysfs的操作。rtc-sysfs.c中定義了這樣一個設備屬性組,如下:
[cpp]?view plain?copy
static?struct?device_attribute?rtc_attrs[]?=?{??
__ATTR(name,?S_IRUGO,?rtc_sysfs_show_name,?NULL),??
__ATTR(date,?S_IRUGO,?rtc_sysfs_show_date,?NULL),??
__ATTR(time,?S_IRUGO,?rtc_sysfs_show_time,?NULL),??
__ATTR(since_epoch,?S_IRUGO,?rtc_sysfs_show_since_epoch,?NULL),??
__ATTR(max_user_freq,?S_IRUGO?|?S_IWUSR,?rtc_sysfs_show_max_user_freq,??
rtc_sysfs_set_max_user_freq),??
__ATTR(hctosys,?S_IRUGO,?rtc_sysfs_show_hctosys,?NULL),??
{?},??
};??
這個屬性組是在class.c的模塊初始化函數中,由rtc_sysfs_init函數賦值給rtc_class->dev_attrs的,以后屬于這個類的設備都會有這些屬性。但是我們知道要想一個設備結構擁有一種屬性,必須調用device_create_file,這樣才會使這個屬性出現在sysfs相關設備目錄里。但是在這里的代碼中只是給這個類的dev_attrs域賦值了這個屬性組指針,而沒有調用device_create_file。我原來以為是在rtc_device_resgister函數中,由rtc_sysfs_add_device完成這個工作,但是這個函數只是給設備添加了鬧鐘屬性,并沒有處理這個屬性組。最后發現這個工作是由device_register來完成的。這里的調用關系有點復雜:
device_register調用device_add
device_add調用 device_add_attrs
device_add_attrs調用device_add_attributes
device_add_attributes調用device_create_file來完成設備的屬性設置的。
設置完屬性后,在/sys/class/rtc/rtc(n)的目錄下就會出現name,date,time等文件,用戶讀這些文件的時候就會調用相應的函數。如讀取name文件,就會調用rtc_sysfs_show_name函數,這個函數也是在rtc-sysfs.c中實現的,作用是讀取并顯示時間。
七. rtc-proc.c?
? ? ? ?這個文件提供RTC的proc文件系統接口。proc文件系統是軟件創建的文件系統,內核通過他向外界導出信息,下面的每一個文件都綁定一個函數,當用戶讀取這個文件的時候,這個函數會向文件寫入信息。rtc-proc.c中初始化了一個文件操作:
[cpp]?view plain?copy
static?const?struct?file_operations?rtc_proc_fops?=?{??
.open???????=?rtc_proc_open,??
.read???????=?seq_read,??
.llseek?????=?seq_lseek,??
.release????=?rtc_proc_release,??
};??
RTC驅動在向RTC核心注冊自己的時候,由注冊函數rtc_device_resgister調用rtc_proc_add_device來實現proc接口的初始化,這個函數如下定義:
[cpp]?view plain?copy
void?rtc_proc_add_device(struct?rtc_device?*rtc)??
{??
if?(rtc->id?==?0)??
proc_create_data("driver/rtc",?0,?NULL,?&rtc_proc_fops,?rtc);??
}??
他主要調用了proc_create_data。proc_create_data完成創建文件節點的作用,并將文件的操作函數與節點聯系起來。調用這個函數后,在/proc/driver目錄下就會有一個文件rtc,應用程序打開這個文件就會調用rtc_proc_open函數,這個函數如下定義:
[cpp]?view plain?copy
static?int?rtc_proc_open(struct?inode?*inode,?struct?file?*file)??
{??
struct?rtc_device?*rtc?=?PDE(inode)->data;??
if?(!try_module_get(THIS_MODULE))??
return?-ENODEV;??
return?single_open(file,?rtc_proc_show,?rtc);??
}??
我們知道一個proc的文件必須與一個操作函數組成一個proc入口項,這個文件才能正常工作。這個函數最主要作用就是調用single_open,創建一個proc文件入口項,使其操作函數是rtc_proc_show,并初始化seq_file接口。rtc_proc_show函數如下定義:
[cpp]?view plain?copy
static?int?rtc_proc_show(struct?seq_file?*seq,?void?*offset)??
{??
int?err;??
struct?rtc_device?*rtc?=?seq->private;??
const?struct?rtc_class_ops?*ops?=?rtc->ops;??
struct?rtc_wkalrm?alrm;??
struct?rtc_time?tm;??
err?=?rtc_read_time(rtc,?&tm);??
if?(err?==?0)?{??
seq_printf(seq,??
"rtc_time\t:?%02d:%02d:%02d\n"??
"rtc_date\t:?%04d-%02d-%02d\n",??
tm.tm_hour,?tm.tm_min,?tm.tm_sec,??
tm.tm_year?+?1900,?tm.tm_mon?+?1,?tm.tm_mday);??
}??
err?=?rtc_read_alarm(rtc,?&alrm);??
if?(err?==?0)?{??
seq_printf(seq,?"alrm_time\t:?");??
if?((unsigned?int)alrm.time.tm_hour?<=?24)??
seq_printf(seq,?"%02d:",?alrm.time.tm_hour);??
else??
seq_printf(seq,?"**:");??
if?((unsigned?int)alrm.time.tm_min?<=?59)??
seq_printf(seq,?"%02d:",?alrm.time.tm_min);??
else??
seq_printf(seq,?"**:");??
if?((unsigned?int)alrm.time.tm_sec?<=?59)??
seq_printf(seq,?"%02d\n",?alrm.time.tm_sec);??
else??
seq_printf(seq,?"**\n");??
seq_printf(seq,?"alrm_date\t:?");??
if?((unsigned?int)alrm.time.tm_year?<=?200)??
seq_printf(seq,?"%04d-",?alrm.time.tm_year?+?1900);??
else??
seq_printf(seq,?"****-");??
if?((unsigned?int)alrm.time.tm_mon?<=?11)??
seq_printf(seq,?"%02d-",?alrm.time.tm_mon?+?1);??
else??
seq_printf(seq,?"**-");??
if?(alrm.time.tm_mday?&&?(unsigned?int)alrm.time.tm_mday?<=?31)??
seq_printf(seq,?"%02d\n",?alrm.time.tm_mday);??
else??
seq_printf(seq,?"**\n");??
seq_printf(seq,?"alarm_IRQ\t:?%s\n",??
alrm.enabled???"yes"?:?"no");??
seq_printf(seq,?"alrm_pending\t:?%s\n",??
alrm.pending???"yes"?:?"no");??
}??
seq_printf(seq,?"24hr\t\t:?yes\n");??
if?(ops->proc)??
ops->proc(rtc->dev.parent,?seq);??
return?0;??
}??
這個函數就是最后給用戶顯示信息的函數了,可以看出他通過調用rtc_deivce中的操作函數,讀取時間,日期和一些其他的信息顯示給用戶。?
六. 總結
RTC核心使底層硬件對用戶來說是透明的,并且減少了編寫驅動程序的工作量。RTC新的驅動接口提供了更多的功能,使系統可以同時存在多個RTC。/dev,sysfs,proc這三種機制的實現使得應用程序能靈活的使用RTC,RTC核心雖然表面上看上去很簡單,但是還是涉及到很多知識,有些東西書上講的還是不夠詳細,還需要通過分析代碼加深理解。 另外RTC核心代碼的組織方式也值得學習,不同功能的代碼放在不同的文件中,簡單明了。??
?
評論
查看更多