It's actually pretty simple, at least if you don't need the implementation details.
First off, on Linux all file systems (ext2, ext3, btrfs, reiserfs, tmpfs, ...) are implemented in the kernel. Some may offload work to userland code through FUSE, but there is still a kernel component. This is an important basic.
When a program wants to read from a file, it will issue various system calls which ultimately end up in the kernel in the form of an
close() sequence (possibly with
seek() thrown in for good measure). The kernel takes the provided path and filename, and through the file system and device I/O layer translates these to physical read requests to some underlying storage.
However, it doesn't have to translate those requests specifically to physical storage. The kernel's contract is that issuing that particular set of system calls will provide the contents of the file in question.
/proc is usually mounted what is known as
procfs. That is a special file system type, but since it is a file system, it really is no different from e.g. an
ext3 file system mounted somewhere. So the request gets passed to the procfs file system driver code, which knows about all these files and returns particular pieces of information from the kernel data structures.
The "storage layer" in this case is the kernel data structures, and
procfs provides a clean, convenient interface to accessing those. Mounting procfs at
/proc is simply convention; you could just as easily mount it elsewhere (and this is sometimes done, e.g. in chroot jails when the process running there needs access to /proc for some reason).
It works the same way if you write a value to some file; at the kernel level, that translates to a series of
close() calls which again get passed to the file system driver; again, in this case, procfs.
The particular reason why you see
empty is that many of these files are exposed with a size of 0 bytes. The 0 byte size is likely an optimization on the kernel side (many of the files in /proc are dynamic and can easily vary in length, possibly even from one read to the next, and calculating the length of each file on every directory read would potentially be very expensive), and going by the comments to this answer,
file first issues a
stat() call to detect any special files, and then takes the opportunity to, if the file size is reported as 0, abort and report the file as being empty. This behavior is actually documented and can be overridden by specifying
--special-files on the
file invocation, although as stated in the manual page that may have side effects. The quote is from the BSD file 5.11, Oct 17 2011 man page.
Normally, file only attempts to read and determine the type of argument files which stat(2) reports are ordinary files. This prevents problems, because reading special files may have peculiar consequences. Specifying the
-s option causes file to also read argument files which are block or character special files. This is useful for determining the filesystem types of the data in raw disk partitions, which are block special files. This option also causes file to disregard the file size as reported by stat(2) since on some systems it reports a zero size for raw disk partitions.