C on amd64 Linux, 36 bytes (timestamp only), 52 49 bytes (real disk activity)
I hard-code the open(2) flags, so this is not portable to other ABIs. Linux on other platforms likely uses the same O_TRUNC, etc., but other POSIX OSes may not.
+4 bytes to pass a correct permission arg to make sure the file is created with owner write access, see below. (This happens to work with gcc 5.2)
somewhat-portable ANSI C, 38/51 bytes (timestamp only), 52/67 bytes (real disk activity)
Based on @Cat's answer, with a tip from @Jens.
The first number is for implementations where an int can hold FILE *fopen()'s return value, second number if we can't do that. On Linux, heap addresses happen to be in the low 32 bits of address space, so it works even without -m32 or -mx32. (Declaring void*fopen(); is shorter than #include <stdio.h>)
Timestamp metadata I/O only:
main(){for(;;)close(open("a",577));} // Linux x86-64
//void*fopen(); // compile with -m32 or -mx32 or whatever, so an int holds a pointer.
main(){for(;;)fclose(fopen("a","w"));}
Writing a byte, actually hitting the disk on Linux 4.2.0 + XFS + lazytime:
main(){for(;write(open("a",577),"",1);close(3));}
write is the for-loop condition, which is fine since it always returns 1. close is the increment.
// semi-portable: storing a FILE* in an int. Works on many systems
main(f){for(;f=fopen("a","w");fclose(f))fputc(0,f);} // 52 bytes
// Should be highly portable, except to systems that require prototypes for all functions.
void*f,*fopen();main(){for(;f=fopen("a","w");fclose(f))fputc(0,f);} // 67 bytes
Explanation of the non-portable version:
The file is created with random garbage permissions. With gcc 5.2, with -O0 or -O3, it happens to include owner write permission, but this is not guaranteed. 0666 is decimal 438. A 3rd arg to open would take another 4 bytes. We're already hard-coding O_TRUNC and so on, but this could break with a different compiler or libc on the same ABI.
We can't omit the 2nd arg to open, because the garbage value happens to include O_EXCL, and O_TRUNC|O_APPEND, so open fails with EINVAL.
We don't need to save the return value from open(). We assume it's 3, because it always will be. Even if we start with fd 3 open, it will be closed after the first iteration. Worst-case, open keeps opening new fds until 3 is the last available file descriptor. So, up to the first 65531 write() calls could fail with EBADF, but will then work normally with every open creating fd = 3.
577 = 0x241 = O_WRONLY|O_CREAT|O_TRUNC on x86-64 Linux. Without O_TRUNC, the inode mod time and change time aren't updated, so a shorter arg isn't possible. O_TRUNC is still essential for the version that calls write to produce actual disk activity, not rewrite in place.
I see some answers that open("a",1). O_CREAT is required if a doesn't already exist. O_CREAT is defined as octal 0100 (64, 0x40) on Linux.
No resource leaks, so it can run forever. strace output:
open("a", O_WRONLY|O_CREAT|O_TRUNC, 03777762713526650) = 3
close(3) = 0
... repeating
or
open("a", O_WRONLY|O_CREAT|O_TRUNC, 01) = 3
write(3, "\0", 1) = 1 # This is the terminating 0 byte in the empty string we pass to write(2)
close(3) = 0
I got the decimal value of the open flags for this ABI using strace -eraw=open on my C++ version.
On a filesystem with the Linux lazytime mount option enabled, a change that only affects inode timestamps will only cause one write per 24 hours. With that mount option disabled, timestamp updating might be a viable way to wear out your SSD. (However, several other answers only do metadata I/O).
alternatives:
shorter non-working:
main(){for(;;)close(write(open("a",577),"",3));} uses write's return value to pass a 3 arg to close. It saves another byte, but doesn't work with gcc -O0 or -O3 on amd64. The garbage in the 3rd arg to open is different, and doesn't include write permission. a gets created the first time, but future iterations all fail with -EACCESS.
longer, working, with different system calls:
main(c){for(open("a",65);pwrite(3,"",1);)sync();} rewrites a byte in-place and calls sync() to sync all filesystems system-wide. This keeps the drive light lit up.
We don't care which byte, so we don't pass 4th arg to pwrite. Yay for sparse files:
$ ll -s a
300K -rwx-wx--- 1 peter peter 128T May 15 11:43 a
Writing one byte at an offset of ~128TiB led to xfs using 300kiB of space to hold the extent map, I guess. Don't try this on OS X with HFS+: IIRC, HFS+ doesn't support sparse files, so it will fill the disk.
XFS is a proper 64bit filesystem, supporting individual files up to 8 exabytes. i.e. 2^63-1, the maximum value off_t can hold.
strace output:
open("a", O_WRONLY|O_CREAT, 03777711166007270) = 3
pwrite(3, "\0", 1, 139989929353760) = 1
sync() = 0
pwrite(3, "\0", 1, 139989929380071) = 1
sync() = 0
...
/dev/null? (Isyes>/dev/nulla valid Bash answer?) \$\endgroup\$