cregit-Linux how code gets into the kernel

Release 4.17 fs/char_dev.c

Directory: fs
// SPDX-License-Identifier: GPL-2.0
/*
 *  linux/fs/char_dev.c
 *
 *  Copyright (C) 1991, 1992  Linus Torvalds
 */

#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/slab.h>
#include <linux/string.h>

#include <linux/major.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/seq_file.h>

#include <linux/kobject.h>
#include <linux/kobj_map.h>
#include <linux/cdev.h>
#include <linux/mutex.h>
#include <linux/backing-dev.h>
#include <linux/tty.h>

#include "internal.h"


static struct kobj_map *cdev_map;

static DEFINE_MUTEX(chrdevs_lock);


#define CHRDEV_MAJOR_HASH_SIZE 255


static struct char_device_struct {
	
struct char_device_struct *next;
	
unsigned int major;
	
unsigned int baseminor;
	
int minorct;
	
char name[64];
	
struct cdev *cdev;		/* will die */

} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

/* index in the above */

static inline int major_to_index(unsigned major) { return major % CHRDEV_MAJOR_HASH_SIZE; }

Contributors

PersonTokensPropCommitsCommitProp
Andrew Morton1280.00%125.00%
Brian Gerst16.67%125.00%
Yang Zhang16.67%125.00%
Joe Korty16.67%125.00%
Total15100.00%4100.00%

#ifdef CONFIG_PROC_FS
void chrdev_show(struct seq_file *f, off_t offset) { struct char_device_struct *cd; mutex_lock(&chrdevs_lock); for (cd = chrdevs[major_to_index(offset)]; cd; cd = cd->next) { if (cd->major == offset) seq_printf(f, "%3d %s\n", cd->major, cd->name); } mutex_unlock(&chrdevs_lock); }

Contributors

PersonTokensPropCommitsCommitProp
Joe Korty2533.33%116.67%
Andrew Morton2026.67%116.67%
Brian Gerst1418.67%116.67%
Logan Gunthorpe1317.33%116.67%
Neil Horman22.67%116.67%
Jes Sorensen11.33%116.67%
Total75100.00%6100.00%

#endif /* CONFIG_PROC_FS */
static int find_dynamic_major(void) { int i; struct char_device_struct *cd; for (i = ARRAY_SIZE(chrdevs)-1; i >= CHRDEV_MAJOR_DYN_END; i--) { if (chrdevs[i] == NULL) return i; } for (i = CHRDEV_MAJOR_DYN_EXT_START; i >= CHRDEV_MAJOR_DYN_EXT_END; i--) { for (cd = chrdevs[major_to_index(i)]; cd; cd = cd->next) if (cd->major == i) break; if (cd == NULL) return i; } return -EBUSY; }

Contributors

PersonTokensPropCommitsCommitProp
Logan Gunthorpe10398.10%150.00%
Srivatsa S. Bhat21.90%150.00%
Total105100.00%2100.00%

/* * Register a single major with a specified minor range. * * If major == 0 this functions will dynamically allocate a major and return * its number. * * If major > 0 this function will attempt to reserve the passed range of * minors and will return zero on success. * * Returns a -ve errno on failure. */
static struct char_device_struct * __register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name) { struct char_device_struct *cd, **cp; int ret = 0; int i; cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL); if (cd == NULL) return ERR_PTR(-ENOMEM); mutex_lock(&chrdevs_lock); if (major == 0) { ret = find_dynamic_major(); if (ret < 0) { pr_err("CHRDEV \"%s\" dynamic allocation region is full\n", name); goto out; } major = ret; } if (major >= CHRDEV_MAJOR_MAX) { pr_err("CHRDEV \"%s\" major requested (%u) is greater than the maximum (%u)\n", name, major, CHRDEV_MAJOR_MAX-1); ret = -EINVAL; goto out; } cd->major = major; cd->baseminor = baseminor; cd->minorct = minorct; strlcpy(cd->name, name, sizeof(cd->name)); i = major_to_index(major); for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) if ((*cp)->major > major || ((*cp)->major == major && (((*cp)->baseminor >= baseminor) || ((*cp)->baseminor + (*cp)->minorct > baseminor)))) break; /* Check for overlapping minor ranges. */ if (*cp && (*cp)->major == major) { int old_min = (*cp)->baseminor; int old_max = (*cp)->baseminor + (*cp)->minorct - 1; int new_min = baseminor; int new_max = baseminor + minorct - 1; /* New driver overlaps from the left. */ if (new_max >= old_min && new_max <= old_max) { ret = -EBUSY; goto out; } /* New driver overlaps from the right. */ if (new_min <= old_max && new_min >= old_min) { ret = -EBUSY; goto out; } } cd->next = *cp; *cp = cd; mutex_unlock(&chrdevs_lock); return cd; out: mutex_unlock(&chrdevs_lock); kfree(cd); return ERR_PTR(ret); }

