2 * fstrim.c -- discard the part (or whole) of mounted filesystem.
4 * Copyright (C) 2010 Red Hat, Inc. All rights reserved.
5 * Written by Lukas Czerner <lczerner@redhat.com>
6 * Karel Zak <kzak@redhat.com>
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <http://www.gnu.org/licenses/>.
22 * This program uses FITRIM ioctl to discard parts or the whole filesystem
23 * online (mounted). You can specify range (start and length) to be
24 * discarded, or simply discard whole filesystem.
36 #include <sys/ioctl.h>
45 #include "closestream.h"
46 #include "pathnames.h"
49 #include "statfs_magic.h"
60 #define FITRIM _IOWR('X', 121, struct fstrim_range)
63 struct fstrim_control
{
64 struct fstrim_range range
;
67 unsigned int verbose
: 1,
72 static int is_directory(const char *path
, int silent
)
76 if (stat(path
, &sb
) == -1) {
78 warn(_("stat of %s failed"), path
);
81 if (!S_ISDIR(sb
.st_mode
)) {
83 warnx(_("%s: not a directory"), path
);
89 /* returns: 0 = success, 1 = unsupported, < 0 = error */
90 static int fstrim_filesystem(struct fstrim_control
*ctl
, const char *path
, const char *devname
)
93 struct fstrim_range range
;
94 char *rpath
= realpath(path
, NULL
);
97 warn(_("cannot get realpath: %s"), path
);
101 /* kernel modifies the range */
102 memcpy(&range
, &ctl
->range
, sizeof(range
));
104 fd
= open(rpath
, O_RDONLY
);
106 warn(_("cannot open %s"), path
);
113 printf(_("%s: 0 B (dry run) trimmed on %s\n"), path
, devname
);
115 printf(_("%s: 0 B (dry run) trimmed\n"), path
);
121 if (ioctl(fd
, FITRIM
, &range
)) {
133 warn(_("%s: FITRIM ioctl failed"), path
);
138 char *str
= size_to_human_string(
139 SIZE_SUFFIX_3LETTER
| SIZE_SUFFIX_SPACE
,
140 (uint64_t) range
.len
);
142 /* TRANSLATORS: The standard value here is a very large number. */
143 printf(_("%s: %s (%" PRIu64
" bytes) trimmed on %s\n"),
144 path
, str
, (uint64_t) range
.len
, devname
);
146 /* TRANSLATORS: The standard value here is a very large number. */
147 printf(_("%s: %s (%" PRIu64
" bytes) trimmed\n"),
148 path
, str
, (uint64_t) range
.len
);
161 static int has_discard(const char *devname
, struct path_cxt
**wholedisk
)
163 struct path_cxt
*pc
= NULL
;
166 int rc
= -1, rdonly
= 0;
168 dev
= sysfs_devname_to_devno(devname
);
172 pc
= ul_new_sysfs_path(dev
, NULL
, NULL
);
177 * This is tricky to read the info from sys/, because the queue
178 * attributes are provided for whole devices (disk) only. We're trying
179 * to reuse the whole-disk sysfs context to optimize this stuff (as
180 * system usually have just one disk only).
182 rc
= sysfs_blkdev_get_wholedisk(pc
, NULL
, 0, &disk
);
183 if (rc
!= 0 || !disk
)
187 /* Partition, try reuse whole-disk context if valid for the
188 * current device, otherwise create new context for the
191 if (*wholedisk
&& sysfs_blkdev_get_devno(*wholedisk
) != disk
) {
192 ul_unref_path(*wholedisk
);
196 *wholedisk
= ul_new_sysfs_path(disk
, NULL
, NULL
);
200 sysfs_blkdev_set_parent(pc
, *wholedisk
);
203 rc
= ul_path_read_u64(pc
, &dg
, "queue/discard_granularity");
205 ul_path_scanf(pc
, "ro", "%d", &rdonly
);
208 return rc
== 0 && dg
> 0 && rdonly
== 0;
214 static int is_unwanted_fs(struct libmnt_fs
*fs
, const char *tgt
, const char *types
)
219 if (mnt_fs_is_pseudofs(fs
))
221 if (mnt_fs_is_netfs(fs
))
223 if (mnt_fs_is_swaparea(fs
))
225 if (mnt_fs_match_fstype(fs
, "autofs"))
227 if (mnt_fs_match_options(fs
, "ro"))
229 if (mnt_fs_match_options(fs
, "+X-fstrim.notrim"))
231 if (types
&& mnt_fs_match_fstype(fs
, types
) == 0)
234 fd
= open(tgt
, O_PATH
);
237 rc
= fstatfs(fd
, &vfs
) != 0 || vfs
.f_type
== STATFS_AUTOFS_MAGIC
;
242 /* FITRIM on read-only filesystem can fail, and it can fail */
243 if (access(tgt
, W_OK
) != 0) {
252 static int uniq_fs_target_cmp(
253 struct libmnt_table
*tb
__attribute__((__unused__
)),
257 return !mnt_fs_streq_target(a
, mnt_fs_get_target(b
));
260 static int uniq_fs_source_cmp(
261 struct libmnt_table
*tb
__attribute__((__unused__
)),
265 if (mnt_fs_is_pseudofs(a
) || mnt_fs_is_netfs(a
) ||
266 mnt_fs_is_pseudofs(b
) || mnt_fs_is_netfs(b
))
269 return !mnt_fs_streq_srcpath(a
, mnt_fs_get_srcpath(b
));
276 * 64 = some failed, some success
278 static int fstrim_all_from_file(struct fstrim_control
*ctl
, const char *filename
)
280 struct libmnt_fs
*fs
;
281 struct libmnt_iter
*itr
;
282 struct libmnt_table
*tab
;
283 struct libmnt_cache
*cache
= NULL
;
284 struct path_cxt
*wholedisk
= NULL
;
285 int cnt
= 0, cnt_err
= 0;
288 tab
= mnt_new_table_from_file(filename
);
290 err(MNT_EX_FAIL
, _("failed to parse %s"), filename
);
292 if (mnt_table_is_empty(tab
)) {
293 mnt_unref_table(tab
);
297 if (streq_paths(filename
, "/etc/fstab"))
300 /* de-duplicate by mountpoints */
301 mnt_table_uniq_fs(tab
, 0, uniq_fs_target_cmp
);
304 char *rootdev
= NULL
;
306 cache
= mnt_new_cache();
308 err(MNT_EX_FAIL
, _("failed to initialize libmount cache"));
310 /* Make sure we trim also root FS on fstab */
311 if (mnt_table_find_target(tab
, "/", MNT_ITER_FORWARD
) == NULL
&&
312 mnt_guess_system_root(0, cache
, &rootdev
) == 0) {
316 err(MNT_EX_FAIL
, _("failed to allocate FS handler"));
317 mnt_fs_set_target(fs
, "/");
318 mnt_fs_set_source(fs
, rootdev
);
319 mnt_fs_set_fstype(fs
, "auto");
320 mnt_table_add_fs(tab
, fs
);
327 itr
= mnt_new_iter(MNT_ITER_BACKWARD
);
329 err(MNT_EX_FAIL
, _("failed to initialize libmount iterator"));
331 /* Remove useless entries and canonicalize the table */
332 while (mnt_table_next_fs(tab
, itr
, &fs
) == 0) {
333 const char *src
= mnt_fs_get_srcpath(fs
),
334 *tgt
= mnt_fs_get_target(fs
);
338 if (!tgt
|| is_unwanted_fs(fs
, tgt
, ctl
->type_pattern
)) {
339 mnt_table_remove_fs(tab
, fs
);
343 /* convert LABEL= (etc.) from fstab to paths */
345 const char *spec
= mnt_fs_get_source(fs
);
348 mnt_table_remove_fs(tab
, fs
);
351 src
= mnt_resolve_spec(spec
, cache
);
352 mnt_fs_set_source(fs
, src
);
355 if (!src
|| *src
!= '/') {
356 mnt_table_remove_fs(tab
, fs
);
360 /* Is it really accessible mountpoint? Not all mountpoints are
361 * accessible (maybe over mounted by another filesystem) */
362 path
= mnt_get_mountpoint(tgt
);
363 if (path
&& streq_paths(path
, tgt
))
367 mnt_table_remove_fs(tab
, fs
);
368 continue; /* overlaying mount */
371 if (!is_directory(tgt
, 1) ||
372 !has_discard(src
, &wholedisk
)) {
373 mnt_table_remove_fs(tab
, fs
);
378 /* de-duplicate by source */
379 mnt_table_uniq_fs(tab
, MNT_UNIQ_FORWARD
, uniq_fs_source_cmp
);
381 mnt_reset_iter(itr
, MNT_ITER_BACKWARD
);
384 while (mnt_table_next_fs(tab
, itr
, &fs
) == 0) {
385 const char *src
= mnt_fs_get_srcpath(fs
),
386 *tgt
= mnt_fs_get_target(fs
);
392 * We're able to detect that the device supports discard, but
393 * things also depend on filesystem or device mapping, for
394 * example LUKS (by default) does not support FSTRIM.
396 * This is reason why we ignore EOPNOTSUPP and ENOTTY errors
397 * from discard ioctl.
399 rc
= fstrim_filesystem(ctl
, tgt
, src
);
402 else if (rc
== 1 && !ctl
->quiet_unsupp
)
403 warnx(_("%s: the discard operation is not supported"), tgt
);
407 ul_unref_path(wholedisk
);
408 mnt_unref_table(tab
);
409 mnt_unref_cache(cache
);
411 if (cnt
&& cnt
== cnt_err
)
412 return MNT_EX_FAIL
; /* all failed */
414 return MNT_EX_SOMEOK
; /* some ok */
416 return MNT_EX_SUCCESS
;
420 * fstrim --all follows "mount -a" return codes:
424 * 64 = some failed, some success
426 static int fstrim_all(struct fstrim_control
*ctl
, const char *tabs
)
428 char *list
= xstrdup(tabs
);
430 int rc
= MNT_EX_FAIL
;
433 ul_path_init_debug();
435 for (file
= strtok(list
, ":"); file
; file
= strtok(NULL
, ":")) {
438 if (stat(file
, &st
) < 0 || !S_ISREG(st
.st_mode
))
441 rc
= fstrim_all_from_file(ctl
, file
);
443 break; /* stop after first non-empty file */
449 static void __attribute__((__noreturn__
)) usage(void)
452 fputs(USAGE_HEADER
, out
);
454 _(" %s [options] <mount point>\n"), program_invocation_short_name
);
456 fputs(USAGE_SEPARATOR
, out
);
457 fputs(_("Discard unused blocks on a mounted filesystem.\n"), out
);
459 fputs(USAGE_OPTIONS
, out
);
460 fputs(_(" -a, --all trim mounted filesystems\n"), out
);
461 fputs(_(" -A, --fstab trim filesystems from /etc/fstab\n"), out
);
462 fputs(_(" -I, --listed-in <list> trim filesystems listed in specified files\n"), out
);
463 fputs(_(" -o, --offset <num> the offset in bytes to start discarding from\n"), out
);
464 fputs(_(" -l, --length <num> the number of bytes to discard\n"), out
);
465 fputs(_(" -m, --minimum <num> the minimum extent length to discard\n"), out
);
466 fputs(_(" -t, --types <list> limit the set of filesystem types\n"), out
);
467 fputs(_(" -v, --verbose print number of discarded bytes\n"), out
);
468 fputs(_(" --quiet-unsupported suppress error messages if trim unsupported\n"), out
);
469 fputs(_(" -n, --dry-run does everything, but trim\n"), out
);
471 fputs(USAGE_SEPARATOR
, out
);
472 fprintf(out
, USAGE_HELP_OPTIONS(21));
474 fputs(USAGE_ARGUMENTS
, out
);
475 fprintf(out
, USAGE_ARG_SIZE(_("<num>")));
477 fprintf(out
, USAGE_MAN_TAIL("fstrim(8)"));
481 int main(int argc
, char **argv
)
486 struct fstrim_control ctl
= {
487 .range
= { .len
= ULLONG_MAX
}
490 OPT_QUIET_UNSUPP
= CHAR_MAX
+ 1
493 static const struct option longopts
[] = {
494 { "all", no_argument
, NULL
, 'a' },
495 { "fstab", no_argument
, NULL
, 'A' },
496 { "help", no_argument
, NULL
, 'h' },
497 { "listed-in", required_argument
, NULL
, 'I' },
498 { "version", no_argument
, NULL
, 'V' },
499 { "offset", required_argument
, NULL
, 'o' },
500 { "length", required_argument
, NULL
, 'l' },
501 { "minimum", required_argument
, NULL
, 'm' },
502 { "types", required_argument
, NULL
, 't' },
503 { "verbose", no_argument
, NULL
, 'v' },
504 { "quiet-unsupported", no_argument
, NULL
, OPT_QUIET_UNSUPP
},
505 { "dry-run", no_argument
, NULL
, 'n' },
509 static const ul_excl_t excl
[] = { /* rows and cols in ASCII order */
513 int excl_st
[ARRAY_SIZE(excl
)] = UL_EXCL_STATUS_INIT
;
515 setlocale(LC_ALL
, "");
516 bindtextdomain(PACKAGE
, LOCALEDIR
);
518 close_stdout_atexit();
520 while ((c
= getopt_long(argc
, argv
, "AahI:l:m:no:t:Vv", longopts
, NULL
)) != -1) {
522 err_exclusive_options(c
, longopts
, excl
, excl_st
);
527 tabs
= _PATH_MNTTAB
; /* fstab */
531 tabs
= _PATH_PROC_MOUNTINFO
; /* mountinfo */
541 ctl
.range
.len
= strtosize_or_err(optarg
,
542 _("failed to parse length"));
545 ctl
.range
.start
= strtosize_or_err(optarg
,
546 _("failed to parse offset"));
549 ctl
.range
.minlen
= strtosize_or_err(optarg
,
550 _("failed to parse minimum extent length"));
553 ctl
.type_pattern
= optarg
;
558 case OPT_QUIET_UNSUPP
:
559 ctl
.quiet_unsupp
= 1;
564 print_version(EXIT_SUCCESS
);
566 errtryhelp(EXIT_FAILURE
);
572 errx(EXIT_FAILURE
, _("no mountpoint specified"));
573 path
= argv
[optind
++];
576 if (optind
!= argc
) {
577 warnx(_("unexpected number of arguments"));
578 errtryhelp(EXIT_FAILURE
);
582 return fstrim_all(&ctl
, tabs
); /* MNT_EX_* codes */
584 if (!is_directory(path
, 0))
587 rc
= fstrim_filesystem(&ctl
, path
, NULL
);
588 if (rc
== 1 && ctl
.quiet_unsupp
)
591 warnx(_("%s: the discard operation is not supported"), path
);
593 return rc
== 0 ? EXIT_SUCCESS
: EXIT_FAILURE
;