cachepc-linux

Fork of AMDESE/linux with modifications for CachePC side-channel attack
git clone https://git.sinitax.com/sinitax/cachepc-linux
Log | Files | Refs | README | LICENSE | sfeed.txt

resctrlfs.c (16061B)


      1// SPDX-License-Identifier: GPL-2.0
      2/*
      3 * Basic resctrl file system operations
      4 *
      5 * Copyright (C) 2018 Intel Corporation
      6 *
      7 * Authors:
      8 *    Sai Praneeth Prakhya <sai.praneeth.prakhya@intel.com>,
      9 *    Fenghua Yu <fenghua.yu@intel.com>
     10 */
     11#include "resctrl.h"
     12
     13static int find_resctrl_mount(char *buffer)
     14{
     15	FILE *mounts;
     16	char line[256], *fs, *mntpoint;
     17
     18	mounts = fopen("/proc/mounts", "r");
     19	if (!mounts) {
     20		perror("/proc/mounts");
     21		return -ENXIO;
     22	}
     23	while (!feof(mounts)) {
     24		if (!fgets(line, 256, mounts))
     25			break;
     26		fs = strtok(line, " \t");
     27		if (!fs)
     28			continue;
     29		mntpoint = strtok(NULL, " \t");
     30		if (!mntpoint)
     31			continue;
     32		fs = strtok(NULL, " \t");
     33		if (!fs)
     34			continue;
     35		if (strcmp(fs, "resctrl"))
     36			continue;
     37
     38		fclose(mounts);
     39		if (buffer)
     40			strncpy(buffer, mntpoint, 256);
     41
     42		return 0;
     43	}
     44
     45	fclose(mounts);
     46
     47	return -ENOENT;
     48}
     49
     50/*
     51 * remount_resctrlfs - Remount resctrl FS at /sys/fs/resctrl
     52 * @mum_resctrlfs:	Should the resctrl FS be remounted?
     53 *
     54 * If not mounted, mount it.
     55 * If mounted and mum_resctrlfs then remount resctrl FS.
     56 * If mounted and !mum_resctrlfs then noop
     57 *
     58 * Return: 0 on success, non-zero on failure
     59 */
     60int remount_resctrlfs(bool mum_resctrlfs)
     61{
     62	char mountpoint[256];
     63	int ret;
     64
     65	ret = find_resctrl_mount(mountpoint);
     66	if (ret)
     67		strcpy(mountpoint, RESCTRL_PATH);
     68
     69	if (!ret && mum_resctrlfs && umount(mountpoint))
     70		ksft_print_msg("Fail: unmounting \"%s\"\n", mountpoint);
     71
     72	if (!ret && !mum_resctrlfs)
     73		return 0;
     74
     75	ksft_print_msg("Mounting resctrl to \"%s\"\n", RESCTRL_PATH);
     76	ret = mount("resctrl", RESCTRL_PATH, "resctrl", 0, NULL);
     77	if (ret)
     78		perror("# mount");
     79
     80	return ret;
     81}
     82
     83int umount_resctrlfs(void)
     84{
     85	if (find_resctrl_mount(NULL))
     86		return 0;
     87
     88	if (umount(RESCTRL_PATH)) {
     89		perror("# Unable to umount resctrl");
     90
     91		return errno;
     92	}
     93
     94	return 0;
     95}
     96
     97/*
     98 * get_resource_id - Get socket number/l3 id for a specified CPU
     99 * @cpu_no:	CPU number
    100 * @resource_id: Socket number or l3_id
    101 *
    102 * Return: >= 0 on success, < 0 on failure.
    103 */
    104int get_resource_id(int cpu_no, int *resource_id)
    105{
    106	char phys_pkg_path[1024];
    107	FILE *fp;
    108
    109	if (get_vendor() == ARCH_AMD)
    110		sprintf(phys_pkg_path, "%s%d/cache/index3/id",
    111			PHYS_ID_PATH, cpu_no);
    112	else
    113		sprintf(phys_pkg_path, "%s%d/topology/physical_package_id",
    114			PHYS_ID_PATH, cpu_no);
    115
    116	fp = fopen(phys_pkg_path, "r");
    117	if (!fp) {
    118		perror("Failed to open physical_package_id");
    119
    120		return -1;
    121	}
    122	if (fscanf(fp, "%d", resource_id) <= 0) {
    123		perror("Could not get socket number or l3 id");
    124		fclose(fp);
    125
    126		return -1;
    127	}
    128	fclose(fp);
    129
    130	return 0;
    131}
    132
    133/*
    134 * get_cache_size - Get cache size for a specified CPU
    135 * @cpu_no:	CPU number
    136 * @cache_type:	Cache level L2/L3
    137 * @cache_size:	pointer to cache_size
    138 *
    139 * Return: = 0 on success, < 0 on failure.
    140 */
    141int get_cache_size(int cpu_no, char *cache_type, unsigned long *cache_size)
    142{
    143	char cache_path[1024], cache_str[64];
    144	int length, i, cache_num;
    145	FILE *fp;
    146
    147	if (!strcmp(cache_type, "L3")) {
    148		cache_num = 3;
    149	} else if (!strcmp(cache_type, "L2")) {
    150		cache_num = 2;
    151	} else {
    152		perror("Invalid cache level");
    153		return -1;
    154	}
    155
    156	sprintf(cache_path, "/sys/bus/cpu/devices/cpu%d/cache/index%d/size",
    157		cpu_no, cache_num);
    158	fp = fopen(cache_path, "r");
    159	if (!fp) {
    160		perror("Failed to open cache size");
    161
    162		return -1;
    163	}
    164	if (fscanf(fp, "%s", cache_str) <= 0) {
    165		perror("Could not get cache_size");
    166		fclose(fp);
    167
    168		return -1;
    169	}
    170	fclose(fp);
    171
    172	length = (int)strlen(cache_str);
    173
    174	*cache_size = 0;
    175
    176	for (i = 0; i < length; i++) {
    177		if ((cache_str[i] >= '0') && (cache_str[i] <= '9'))
    178
    179			*cache_size = *cache_size * 10 + (cache_str[i] - '0');
    180
    181		else if (cache_str[i] == 'K')
    182
    183			*cache_size = *cache_size * 1024;
    184
    185		else if (cache_str[i] == 'M')
    186
    187			*cache_size = *cache_size * 1024 * 1024;
    188
    189		else
    190			break;
    191	}
    192
    193	return 0;
    194}
    195
    196#define CORE_SIBLINGS_PATH	"/sys/bus/cpu/devices/cpu"
    197
    198/*
    199 * get_cbm_mask - Get cbm mask for given cache
    200 * @cache_type:	Cache level L2/L3
    201 * @cbm_mask:	cbm_mask returned as a string
    202 *
    203 * Return: = 0 on success, < 0 on failure.
    204 */
    205int get_cbm_mask(char *cache_type, char *cbm_mask)
    206{
    207	char cbm_mask_path[1024];
    208	FILE *fp;
    209
    210	if (!cbm_mask)
    211		return -1;
    212
    213	sprintf(cbm_mask_path, "%s/%s/cbm_mask", CBM_MASK_PATH, cache_type);
    214
    215	fp = fopen(cbm_mask_path, "r");
    216	if (!fp) {
    217		perror("Failed to open cache level");
    218
    219		return -1;
    220	}
    221	if (fscanf(fp, "%s", cbm_mask) <= 0) {
    222		perror("Could not get max cbm_mask");
    223		fclose(fp);
    224
    225		return -1;
    226	}
    227	fclose(fp);
    228
    229	return 0;
    230}
    231
    232/*
    233 * get_core_sibling - Get sibling core id from the same socket for given CPU
    234 * @cpu_no:	CPU number
    235 *
    236 * Return:	> 0 on success, < 0 on failure.
    237 */
    238int get_core_sibling(int cpu_no)
    239{
    240	char core_siblings_path[1024], cpu_list_str[64];
    241	int sibling_cpu_no = -1;
    242	FILE *fp;
    243
    244	sprintf(core_siblings_path, "%s%d/topology/core_siblings_list",
    245		CORE_SIBLINGS_PATH, cpu_no);
    246
    247	fp = fopen(core_siblings_path, "r");
    248	if (!fp) {
    249		perror("Failed to open core siblings path");
    250
    251		return -1;
    252	}
    253	if (fscanf(fp, "%s", cpu_list_str) <= 0) {
    254		perror("Could not get core_siblings list");
    255		fclose(fp);
    256
    257		return -1;
    258	}
    259	fclose(fp);
    260
    261	char *token = strtok(cpu_list_str, "-,");
    262
    263	while (token) {
    264		sibling_cpu_no = atoi(token);
    265		/* Skipping core 0 as we don't want to run test on core 0 */
    266		if (sibling_cpu_no != 0 && sibling_cpu_no != cpu_no)
    267			break;
    268		token = strtok(NULL, "-,");
    269	}
    270
    271	return sibling_cpu_no;
    272}
    273
    274/*
    275 * taskset_benchmark - Taskset PID (i.e. benchmark) to a specified cpu
    276 * @bm_pid:	PID that should be binded
    277 * @cpu_no:	CPU number at which the PID would be binded
    278 *
    279 * Return: 0 on success, non-zero on failure
    280 */
    281int taskset_benchmark(pid_t bm_pid, int cpu_no)
    282{
    283	cpu_set_t my_set;
    284
    285	CPU_ZERO(&my_set);
    286	CPU_SET(cpu_no, &my_set);
    287
    288	if (sched_setaffinity(bm_pid, sizeof(cpu_set_t), &my_set)) {
    289		perror("Unable to taskset benchmark");
    290
    291		return -1;
    292	}
    293
    294	return 0;
    295}
    296
    297/*
    298 * run_benchmark - Run a specified benchmark or fill_buf (default benchmark)
    299 *		   in specified signal. Direct benchmark stdio to /dev/null.
    300 * @signum:	signal number
    301 * @info:	signal info
    302 * @ucontext:	user context in signal handling
    303 *
    304 * Return: void
    305 */
    306void run_benchmark(int signum, siginfo_t *info, void *ucontext)
    307{
    308	int operation, ret, malloc_and_init_memory, memflush;
    309	unsigned long span, buffer_span;
    310	char **benchmark_cmd;
    311	char resctrl_val[64];
    312	FILE *fp;
    313
    314	benchmark_cmd = info->si_ptr;
    315
    316	/*
    317	 * Direct stdio of child to /dev/null, so that only parent writes to
    318	 * stdio (console)
    319	 */
    320	fp = freopen("/dev/null", "w", stdout);
    321	if (!fp)
    322		PARENT_EXIT("Unable to direct benchmark status to /dev/null");
    323
    324	if (strcmp(benchmark_cmd[0], "fill_buf") == 0) {
    325		/* Execute default fill_buf benchmark */
    326		span = strtoul(benchmark_cmd[1], NULL, 10);
    327		malloc_and_init_memory = atoi(benchmark_cmd[2]);
    328		memflush =  atoi(benchmark_cmd[3]);
    329		operation = atoi(benchmark_cmd[4]);
    330		sprintf(resctrl_val, "%s", benchmark_cmd[5]);
    331
    332		if (strncmp(resctrl_val, CMT_STR, sizeof(CMT_STR)))
    333			buffer_span = span * MB;
    334		else
    335			buffer_span = span;
    336
    337		if (run_fill_buf(buffer_span, malloc_and_init_memory, memflush,
    338				 operation, resctrl_val))
    339			fprintf(stderr, "Error in running fill buffer\n");
    340	} else {
    341		/* Execute specified benchmark */
    342		ret = execvp(benchmark_cmd[0], benchmark_cmd);
    343		if (ret)
    344			perror("wrong\n");
    345	}
    346
    347	fclose(stdout);
    348	PARENT_EXIT("Unable to run specified benchmark");
    349}
    350
    351/*
    352 * create_grp - Create a group only if one doesn't exist
    353 * @grp_name:	Name of the group
    354 * @grp:	Full path and name of the group
    355 * @parent_grp:	Full path and name of the parent group
    356 *
    357 * Return: 0 on success, non-zero on failure
    358 */
    359static int create_grp(const char *grp_name, char *grp, const char *parent_grp)
    360{
    361	int found_grp = 0;
    362	struct dirent *ep;
    363	DIR *dp;
    364
    365	/*
    366	 * At this point, we are guaranteed to have resctrl FS mounted and if
    367	 * length of grp_name == 0, it means, user wants to use root con_mon
    368	 * grp, so do nothing
    369	 */
    370	if (strlen(grp_name) == 0)
    371		return 0;
    372
    373	/* Check if requested grp exists or not */
    374	dp = opendir(parent_grp);
    375	if (dp) {
    376		while ((ep = readdir(dp)) != NULL) {
    377			if (strcmp(ep->d_name, grp_name) == 0)
    378				found_grp = 1;
    379		}
    380		closedir(dp);
    381	} else {
    382		perror("Unable to open resctrl for group");
    383
    384		return -1;
    385	}
    386
    387	/* Requested grp doesn't exist, hence create it */
    388	if (found_grp == 0) {
    389		if (mkdir(grp, 0) == -1) {
    390			perror("Unable to create group");
    391
    392			return -1;
    393		}
    394	}
    395
    396	return 0;
    397}
    398
    399static int write_pid_to_tasks(char *tasks, pid_t pid)
    400{
    401	FILE *fp;
    402
    403	fp = fopen(tasks, "w");
    404	if (!fp) {
    405		perror("Failed to open tasks file");
    406
    407		return -1;
    408	}
    409	if (fprintf(fp, "%d\n", pid) < 0) {
    410		perror("Failed to wr pid to tasks file");
    411		fclose(fp);
    412
    413		return -1;
    414	}
    415	fclose(fp);
    416
    417	return 0;
    418}
    419
    420/*
    421 * write_bm_pid_to_resctrl - Write a PID (i.e. benchmark) to resctrl FS
    422 * @bm_pid:		PID that should be written
    423 * @ctrlgrp:		Name of the control monitor group (con_mon grp)
    424 * @mongrp:		Name of the monitor group (mon grp)
    425 * @resctrl_val:	Resctrl feature (Eg: mbm, mba.. etc)
    426 *
    427 * If a con_mon grp is requested, create it and write pid to it, otherwise
    428 * write pid to root con_mon grp.
    429 * If a mon grp is requested, create it and write pid to it, otherwise
    430 * pid is not written, this means that pid is in con_mon grp and hence
    431 * should consult con_mon grp's mon_data directory for results.
    432 *
    433 * Return: 0 on success, non-zero on failure
    434 */
    435int write_bm_pid_to_resctrl(pid_t bm_pid, char *ctrlgrp, char *mongrp,
    436			    char *resctrl_val)
    437{
    438	char controlgroup[128], monitorgroup[512], monitorgroup_p[256];
    439	char tasks[1024];
    440	int ret = 0;
    441
    442	if (strlen(ctrlgrp))
    443		sprintf(controlgroup, "%s/%s", RESCTRL_PATH, ctrlgrp);
    444	else
    445		sprintf(controlgroup, "%s", RESCTRL_PATH);
    446
    447	/* Create control and monitoring group and write pid into it */
    448	ret = create_grp(ctrlgrp, controlgroup, RESCTRL_PATH);
    449	if (ret)
    450		goto out;
    451	sprintf(tasks, "%s/tasks", controlgroup);
    452	ret = write_pid_to_tasks(tasks, bm_pid);
    453	if (ret)
    454		goto out;
    455
    456	/* Create mon grp and write pid into it for "mbm" and "cmt" test */
    457	if (!strncmp(resctrl_val, CMT_STR, sizeof(CMT_STR)) ||
    458	    !strncmp(resctrl_val, MBM_STR, sizeof(MBM_STR))) {
    459		if (strlen(mongrp)) {
    460			sprintf(monitorgroup_p, "%s/mon_groups", controlgroup);
    461			sprintf(monitorgroup, "%s/%s", monitorgroup_p, mongrp);
    462			ret = create_grp(mongrp, monitorgroup, monitorgroup_p);
    463			if (ret)
    464				goto out;
    465
    466			sprintf(tasks, "%s/mon_groups/%s/tasks",
    467				controlgroup, mongrp);
    468			ret = write_pid_to_tasks(tasks, bm_pid);
    469			if (ret)
    470				goto out;
    471		}
    472	}
    473
    474out:
    475	ksft_print_msg("Writing benchmark parameters to resctrl FS\n");
    476	if (ret)
    477		perror("# writing to resctrlfs");
    478
    479	return ret;
    480}
    481
    482/*
    483 * write_schemata - Update schemata of a con_mon grp
    484 * @ctrlgrp:		Name of the con_mon grp
    485 * @schemata:		Schemata that should be updated to
    486 * @cpu_no:		CPU number that the benchmark PID is binded to
    487 * @resctrl_val:	Resctrl feature (Eg: mbm, mba.. etc)
    488 *
    489 * Update schemata of a con_mon grp *only* if requested resctrl feature is
    490 * allocation type
    491 *
    492 * Return: 0 on success, non-zero on failure
    493 */
    494int write_schemata(char *ctrlgrp, char *schemata, int cpu_no, char *resctrl_val)
    495{
    496	char controlgroup[1024], schema[1024], reason[64];
    497	int resource_id, ret = 0;
    498	FILE *fp;
    499
    500	if (strncmp(resctrl_val, MBA_STR, sizeof(MBA_STR)) &&
    501	    strncmp(resctrl_val, CAT_STR, sizeof(CAT_STR)) &&
    502	    strncmp(resctrl_val, CMT_STR, sizeof(CMT_STR)))
    503		return -ENOENT;
    504
    505	if (!schemata) {
    506		ksft_print_msg("Skipping empty schemata update\n");
    507
    508		return -1;
    509	}
    510
    511	if (get_resource_id(cpu_no, &resource_id) < 0) {
    512		sprintf(reason, "Failed to get resource id");
    513		ret = -1;
    514
    515		goto out;
    516	}
    517
    518	if (strlen(ctrlgrp) != 0)
    519		sprintf(controlgroup, "%s/%s/schemata", RESCTRL_PATH, ctrlgrp);
    520	else
    521		sprintf(controlgroup, "%s/schemata", RESCTRL_PATH);
    522
    523	if (!strncmp(resctrl_val, CAT_STR, sizeof(CAT_STR)) ||
    524	    !strncmp(resctrl_val, CMT_STR, sizeof(CMT_STR)))
    525		sprintf(schema, "%s%d%c%s", "L3:", resource_id, '=', schemata);
    526	if (!strncmp(resctrl_val, MBA_STR, sizeof(MBA_STR)))
    527		sprintf(schema, "%s%d%c%s", "MB:", resource_id, '=', schemata);
    528
    529	fp = fopen(controlgroup, "w");
    530	if (!fp) {
    531		sprintf(reason, "Failed to open control group");
    532		ret = -1;
    533
    534		goto out;
    535	}
    536
    537	if (fprintf(fp, "%s\n", schema) < 0) {
    538		sprintf(reason, "Failed to write schemata in control group");
    539		fclose(fp);
    540		ret = -1;
    541
    542		goto out;
    543	}
    544	fclose(fp);
    545
    546out:
    547	ksft_print_msg("Write schema \"%s\" to resctrl FS%s%s\n",
    548		       schema, ret ? " # " : "",
    549		       ret ? reason : "");
    550
    551	return ret;
    552}
    553
    554bool check_resctrlfs_support(void)
    555{
    556	FILE *inf = fopen("/proc/filesystems", "r");
    557	DIR *dp;
    558	char *res;
    559	bool ret = false;
    560
    561	if (!inf)
    562		return false;
    563
    564	res = fgrep(inf, "nodev\tresctrl\n");
    565
    566	if (res) {
    567		ret = true;
    568		free(res);
    569	}
    570
    571	fclose(inf);
    572
    573	ksft_print_msg("%s Check kernel supports resctrl filesystem\n",
    574		       ret ? "Pass:" : "Fail:");
    575
    576	if (!ret)
    577		return ret;
    578
    579	dp = opendir(RESCTRL_PATH);
    580	ksft_print_msg("%s Check resctrl mountpoint \"%s\" exists\n",
    581		       dp ? "Pass:" : "Fail:", RESCTRL_PATH);
    582	if (dp)
    583		closedir(dp);
    584
    585	ksft_print_msg("resctrl filesystem %s mounted\n",
    586		       find_resctrl_mount(NULL) ? "not" : "is");
    587
    588	return ret;
    589}
    590
    591char *fgrep(FILE *inf, const char *str)
    592{
    593	char line[256];
    594	int slen = strlen(str);
    595
    596	while (!feof(inf)) {
    597		if (!fgets(line, 256, inf))
    598			break;
    599		if (strncmp(line, str, slen))
    600			continue;
    601
    602		return strdup(line);
    603	}
    604
    605	return NULL;
    606}
    607
    608/*
    609 * validate_resctrl_feature_request - Check if requested feature is valid.
    610 * @resctrl_val:	Requested feature
    611 *
    612 * Return: True if the feature is supported, else false
    613 */
    614bool validate_resctrl_feature_request(const char *resctrl_val)
    615{
    616	struct stat statbuf;
    617	bool found = false;
    618	char *res;
    619	FILE *inf;
    620
    621	if (!resctrl_val)
    622		return false;
    623
    624	if (remount_resctrlfs(false))
    625		return false;
    626
    627	if (!strncmp(resctrl_val, CAT_STR, sizeof(CAT_STR))) {
    628		if (!stat(L3_PATH, &statbuf))
    629			return true;
    630	} else if (!strncmp(resctrl_val, MBA_STR, sizeof(MBA_STR))) {
    631		if (!stat(MB_PATH, &statbuf))
    632			return true;
    633	} else if (!strncmp(resctrl_val, MBM_STR, sizeof(MBM_STR)) ||
    634		   !strncmp(resctrl_val, CMT_STR, sizeof(CMT_STR))) {
    635		if (!stat(L3_MON_PATH, &statbuf)) {
    636			inf = fopen(L3_MON_FEATURES_PATH, "r");
    637			if (!inf)
    638				return false;
    639
    640			if (!strncmp(resctrl_val, CMT_STR, sizeof(CMT_STR))) {
    641				res = fgrep(inf, "llc_occupancy");
    642				if (res) {
    643					found = true;
    644					free(res);
    645				}
    646			}
    647
    648			if (!strncmp(resctrl_val, MBM_STR, sizeof(MBM_STR))) {
    649				res = fgrep(inf, "mbm_total_bytes");
    650				if (res) {
    651					free(res);
    652					res = fgrep(inf, "mbm_local_bytes");
    653					if (res) {
    654						found = true;
    655						free(res);
    656					}
    657				}
    658			}
    659			fclose(inf);
    660		}
    661	}
    662
    663	return found;
    664}
    665
    666int filter_dmesg(void)
    667{
    668	char line[1024];
    669	FILE *fp;
    670	int pipefds[2];
    671	pid_t pid;
    672	int ret;
    673
    674	ret = pipe(pipefds);
    675	if (ret) {
    676		perror("pipe");
    677		return ret;
    678	}
    679	pid = fork();
    680	if (pid == 0) {
    681		close(pipefds[0]);
    682		dup2(pipefds[1], STDOUT_FILENO);
    683		execlp("dmesg", "dmesg", NULL);
    684		perror("executing dmesg");
    685		exit(1);
    686	}
    687	close(pipefds[1]);
    688	fp = fdopen(pipefds[0], "r");
    689	if (!fp) {
    690		perror("fdopen(pipe)");
    691		kill(pid, SIGTERM);
    692
    693		return -1;
    694	}
    695
    696	while (fgets(line, 1024, fp)) {
    697		if (strstr(line, "intel_rdt:"))
    698			ksft_print_msg("dmesg: %s", line);
    699		if (strstr(line, "resctrl:"))
    700			ksft_print_msg("dmesg: %s", line);
    701	}
    702	fclose(fp);
    703	waitpid(pid, NULL, 0);
    704
    705	return 0;
    706}
    707
    708int validate_bw_report_request(char *bw_report)
    709{
    710	if (strcmp(bw_report, "reads") == 0)
    711		return 0;
    712	if (strcmp(bw_report, "writes") == 0)
    713		return 0;
    714	if (strcmp(bw_report, "nt-writes") == 0) {
    715		strcpy(bw_report, "writes");
    716		return 0;
    717	}
    718	if (strcmp(bw_report, "total") == 0)
    719		return 0;
    720
    721	fprintf(stderr, "Requested iMC B/W report type unavailable\n");
    722
    723	return -1;
    724}
    725
    726int perf_event_open(struct perf_event_attr *hw_event, pid_t pid, int cpu,
    727		    int group_fd, unsigned long flags)
    728{
    729	int ret;
    730
    731	ret = syscall(__NR_perf_event_open, hw_event, pid, cpu,
    732		      group_fd, flags);
    733	return ret;
    734}
    735
    736unsigned int count_bits(unsigned long n)
    737{
    738	unsigned int count = 0;
    739
    740	while (n) {
    741		count += n & 1;
    742		n >>= 1;
    743	}
    744
    745	return count;
    746}