Contributors

PersonTokensPropCommitsCommitProp
Andrew Morton18346.10%215.38%
Amos Waterland9122.92%17.69%
Brian Gerst4010.08%17.69%
Logan Gunthorpe369.07%215.38%
Al Viro225.54%17.69%
Cyrill V. Gorcunov71.76%17.69%
Linus Walleij71.76%17.69%
Neil Horman41.01%17.69%
Srivatsa S. Bhat30.76%17.69%
Jes Sorensen30.76%17.69%
Oliver Neukum10.25%17.69%
Total397100.00%13100.00%


static struct char_device_struct * __unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct) { struct char_device_struct *cd = NULL, **cp; int i = major_to_index(major); mutex_lock(&chrdevs_lock); for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next) if ((*cp)->major == major && (*cp)->baseminor == baseminor && (*cp)->minorct == minorct) break; if (*cp) { cd = *cp; *cp = cd->next; } mutex_unlock(&chrdevs_lock); return cd; }

Contributors

PersonTokensPropCommitsCommitProp
Andrew Morton7863.41%240.00%
Al Viro2822.76%120.00%
Brian Gerst1512.20%120.00%
Jes Sorensen21.63%120.00%
Total123100.00%5100.00%

/** * register_chrdev_region() - register a range of device numbers * @from: the first in the desired range of device numbers; must include * the major number. * @count: the number of consecutive device numbers required * @name: the name of the device or driver. * * Return value is zero on success, a negative error code on failure. */
int register_chrdev_region(dev_t from, unsigned count, const char *name) { struct char_device_struct *cd; dev_t to = from + count; dev_t n, next; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n)+1, 0); if (next > to) next = to; cd = __register_chrdev_region(MAJOR(n), MINOR(n), next - n, name); if (IS_ERR(cd)) goto fail; } return 0; fail: to = n; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n)+1, 0); kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); } return PTR_ERR(cd); }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro16094.67%125.00%
Brian Gerst63.55%125.00%
Andrew Morton21.18%125.00%
Stephen Hemminger10.59%125.00%
Total169100.00%4100.00%

/** * alloc_chrdev_region() - register a range of char device numbers * @dev: output parameter for first assigned number * @baseminor: first of the requested range of minor numbers * @count: the number of minor numbers required * @name: the name of the associated device or driver * * Allocates a range of char device numbers. The major number will be * chosen dynamically, and returned (along with the first minor number) * in @dev. Returns zero or a negative error code. */
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name) { struct char_device_struct *cd; cd = __register_chrdev_region(0, baseminor, count, name); if (IS_ERR(cd)) return PTR_ERR(cd); *dev = MKDEV(cd->major, cd->baseminor); return 0; }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro5175.00%120.00%
Andrew Morton811.76%120.00%
Brian Gerst45.88%120.00%
Paul Fulghum45.88%120.00%
Stephen Hemminger11.47%120.00%
Total68100.00%5100.00%

