近期需要实现安卓驱动程序中的固件下载功能,故而研究了安卓源码相关的函数,在此总结一下学习成果。

首先,要知道安卓系统对此是编写了对应的函数接口的,即request_firmware。主要有两种方法来下载固件文件,这两种方法只要读懂了request_firmware等函数的内容以后,实现起来都很简单。

一种是在安卓源码中修改fw_path参数,在其中添加一条固件所在文件夹位置的路径,但因客户不希望改动源码,故而放弃;另一种是仿照request_firmware的函数调用流程,在驱动中实现读取固件文件并获取数据传给下层的函数,如此只需改变驱动,这也是我最终采取的方法。
代码函数基本在…/kernel/drivers/base/firmware_loader下

以下开始分析安卓中的request_firmware为起点的函数调用流程:

request_firmware

int
request_firmware(const struct firmware **firmware_p, const char *name,
		 struct device *device)
{
	int ret;

	/* Need to pin this module until return */
	__module_get(THIS_MODULE);
	ret = _request_firmware(firmware_p, name, device,
				FW_OPT_UEVENT | FW_OPT_FALLBACK);
	module_put(THIS_MODULE);
	return ret;
}

由此可见,在驱动中调用request_firmware需要几个参数:firmware_p是驱动中存储对应固件的结构体指针的地址;name是固件的文件名称,如“fmac.bin"这样的字符串格式;device则是存储当前设备的结构体指针。
故而,在驱动中调用的格式应当为ret = request_firmware(&firmware, "fmac.bin”, device);

_request_firmware

/* called from request_firmware() and request_firmware_work_func() */
static int
_request_firmware(const struct firmware **firmware_p, const char *name,
		  struct device *device, unsigned int opt_flags)
{
	struct firmware *fw;
	long timeout;
	int ret;

	if (!firmware_p)//先判断是否为NULL
		return -EINVAL;

	if (!name || name[0] == '\0')//判断是否错误文件名字符串
		return -EINVAL;

	ret = _request_firmware_prepare(&fw, name, device);//创建需要用到的结构体并申请空间等准备操作
	if (ret <= 0) /* error or already assigned */
		goto out;

	...............
	//一些超时或出错判断,以及flags不同值对应的功能的函数调用
	
	ret = fw_get_filesystem_firmware(device, fw->priv);//最为关键的一个函数,读取固件文件数据
	if (ret) {
		if (!(opt_flags & FW_OPT_NO_WARN))
			dev_warn(device,
				 "Direct firmware load for %s failed with error %d\n",
				 name, ret);
		if (opt_flags & FW_OPT_USERHELPER) {
			dev_warn(device, "Falling back to user helper\n");
			ret = fw_load_from_user_helper(fw, name, device,
						       opt_flags, timeout);
		}
	}

	...............
	//一些赋值和释放,以及flags不同值对应的功能的函数调用还有保护操作等等
	
	*firmware_p = fw;
	return ret;
}

__request_firmware的参数除了上层函数传进来的参数以外,还多了一个标识符flags。

其中最为主要的就是它调用的两个函数:_request_firmware_prepare与fw_get_filesystem_firmware,前者初始化各类结构体,若return 1说明需要load固件,return 0说明不需要,负数说明出错;后者通过传递进去的赋好值的结构体变量读取对应文件并将数据存到对应的变量参数里。

_request_firmware_prepare

/* prepare firmware and firmware_buf structs;
 * return 0 if a firmware is already assigned, 1 if need to load one,
 * or a negative error code
 */
static int
_request_firmware_prepare(struct firmware **firmware_p, const char *name,
			  struct device *device)
{
	struct firmware *firmware;
	struct firmware_buf *buf;
	int ret;

	*firmware_p = firmware = kzalloc(sizeof(*firmware), GFP_KERNEL);//申请内核空间
	if (!firmware) {//若申请失败,返回错误类型值
		dev_err(device, "%s: kmalloc(struct firmware) failed\n",
			__func__);
		return -ENOMEM;
	}

	if (fw_get_builtin_firmware(firmware, name)) {//先遍历内部builtin_firmware列表,查看当前固件是否在内。若查找成功,则无需重新创建注册固件,成功返回即可
		dev_dbg(device, "firmware: using built-in firmware %s\n", name);
		return 0; /* assigned */
	}

	ret = fw_lookup_and_allocate_buf(name, &fw_cache, &buf);//查找

	/*
	 * bind with 'buf' now to avoid warning in failure path
	 * of requesting firmware.
	 */
	firmware->priv = buf;

	if (ret > 0) {
		ret = sync_cached_firmware_buf(buf);
		if (!ret) {
			fw_set_page_data(buf, firmware);
			return 0; /* assigned */
		}
	}

	if (ret < 0)
		return ret;
	return 1; /* need to load */
}

进此函数,首先分配空间给firmware变量。之后,调用fw_get_builtin_firmware从内核builtin_fw段中查找是否有相匹配的固件文件。若查找失败,则调用fw_lookup_and_allocate_buf,进行进一步的查找,并创建firmware_buffer的内存。

fw_lookup_and_allocate_buf返回后,buf->fw_id将会是值为固件文件名的字符串,此时将buf赋值给firmware->priv。

如果返回值ret>0,则说明无需load,之前已load过并找到了匹配项。这种情况下,需要与cache同步一下固件文件信息。
如果ret<0,则说明出现错误,直接返回此值向上层反馈。
return 1说明此固件需要load。

fw_get_builtin_firmware

static bool fw_get_builtin_firmware(struct firmware *fw, const char *name)
{
	struct builtin_fw *b_fw;

	for (b_fw = __start_builtin_fw; b_fw != __end_builtin_fw; b_fw++) {
		if (strcmp(name, b_fw->name) == 0) {
			fw->size = b_fw->size;
			fw->data = b_fw->data;
			return true;
		}
	}

	return false;
}

fw_get_builtin_firmware内部很简单,就是用for循环遍历内核内部从__start_builtin_fw到__end_builtin_fw的builtin_fw段中是否有与当前所需固件相同文件名的文件(即与所求固件条件相匹配的文件),有则将数据和大小均赋值给传进来的firmware变量里,并告知上层调用它的函数已找到所求固件,返回true。

fw_lookup_and_allocate_buf

static int fw_lookup_and_allocate_buf(const char *fw_name,
				      struct firmware_cache *fwc,
				      struct firmware_buf **buf)
{
	struct firmware_buf *tmp;

	spin_lock(&fwc->lock);
	tmp = __fw_lookup_buf(fw_name);
	if (tmp) {
		kref_get(&tmp->ref);
		spin_unlock(&fwc->lock);
		*buf = tmp;
		return 1;
	}
	tmp = __allocate_fw_buf(fw_name, fwc);
	if (tmp)
		list_add(&tmp->list, &fwc->head);
	spin_unlock(&fwc->lock);

	*buf = tmp;

	return tmp ? 0 : -ENOMEM;
}

本函数的一参数fwc是一全局变量。hcd.c
本函数先调用了__fw_lookup_buf,在全局变量fw_cache中查找,即从固件的cache中查找是否存在尚未被替换掉的相匹配的固件,如果有返回1说明无需load。代码如下:


static struct firmware_buf *__fw_lookup_buf(const char *fw_name)
{
	struct firmware_buf *tmp;
	struct firmware_cache *fwc = &fw_cache;

	list_for_each_entry(tmp, &fwc->head, list)
		if (!strcmp(tmp->fw_id, fw_name))
			return tmp;
	return NULL;
}

之后,若没有找到,则需要调用__allocate_fw_buf创建并分配一个firmware_buf结构体变量。若成功创建并初始化,则将当前的buf添加到cache链表中。函数return的判断,若tmp!=NULL,则返回0,说明需要load;反之返回负数,说明分配空间时出错。代码如下:

static struct firmware_buf *__allocate_fw_buf(const char *fw_name,
					      struct firmware_cache *fwc)
{
	struct firmware_buf *buf;

	buf = kzalloc(sizeof(*buf), GFP_ATOMIC);
	if (!buf)
		return NULL;

	buf->fw_id = kstrdup_const(fw_name, GFP_ATOMIC);
	if (!buf->fw_id) {
		kfree(buf);
		return NULL;
	}

	kref_init(&buf->ref);
	buf->fwc = fwc;
	init_completion(&buf->completion);
#ifdef CONFIG_FW_LOADER_USER_HELPER
	INIT_LIST_HEAD(&buf->pending_list);
#endif

	pr_debug("%s: fw-%s buf=%p\n", __func__, fw_name, buf);

	return buf;
}

函数一进来就为firmware_buf变量分配内存,buf->fw_id对应的是固件文件名的字符串结构也需申请一段空间并将文件名赋值过去。此后再需要用固件文件名都将从buf->fw_id中获取。

fw_get_filesystem_firmware

最重要的函数来了,在这个函数里完成了打开固件文件并读取数据放到变量中的关键操作。

static int fw_get_filesystem_firmware(struct device *device,
				       struct firmware_buf *buf)
{
	int i, len;
	int rc = -ENOENT;
	char *path;

	path = __getname();
	if (!path)
		return -ENOMEM;

	for (i = 0; i < ARRAY_SIZE(fw_path); i++) {
		struct file *file;

		/* skip the unset customized path */
		if (!fw_path[i][0])
			continue;

		len = snprintf(path, PATH_MAX, "%s/%s",
			       fw_path[i], buf->fw_id);
		if (len >= PATH_MAX) {
			rc = -ENAMETOOLONG;
			break;
		}

		file = filp_open(path, O_RDONLY, 0);
		if (IS_ERR(file))
			continue;
		rc = fw_read_file_contents(file, buf);
		fput(file);
		if (rc)
			dev_warn(device, "firmware, attempted to load %s, but failed with error %d\n",
				path, rc);
		else
			break;
	}
	__putname(path);

	if (!rc) {
		dev_dbg(device, "firmware: direct-loading firmware %s\n",
			buf->fw_id);
		printk("JINGSU:firmware: direct-loading firmware %s\n",buf->fw_id);//jingsu
		mutex_lock(&fw_lock);
		set_bit(FW_STATUS_DONE, &buf->status);
		complete_all(&buf->completion);
		mutex_unlock(&fw_lock);
	}

	return rc;
}

函数一上来调用了__getname()这个函数和__putname()是一对,必须配对使用。
二者定义在~/android-8.1/kernel/include/linux/fs.h中:

#define __getname()             kmem_cache_alloc(names_cachep, GFP_KERNEL)
#define __putname(name)         kmem_cache_free(names_cachep, (void *)(name))

由定义式可看出实际上就是申请和释放内存。
为path变量申请内存后,之后的for循环里,依次遍历fw_path中的表项,调用snprintf将文件路径(fw_path[i])和文件名(buf->fw_id)拼接成一个完整的字符串,值为固件的完整存放位置,如"/vendor/firmware/fmac.bin"。

若对应路径的文件存在且打开成功,则调用fw_read_file_contents读取其内部数据,之后调用fput(file)对文件做一些保护操作后关闭。fw_read_file_contents读取完毕后,还有一些状态等保护操作,略。此函数结束时,已获取到了固件文件的大小和数据在firmware_buf变量中,这对应的是在函数_request_firmware中的firmware->priv。当返回到_request_firmware里时,firmware->priv中已有了data和size的值。

其中,fput定义在~/android-8.1/kernel/fs/file_table.c中:
atomic_long_dec_and_test是原子操作函数,详细讲解可看blog
https://blog.csdn.net/yikai2009/article/details/8650221

这里调用的原子函数是自减然后判断是否为0,是则返回true。这是文件并发争用情况下的保护操作,即若此时多线程在使用此文件,则无需顾及关闭文件等一系列保护文件的操作;若此时线程是最后一个使用本文件的,则需要实施这些操作。

if内的多条语句是一些安全、状态、任务处理等等,最终会导致此文件安全的关闭。

void fput(struct file *file)
{
        if (atomic_long_dec_and_test(&file->f_count)) {
                struct task_struct *task = current;

                if (likely(!in_interrupt() && !(task->flags & PF_KTHREAD))) {
                        init_task_work(&file->f_u.fu_rcuhead, ____fput);
                        if (!task_work_add(task, &file->f_u.fu_rcuhead, true))
                                return;
                        /*
                         * After this task has run exit_task_work(),
                         * task_work_add() will fail.  Fall through to delayed
                         * fput to avoid leaking *file.
                         */
                }

                if (llist_add(&file->f_u.fu_llist, &delayed_fput_list))
                        schedule_delayed_work(&delayed_fput_work, 1);
        }
}

我在驱动中仿写文件读取时(即第二种实现方法里),此处直接用了filp_close替代用来关闭文件,因为该固件文件只有我们驱动自己会调用,故不涉及到争用问题,也无需各类文件保护,只需使用完毕关闭文件即可。

而fw_path是一个全局变量,里面自带一些系统默认的固件存放位置,如下:

static const char * const fw_path[] = {
	fw_path_para,
	//"vendor/firmware",//可以在此处添加目的文件存放位置,格式如本行。
	//其他行为系统自带
	"/lib/firmware/updates/" UTS_RELEASE,
	"/lib/firmware/updates",
	"/lib/firmware/" UTS_RELEASE,
	"/lib/firmware"
};

修改fw_path就是文章一开头提到的修改内核的实现方法,例如我需要load的固件存放位置是/vendor/firmware,则需在此处添一行位置字符串,然后在驱动中调用request_firmware即可成功下载固件。

fw_read_file_contents

 int fw_read_file_contents(struct file *file, struct firmware_buf *fw_buf)
{
	int size;
	char *buf;
	int rc;

	if (!S_ISREG(file_inode(file)->i_mode))
		return -EINVAL;
	size = i_size_read(file_inode(file));//获得文件大小,以字节为单位
	if (size <= 0)
		return -EINVAL;
	buf = vmalloc(size);//申请空间
	if (!buf)
		return -ENOMEM;
	rc = kernel_read(file, 0, buf, size);//将从file指向的文件里,从0位置开始读取size个字节大小的数据到存到buf中,返回已读取的字节数
	if (rc != size) {//若已读取的字节数与文件大小不一致则出现错误
		if (rc > 0)
			rc = -EIO;
		goto fail;
	}
	rc = security_kernel_fw_from_file(file, buf, size);
	if (rc)
		goto fail;
	fw_buf->data = buf;
	fw_buf->size = size;
	return 0;
fail:
	vfree(buf);
	return rc;
}

本函数重点是kernel_read函数,是内核用于读文件的接口函数,定义位于~/android-8.1/kernel/fs/exec.c
security_kernel_fw_from_file函数用于文件保护,定义位于~/android-8.1/kernel/security/security.c

由上代码可知,安卓内核中,主要有几种方式load firmware:

  1. 在builtin段中查找
  2. 从cache中查找
  3. 通过fw_path中路径查找

PS:前两天需要将驱动移植到安卓10上,由于8和10的源码差距较大,原先驱动中的固件下载部分出现了一些问题,后查明是安卓10的kernel_read函数定义与之前不同了。
总体而言,10里的固件下载流程差的不多,就是多了几个例如kernel_read_file_from_path
、security_kernel_post_read_file等函数。如果想要跟代码还是可以从request_firmware跟起。

此外,部分函数所在文件位置不一定和上文相同,请自行grep。

附上两版本的kernel_read定义来看下这个巨坑!!!这个教训告诉我们,当内核版本更换导致问题的时候,首先要谨慎的检查我们调用的内核函数是否出现更改。

//安卓8
int kernel_read(struct file *file, loff_t offset, char *addr, unsigned long count)

//安卓10
ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