/** * __register_chrdev() - create and register a cdev occupying a range of minors * @major: major device number or 0 for dynamic allocation * @baseminor: first of the requested range of minor numbers * @count: the number of minor numbers required * @name: name of this range of devices * @fops: file operations associated with this devices * * If @major == 0 this functions will dynamically allocate a major and return * its number. * * If @major > 0 this function will attempt to reserve a device with the given * major number and will return zero on success. * * Returns a -ve errno on failure. * * The name of this device has nothing to do with the name of the device in * /dev. It only helps to keep track of the different owners of devices. If * your module name has only one type of devices it's ok to use e.g. the name * of the module here. */
int __register_chrdev(unsigned int major, unsigned int baseminor, unsigned int count, const char *name, const struct file_operations *fops) { struct char_device_struct *cd; struct cdev *cdev; int err = -ENOMEM; cd = __register_chrdev_region(major, baseminor, count, name); if (IS_ERR(cd)) return PTR_ERR(cd); cdev = cdev_alloc(); if (!cdev) goto out2; cdev->owner = fops->owner; cdev->ops = fops; kobject_set_name(&cdev->kobj, "%s", name); err = cdev_add(cdev, MKDEV(cd->major, baseminor), count); if (err) goto out; cd->cdev = cdev; return major ? 0 : cd->major; out: kobject_put(&cdev->kobj); out2: kfree(__unregister_chrdev_region(cd->major, baseminor, count)); return err; }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro15486.52%450.00%
Tejun Heo158.43%112.50%
Brian Gerst42.25%112.50%
Stephen Hemminger42.25%112.50%
Arjan van de Ven10.56%112.50%
Total178100.00%8100.00%

/** * unregister_chrdev_region() - unregister a range of device numbers * @from: the first in the range of numbers to unregister * @count: the number of device numbers to unregister * * This function will unregister a range of @count device numbers, * starting with @from. The caller should normally be the one who * allocated those numbers in the first place... */
void unregister_chrdev_region(dev_t from, unsigned count) { dev_t to = from + count; dev_t n, next; for (n = from; n < to; n = next) { next = MKDEV(MAJOR(n)+1, 0); if (next > to) next = to; kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n)); } }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro83100.00%2100.00%
Total83100.00%2100.00%

/** * __unregister_chrdev - unregister and destroy a cdev * @major: major device number * @baseminor: first of the range of minor numbers * @count: the number of minor numbers this cdev is occupying * @name: name of this range of devices * * Unregister and destroy the cdev occupying the region described by * @major, @baseminor and @count. This function undoes what * __register_chrdev() did. */
void __unregister_chrdev(unsigned int major, unsigned int baseminor, unsigned int count, const char *name) { struct char_device_struct *cd; cd = __unregister_chrdev_region(major, baseminor, count); if (cd && cd->cdev) cdev_del(cd->cdev); kfree(cd); }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro2950.00%240.00%
Andrew Morton1729.31%120.00%
Tejun Heo1118.97%120.00%
Akinobu Mita11.72%120.00%
Total58100.00%5100.00%

static DEFINE_SPINLOCK(cdev_lock);
static struct kobject *cdev_get(struct cdev *p) { struct module *owner = p->owner; struct kobject *kobj; if (owner && !try_module_get(owner)) return NULL; kobj = kobject_get(&p->kobj); if (!kobj) module_put(owner); return kobj; }

Contributors

PersonTokensPropCommitsCommitProp
Arjan van de Ven63100.00%1100.00%
Total63100.00%1100.00%


void cdev_put(struct cdev *p) { if (p) { struct module *owner = p->owner; kobject_put(&p->kobj); module_put(owner); } }

Contributors

PersonTokensPropCommitsCommitProp
Arjan van de Ven2976.32%150.00%
Brian King923.68%150.00%
Total38100.00%2100.00%

/* * Called every time a character special file is opened */
static int chrdev_open(struct inode *inode, struct file *filp) { const struct file_operations *fops; struct cdev *p; struct cdev *new = NULL; int ret = 0; spin_lock(&cdev_lock); p = inode->i_cdev; if (!p) { struct kobject *kobj; int idx; spin_unlock(&cdev_lock); kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); if (!kobj) return -ENXIO; new = container_of(kobj, struct cdev, kobj); spin_lock(&cdev_lock); /* Check i_cdev again in case somebody beat us to it while we dropped the lock. */ p = inode->i_cdev; if (!p) { inode->i_cdev = p = new; list_add(&inode->i_devices, &p->list); new = NULL; } else if (!cdev_get(p)) ret = -ENXIO; } else if (!cdev_get(p)) ret = -ENXIO; spin_unlock(&cdev_lock); cdev_put(new); if (ret) return ret; ret = -ENXIO; fops = fops_get(p->ops); if (!fops) goto out_cdev_put; replace_fops(filp, fops); if (filp->f_op->open) { ret = filp->f_op->open(inode, filp); if (ret) goto out_cdev_put; } return 0; out_cdev_put: cdev_put(p); return ret; }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro18869.63%228.57%
Brian Gerst5921.85%114.29%
Christoph Hellwig186.67%114.29%
Andrew Morton31.11%114.29%
Jonathan Corbet10.37%114.29%
Cheng Renquan10.37%114.29%
Total270100.00%7100.00%


void cd_forget(struct inode *inode) { spin_lock(&cdev_lock); list_del_init(&inode->i_devices); inode->i_cdev = NULL; inode->i_mapping = &inode->i_data; spin_unlock(&cdev_lock); }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro3680.00%150.00%
Dan J Williams920.00%150.00%
Total45100.00%2100.00%


static void cdev_purge(struct cdev *cdev) { spin_lock(&cdev_lock); while (!list_empty(&cdev->list)) { struct inode *inode; inode = container_of(cdev->list.next, struct inode, i_devices); list_del_init(&inode->i_devices); inode->i_cdev = NULL; } spin_unlock(&cdev_lock); }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro7098.59%150.00%
Adrian Bunk11.41%150.00%
Total71100.00%2100.00%

/* * Dummy default file-operations: the only thing this does * is contain the open that then fills in the correct operations * depending on the special file... */ const struct file_operations def_chr_fops = { .open = chrdev_open, .llseek = noop_llseek, };
static struct kobject *exact_match(dev_t dev, int *part, void *data) { struct cdev *p = data; return &p->kobj; }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro32100.00%1100.00%
Total32100.00%1100.00%


static int exact_lock(dev_t dev, void *data) { struct cdev *p = data; return cdev_get(p) ? 0 : -1; }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro31100.00%1100.00%
Total31100.00%1100.00%

/** * cdev_add() - add a char device to the system * @p: the cdev structure for the device * @dev: the first device number for which this device is responsible * @count: the number of consecutive minor numbers corresponding to this * device * * cdev_add() adds the device represented by @p to the system, making it * live immediately. A negative error code is returned on failure. */
int cdev_add(struct cdev *p, dev_t dev, unsigned count) { int error; p->dev = dev; p->count = count; error = kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p); if (error) return error; kobject_get(p->kobj.parent); return 0; }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro3652.17%250.00%
Dmitry Torokhov2434.78%125.00%
Jonathan Corbet913.04%125.00%
Total69100.00%4100.00%

/** * cdev_set_parent() - set the parent kobject for a char device * @p: the cdev structure * @kobj: the kobject to take a reference to * * cdev_set_parent() sets a parent kobject which will be referenced * appropriately so the parent is not freed before the cdev. This * should be called before cdev_add. */
void cdev_set_parent(struct cdev *p, struct kobject *kobj) { WARN_ON(!kobj->state_initialized); p->kobj.parent = kobj; }

Contributors

PersonTokensPropCommitsCommitProp
Logan Gunthorpe31100.00%1100.00%
Total31100.00%1100.00%

/** * cdev_device_add() - add a char device and it's corresponding * struct device, linkink * @dev: the device structure * @cdev: the cdev structure * * cdev_device_add() adds the char device represented by @cdev to the system, * just as cdev_add does. It then adds @dev to the system using device_add * The dev_t for the char device will be taken from the struct device which * needs to be initialized first. This helper function correctly takes a * reference to the parent device so the parent will not get released until * all references to the cdev are released. * * This helper uses dev->devt for the device number. If it is not set * it will not add the cdev and it will be equivalent to device_add. * * This function should be used whenever the struct cdev and the * struct device are members of the same structure whose lifetime is * managed by the struct device. * * NOTE: Callers must assume that userspace was able to open the cdev and * can call cdev fops callbacks at any time, even if this function fails. */
int cdev_device_add(struct cdev *cdev, struct device *dev) { int rc = 0; if (dev->devt) { cdev_set_parent(cdev, &dev->kobj); rc = cdev_add(cdev, dev->devt, 1); if (rc) return rc; } rc = device_add(dev); if (rc) cdev_del(cdev); return rc; }

Contributors

PersonTokensPropCommitsCommitProp
Logan Gunthorpe77100.00%1100.00%
Total77100.00%1100.00%

/** * cdev_device_del() - inverse of cdev_device_add * @dev: the device structure * @cdev: the cdev structure * * cdev_device_del() is a helper function to call cdev_del and device_del. * It should be used whenever cdev_device_add is used. * * If dev->devt is not set it will not remove the cdev and will be equivalent * to device_del. * * NOTE: This guarantees that associated sysfs callbacks are not running * or runnable, however any cdevs already open will remain and their fops * will still be callable even after this function returns. */
void cdev_device_del(struct cdev *cdev, struct device *dev) { device_del(dev); if (dev->devt) cdev_del(cdev); }

Contributors

PersonTokensPropCommitsCommitProp
Logan Gunthorpe31100.00%1100.00%
Total31100.00%1100.00%


static void cdev_unmap(dev_t dev, unsigned count) { kobj_unmap(cdev_map, dev, count); }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro2095.24%150.00%
Jonathan Corbet14.76%150.00%
Total21100.00%2100.00%

/** * cdev_del() - remove a cdev from the system * @p: the cdev structure to be removed * * cdev_del() removes @p from the system, possibly freeing the structure * itself. * * NOTE: This guarantees that cdev device will no longer be able to be * opened, however any cdevs already open will remain and their fops will * still be callable even after cdev_del returns. */
void cdev_del(struct cdev *p) { cdev_unmap(p->dev, p->count); kobject_put(&p->kobj); }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro1862.07%266.67%
Jonathan Corbet1137.93%133.33%
Total29100.00%3100.00%


static void cdev_default_release(struct kobject *kobj) { struct cdev *p = container_of(kobj, struct cdev, kobj); struct kobject *parent = kobj->parent; cdev_purge(p); kobject_put(parent); }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro3168.89%150.00%
Dmitry Torokhov1431.11%150.00%
Total45100.00%2100.00%


static void cdev_dynamic_release(struct kobject *kobj) { struct cdev *p = container_of(kobj, struct cdev, kobj); struct kobject *parent = kobj->parent; cdev_purge(p); kfree(p); kobject_put(parent); }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro3672.00%266.67%
Dmitry Torokhov1428.00%133.33%
Total50100.00%3100.00%

static struct kobj_type ktype_cdev_default = { .release = cdev_default_release, }; static struct kobj_type ktype_cdev_dynamic = { .release = cdev_dynamic_release, }; /** * cdev_alloc() - allocate a cdev structure * * Allocates and returns a cdev structure, or NULL on failure. */
struct cdev *cdev_alloc(void) { struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL); if (p) { INIT_LIST_HEAD(&p->list); kobject_init(&p->kobj, &ktype_cdev_dynamic); } return p; }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro4890.57%240.00%
Greg Kroah-Hartman47.55%240.00%
Oliver Neukum11.89%120.00%
Total53100.00%5100.00%

/** * cdev_init() - initialize a cdev structure * @cdev: the structure to initialize * @fops: the file_operations for this device * * Initializes @cdev, remembering @fops, making it ready to add to the * system with cdev_add(). */
void cdev_init(struct cdev *cdev, const struct file_operations *fops) { memset(cdev, 0, sizeof *cdev); INIT_LIST_HEAD(&cdev->list); kobject_init(&cdev->kobj, &ktype_cdev_default); cdev->ops = fops; }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro3669.23%233.33%
Roland Dreier1121.15%116.67%
Greg Kroah-Hartman47.69%233.33%
Arjan van de Ven11.92%116.67%
Total52100.00%6100.00%


static struct kobject *base_probe(dev_t dev, int *part, void *data) { if (request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev)) > 0) /* Make old-style 2.4 aliases work */ request_module("char-major-%d", MAJOR(dev)); return NULL; }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro3057.69%125.00%
Andrew Morton2140.38%250.00%
Randy Dunlap11.92%125.00%
Total52100.00%4100.00%


void __init chrdev_init(void) { cdev_map = kobj_map_init(base_probe, &chrdevs_lock); }

Contributors

PersonTokensPropCommitsCommitProp
Al Viro1688.89%133.33%
Linus Torvalds15.56%133.33%
Greg Kroah-Hartman15.56%133.33%
Total18100.00%3100.00%

/* Let modules do char dev stuff */ EXPORT_SYMBOL(register_chrdev_region); EXPORT_SYMBOL(unregister_chrdev_region); EXPORT_SYMBOL(alloc_chrdev_region); EXPORT_SYMBOL(cdev_init); EXPORT_SYMBOL(cdev_alloc); EXPORT_SYMBOL(cdev_del); EXPORT_SYMBOL(cdev_add); EXPORT_SYMBOL(cdev_set_parent); EXPORT_SYMBOL(cdev_device_add); EXPORT_SYMBOL(cdev_device_del); EXPORT_SYMBOL(__register_chrdev); EXPORT_SYMBOL(__unregister_chrdev);

Overall Contributors

PersonTokensPropCommitsCommitProp
Al Viro120046.44%58.06%
Andrew Morton41716.14%812.90%
Logan Gunthorpe31412.15%34.84%
Brian Gerst1746.73%11.61%
Arjan van de Ven953.68%34.84%
Amos Waterland913.52%11.61%
Dmitry Torokhov522.01%11.61%
Joe Korty361.39%11.61%
Tejun Heo301.16%11.61%
Jonathan Corbet271.04%46.45%
Christoph Hellwig210.81%23.23%
Roland Dreier110.43%11.61%
Greg Kroah-Hartman100.39%46.45%
Jes Sorensen100.39%11.61%
Linus Torvalds100.39%34.84%
David Howells90.35%34.84%
Brian King90.35%11.61%
Neil Horman90.35%11.61%
Dan J Williams90.35%11.61%
Cyrill V. Gorcunov70.27%11.61%
Linus Walleij70.27%11.61%
Thomas Gleixner70.27%11.61%
Stephen Hemminger60.23%23.23%
Srivatsa S. Bhat50.19%23.23%
Arnd Bergmann50.19%11.61%
Paul Fulghum40.15%11.61%
Oliver Neukum20.08%11.61%
Adrian Bunk10.04%11.61%
Randy Dunlap10.04%11.61%
Cheng Renquan10.04%11.61%
Partha Pratim Mukherjee10.04%11.61%
Akinobu Mita10.04%11.61%
Yang Zhang10.04%11.61%
Andries E. Brouwer10.04%11.61%
Total2584100.00%62100.00%
Directory: fs
Information contained on this website is for historical information purposes only and does not indicate or represent copyright ownership.
Created with cregit.