The primary use case considered is to provide interoperability with the POSIX functions
fcntl, lockf & fdopen.
Specifically to enable:
- file locking
I'm not sure I understand the relevance here. There are many libraries that provide support for file locking. Is AFIO on track to become part of the standard?
or are you just suggesting that alternative approaches (like handling locking entirely independently of fstream) are in general better?
Even if that is true, does it eliminate all realistic uses for accessing the file descripter of an fstream?
I therefore suggest the methods make use of a 'file descriptor' struct which is opaque to the standard
but allows implementation defined access to the low level representation.
On a POSIX system this could be a POD type like:
struct std::file_descriptor
{
int fileDes;
};
On Tuesday, 8 August 2017 18:54:36 PDT Niall Douglas wrote:
> It's basically a bitfield for metadata and a union for constexpr init, int
> fd, int pid and void *handle.
What if it's a file descriptor that represents a process? :-)
I was just hoping to fix a simple and obvious defect without being too ambitious. With that in mind can you suggest good arguments for just returning the handle versus providing stdio_filebuf and native_filebuf as Ville suggested?
What platforms beyond POSIXish and Windows need to be considered? Where in the standard is that documented?
What platforms beyond POSIXish and Windows need to be considered? Where in the standard is that documented?Historically both C and C++ have taken the view that only POSIX need be considered.
Historically both C and C++ have taken the view that only POSIX need be considered.
... they have? I don't recall any specific parts of those standards that were non-POSIX-hostile.
Indeed, the fact that `wchar_t` exists at all (and is implementation-defined), rather than keeping to the POSIX assumption of byte-sized `char`s suggests quite a lot of consideration of non-POSIX systems.
For arguments sake though why shouldn't FILE* and HANDLE=void* be used interchangeably? We could require the file descriptor struct contains a pointer which on Posix is a FILE* and on windows is a HANDLE. You can get the integer FD on Posix using fileno(). In fact why not go further and have the opaque file descriptor just be a pointer which is FILE* if _POSIX_C_SOURCE is defined and HANDLE=void* on Windows. What would that break and what limits would it impose?
On 12 Aug 2017, at 16:27, Niall Douglas <nialldo...@gmail.com> wrote:
> You may also wish to retrieve the underlying FILE * on Windows.
>
> Also you can't assume HANDLE = void *. If you turn on strict type checking in windows.h, it won't be.
Note that the implicit assumption that basic_filebuf has to be implemented in terms of FILE* is wrong! Despite the description of the standard defining behavior in terms of <stdio.h> there is no mandate that it is implemented that way. I know that Plauger's *choice* was to do so but other choices are possible and are arguably more reasonable.
My implementation certainly travels in terms of file descriptors (I never really cared about Windows). Using a FILE* would imply that either filebuf really is a FILE (i.e., the buffers are conflated, that's the libstdc++ choice when based on glibc as far as I know) or that independent buffers are used making file streams noticably slower. On POSIX it is common that a FILE* can be constructed from a file descriptor. If I had to create a FILE* from a stream it could be done but the underlying buffering data structures would be entirely separate.
If there should be access to some underlying structure I'd argue against anything which proposes something different than access to a native handle. That would be a non-owning representation of a possibly implement defined type (rather than an unspecified type). An implement would then chiise what to return: a file descriptor, a FILE*, a HANDLE, or whatever else it sees fit.
If users want to use something different I think they are best off just creating their own stream buffer in term of whatever file access they see fit. We shouldn't waste much time on a fairly niche requirement and we shall certainly not impose any constraints limiting implementation freedom for something like that! Streams are user-extensible and there is no need to put everything into them. That is quite different to FILE* based operations which are not user extensible.
"By default, all eight standard C++ streams are synchronized with their respective C streams."
Incidentally can't you implement sync_with_stdio() by just calling fsync() on the native descriptor before each stream operation and flush() on the stream after wards.
Is there some other trick that sync_with_stdio enables better performance?
On 14 August 2017 at 19:41, Niall Douglas <nialldo...@gmail.com> wrote:
> As much as I support the exposure of the underlying file handle for
> iostreams, I do have to question the use cases:
>
> 1. Byte range locking has highly non-portable semantics, and is downright
> dangerous to use on POSIX with iostreams. Any code using the underlying fd
> for byte range locking on POSIX is probably incorrect.
>
> 2. fsync() generally does not do what people think it does, or what POSIX
> says it must do, and that is an increasing problem with time rather than
> decreasing i.e. fsync() is ever more becoming a partial or total noop on
> more and more systems. Any code using fsync() is probably incorrect.
>
> What other major use case is there for exposing the native file handle for
> iostreams? I suppose maybe handing fds off to child processes. But that's
> best implemented by you opening the fds by hand, configuring them, then
> wrapping them into iostreams if needed.
>
> many POSIX implementations don't permit O_SYNC to be changed after fd open.
> So I'm kinda running out of valid use cases now.
>
> I'm open to being corrected, but I've gotta wonder here, if someone wants to
> poke the internals of iostreams, perhaps they should just not use iostreams?
These are yet further reasons to leave iostreams' and filebuf's API
alone. It's not
generally sane to expose the underlying file descriptor at that level.
If you think it's
something to fiddle with, make sure you know that you're dealing with a filebuf
that is willing to expose the descriptor, cast down to that derived
filebuf and get
the descriptor from there.
There was a remark earlier that filebuf is not the only interface to file IO in C++.
The only other one I know of is cstdio. I don't really class that as 'C++' anymore than I would another library with a C binding.
It is in the standard because compatibility with C is not only good but essential.
>
> 1. Byte range locking has highly non-portable semantics, and is downright
> dangerous to use on POSIX with iostreams. Any code using the underlying fd
> for byte range locking on POSIX is probably incorrect.
>I think you mean non-portable rather than incorrect. I know of several implementations that work correctly.
I would not trust them to work correctly on a different platform out of the box or even a different file-system
on same platform. That doesn't make it a bad thing to do.
They can be made a little more portable with effort.
>
> What other major use case is there for exposing the native file handle for
> iostreams? I suppose maybe handing fds off to child processes. But that's
> best implemented by you opening the fds by hand, configuring them, then
> wrapping them into iostreams if needed.
>
A pipe is exactly one of the other uses I would have in mind.
However, the semantics of pipe mean it must be an entirely separate class from filebuf
(obviously as it isn't really a file).
> So that just leaves the native_filebuf == filebuf question.
That should evaluate to false, and perhaps even so that you can't cast
a filebuf to a native_filebuf,
i.e. they wouldn't have an inheritance relationship.
>
> 1. Byte range locking has highly non-portable semantics, and is downright
> dangerous to use on POSIX with iostreams. Any code using the underlying fd
> for byte range locking on POSIX is probably incorrect.
>I think you mean non-portable rather than incorrect. I know of several implementations that work correctly.No, I mean incorrect.The byte range locking API is so severely broken in POSIX as to make it impossible to write correct code with it.It is possible to write correct code if and only if:1. If you control all file descriptors in the entire process.
2. Files are never big.
3. Files are never on a network drive.
4. You don't care about pathological performance occurring (like, single digit grants per second).
5. You don't use threads.
6. You don't care about power consumption.
7. You don't switch between shared and exclusive on non-identical ranges.
8. You never recurse into code which needs to take a lock whilst holding a lock.
9. You can guarantee no third party is permuting the bit of filesystem you are using.
But if you can meet all those conditions, then almost any other form of synchronisation is better and faster. The sole thing which byte range locks have which is useful is that they auto-release if the holding process suddenly exits. That's it.
I would not trust them to work correctly on a different platform out of the box or even a different file-system
on same platform. That doesn't make it a bad thing to do.
They can be made a little more portable with effort.AFIO provides four implementations of afio::algorithm::shared_fs_mutex. It makes use of byte range locks to implement those, but mostly solely as sentinels for detecting sudden process exit by another process holding a lock.
Now, Windows on the other hand really nailed byte range locks beautifully. Correct design. Amazing performance, Scales beautifully. Doesn't burn the CPU. Works with threads. Async API. POSIX literally should take the NT byte range lock design and use it verbatim. It's the right design. Shame about the mandatory locking though, choice of advisory or mandatory would have been better.
>
> What other major use case is there for exposing the native file handle for
> iostreams? I suppose maybe handing fds off to child processes. But that's
> best implemented by you opening the fds by hand, configuring them, then
> wrapping them into iostreams if needed.
>
A pipe is exactly one of the other uses I would have in mind.
However, the semantics of pipe mean it must be an entirely separate class from filebuf
(obviously as it isn't really a file).Unless they've removed it, the Networking TS should implement iostreams integration for pipes. ASIO certainly does. If it's still there, then you're covered by the TS and can untick that use case too.Niall
On 15 August 2017 at 03:59, <torto...@gmail.com> wrote:
> I see what your saying but I'm not sure I buy it here.
>
> Does the rest of the standard separating portable and non-portable classes
> or does it just label functions accordingly?
False equivalence. A std::thread creates a thread, and gives you
access to the native handle.
However, this leads to the main difference of filebuf and
native_filebuf. filebuf opens a file
for you. While native_filebuf can be made to do that as well, its main
motivation is to
adopt an existing descriptor to an already open file.
> A slightly related issue. You have an extra cognitive burden of needing
> another filebuf variant which needs to be specified in full.
> I'm not sure that standardese lets you say - exactly like this class but
> with two additional functions. Maybe it does?
> Adding a couple of functions looks dangerously like derivation but in this
> case the derivation is possibly inverted. What is a filebuf if not a
> native_filebuf with those
> two functions hidden?
See above.
> I think if we want to make a distinction it has to be on more solid grounds.
> Buffering might make sense. If you said a native_filebuf has no buffering at
> all and a filebuf has some mandated buffering that might be the significant
> difference to justify it.
That would be complete nonsense. Whether a native_filebuf ends up
doing buffered i/o
depends on how you opened the file. And filebuf has no mandated
buffering, it can hit
the disk on every byte if it so chooses, although that would be a poor
implementation.
The byte range locking API is so severely broken in POSIX as to make it impossible to write correct code with it.It is possible to write correct code if and only if:1. If you control all file descriptors in the entire process.why wouldn't you? or rather why would you think it was a good idea not to if you are trying to lock things?
2. Files are never big.The reason for this one is less clear? performance perhaps? How big is big?
3. Files are never on a network drive.Yes. File locking 101 - don't use NFS or Samba. Although allegedly it might work better on NFS4 which no-one has properly implemented yet.
I haven't tried it on other remote file systems like sshfs. But I'm sure it would be 'interesting'.
4. You don't care about pathological performance occurring (like, single digit grants per second).That sounds like it should be the program's fault for locking too often. i.e. poor design
5. You don't use threads.Just be careful about which thread is doing the locking. Its an issue as with many other shareable resources.
6. You don't care about power consumption.This one makes little sense to me. Even on an embedded system it should be a case of flipping a few bit and checking if they're flipped.
7. You don't switch between shared and exclusive on non-identical ranges.That sounds like a potentially bad design too.
8. You never recurse into code which needs to take a lock whilst holding a lock.Don't deadlock. Multithreading 101
9. You can guarantee no third party is permuting the bit of filesystem you are using.Yes. File locking is for interprocess synchronisation. You have to be in control of the processes involved in that.
But if you can meet all those conditions, then almost any other form of synchronisation is better and faster. The sole thing which byte range locks have which is useful is that they auto-release if the holding process suddenly exits. That's it.
Wouldn't a lot of other forms of synchronisation have to re-invent advisory locking for themselves to do this?
I would not trust them to work correctly on a different platform out of the box or even a different file-system
on same platform. That doesn't make it a bad thing to do.
They can be made a little more portable with effort.AFIO provides four implementations of afio::algorithm::shared_fs_mutex. It makes use of byte range locks to implement those, but mostly solely as sentinels for detecting sudden process exit by another process holding a lock.Your need for locks here seems to agree with my assessment above?
Now, Windows on the other hand really nailed byte range locks beautifully. Correct design. Amazing performance, Scales beautifully. Doesn't burn the CPU. Works with threads. Async API. POSIX literally should take the NT byte range lock design and use it verbatim. It's the right design. Shame about the mandatory locking though, choice of advisory or mandatory would have been better.That is not something I often here about a Windows API but it could perhaps be a Windows person versus Unix person thing?
Care to elaborate?
Unless they've removed it, the Networking TS should implement iostreams integration for pipes. ASIO certainly does. If it's still there, then you're covered by the TS and can untick that use case too.Niall
Pipes was never ticked for this proposal.
Pipe doesn't appear in http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4656.pdf other than in reference to sigpipe.
But I think ASIO was split into bits so they might be in another proposal somewhere.
The byte range locking API is so severely broken in POSIX as to make it impossible to write correct code with it.It is possible to write correct code if and only if:1. If you control all file descriptors in the entire process.why wouldn't you? or rather why would you think it was a good idea not to if you are trying to lock things?Almost no software today doesn't make extensive use of third party libraries, often with source code which cannot be easily modified.And due to POSIX dropping all byte range locks as soon as any fd to that inode is closed, it makes byte range locks inherently problematic.Consider for example an implementation of filesystem::path::exists() which tries to open the path to test for existence. It would open and then close a fd. If any code elsewhere in the process has byte range locks open on that inode, they get dropped.Before you say "just use stat() then", you can't in many cases e.g. if the filesystem is permuting randomly, because then you can't use paths at all. Also, incidentally, there is nothing stopping stat() being implemented by your libc as open()-fstat()-close() like it must be on Windows. Again, game over thanks to the design of POSIX byte range locks.
2. Files are never big.The reason for this one is less clear? performance perhaps? How big is big?struct flock due to some amazing bad design uses signed values, thus rendering the top half of your file unlockable.You might think that not important. For filesystem algorithm programmers who might use the entire 64 bit space as an open hash table using sparse storage and hole punching, it's a showstopper.I've also seen implementations fail at offsets after 1<<62 rather than 1<<63. Almost certainly a bug. But not comforting.
3. Files are never on a network drive.Yes. File locking 101 - don't use NFS or Samba. Although allegedly it might work better on NFS4 which no-one has properly implemented yet.
I haven't tried it on other remote file systems like sshfs. But I'm sure it would be 'interesting'.Windows-type oplocks, if implemented correctly, are a much better design.
4. You don't care about pathological performance occurring (like, single digit grants per second).That sounds like it should be the program's fault for locking too often. i.e. poor designNo, it's poor quality of implementation in some kernels and/or filing systems. In some cases they scale exponentially inverse to physical CPU count for example. Wonderful.
5. You don't use threads.Just be careful about which thread is doing the locking. Its an issue as with many other shareable resources.It's not that.POSIX byte range locks are per-inode, and don't detect attempts to lock the same region twice, rather they just ignore the second attempt and then release too early.You'll probably say now that better design of your code would fix this. But you don't control the threads in your process increasingly any more, various third party libraries will spin up threads and go run stuff in the background out of your control.
6. You don't care about power consumption.This one makes little sense to me. Even on an embedded system it should be a case of flipping a few bit and checking if they're flipped.POSIX byte range locks give you exactly two choices: block until lock granted, or return immediately.You can't wait for a timeout. You can't be notified when it's become free. You can't do other work whilst being blocked.Thus you end up either spinning on the lock burning CPU, or launching kernel threads for the sole purpose of waiting on the lock asynchronously.
This hits battery life badly on mobile devices. Lock files are actually cheaper on power budget, which is sad.
7. You don't switch between shared and exclusive on non-identical ranges.That sounds like a potentially bad design too.My issue is the lack of specification of atomicity. Implementations don't say whether shared to exclusive upgrades are atomic, for identical ranges or overlapping ranges.It's a common use case to have a region locked for shared use, then you want to lock some or all of it for exclusive use without anybody else modifying it before the exclusive lock is granted. The POSIX API isn't fit for this purpose, it splits and replaces locks instead of laying exclusive over shared. Unhelpful.
You can't upgrade locks at all on Windows interestingly, but you can atomically downgrade them i.e. exclusive -> shared atomically. This isn't as useful, but at least the behaviour is guaranteed.8. You never recurse into code which needs to take a lock whilst holding a lock.Don't deadlock. Multithreading 101You don't always control such code. What we'd much prefer to see is EDEADLK being returned.9. You can guarantee no third party is permuting the bit of filesystem you are using.Yes. File locking is for interprocess synchronisation. You have to be in control of the processes involved in that.A major attack vector is generating races by maliciously fiddling with filesystem under IPC usage. TOCTOU etcYou might say "set perms etc" but in fact all that is unnecessary with a non-broken design. I hate to keep chirping on about Windows, but it won't let anybody permute part of a filesystem being used for synchronisation, thus eliminating TOCTOU et al entirely.But if you can meet all those conditions, then almost any other form of synchronisation is better and faster. The sole thing which byte range locks have which is useful is that they auto-release if the holding process suddenly exits. That's it.
Wouldn't a lot of other forms of synchronisation have to re-invent advisory locking for themselves to do this?If the kernel supplied implementation is really lousy - and everywhere but FreeBSD it is - then you're better off.
I would not trust them to work correctly on a different platform out of the box or even a different file-system
on same platform. That doesn't make it a bad thing to do.
They can be made a little more portable with effort.AFIO provides four implementations of afio::algorithm::shared_fs_mutex. It makes use of byte range locks to implement those, but mostly solely as sentinels for detecting sudden process exit by another process holding a lock.Your need for locks here seems to agree with my assessment above?When you have multiple processes working on the same data, often you need to synchronise. You try not to of course, you exploit the natural synchronisation built into i/o which is actually finally viable as of just these past months thanks to Microsoft fixing Windows to follow POSIX i/o concurrency guarantees. You need the very latest Windows 10 however, and a pretty recent Linux kernel. But it does work, and it's portable-ish.AFIO uses a list based system for its locking algorithms, so process A says it'll be locking 5, 22 and 13. Process B says it'll be locking 6, 99, and 13. The synchronisation will happen on the 13.The numbers are arbitrary and mean whatever the application chooses them to mean. Under the bonnet, the four implementations have very different approaches to implementation. Some have amazing performance but are anti-social. Others scale amazingly. Some are NFS/Samba friendly. Some are shared memory only.afio::shared_fs_mutex is an abstract base class, so runtime code can swap implementations and higher level code doesn't need to care how the locking works.
On Monday, 14 August 2017 19:22:57 PDT Niall Douglas wrote:
> Consider for example an implementation of filesystem::path::exists() which
> tries to open the path to test for existence. It would open and then close
> a fd. If any code elsewhere in the process has byte range locks open on
> that inode, they get dropped.
Why would it open instead of access() with F_OK?
but there are a couple of questions here. Why would you need check if a file exists if you've just locked it? similarly why would you be separately opening and closing the same file descriptor?
This sounds like a multi-threading issue. I'd agree that posix is weaker where threads are concerned as the locking and signalling APIs predate that.
The expectation is that you use processes instead. Its a fundamentally different philosophy than on windows where processes are expensive and threads are king.
Processes are much cheaper on Linux than windows (that is of course not mandated by posix).
This does create a lot of difficulty in trying to write an application that is portable between windows and Linux.
However, you can work around that by designating only one thread is responsible for each lock. It works a bit less well for signals.
The descriptions here (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365713(v=vs.85).aspx) a little off putting
The description of locking a file by using a function called CreateFile() twice on the same file is far from an intuitive API.
Of course it's the semantics that are of interest here and I need to read more to grok the design.
"For example, in the execution of a batch file, the batch file may be opened and closed once for each line of the file. A batch opportunistic lock opens the batch file on the server and keeps it open. As the command processor "opens" and "closes" the batch file, the network redirector intercepts the open and close commands."
spoken like a OS that isn't littered with thousands of little scripting languages :).
No, it's poor quality of implementation in some kernels and/or filing systems. In some cases they scale exponentially inverse to physical CPU count for example. Wonderful.That would be a bug report then. Though one requiring a complete re-design is quite hard to fix.
If the kernel supplied implementation is really lousy - and everywhere but FreeBSD it is - then you're better off.
Is that down to bugs or does FreeBSD tighten up and improve on the basic posix specification?
When you have multiple processes working on the same data, often you need to synchronise. You try not to of course, you exploit the natural synchronisation built into i/o which is actually finally viable as of just these past months thanks to Microsoft fixing Windows to follow POSIX i/o concurrency guarantees. You need the very latest Windows 10 however, and a pretty recent Linux kernel. But it does work, and it's portable-ish.AFIO uses a list based system for its locking algorithms, so process A says it'll be locking 5, 22 and 13. Process B says it'll be locking 6, 99, and 13. The synchronisation will happen on the 13.The numbers are arbitrary and mean whatever the application chooses them to mean. Under the bonnet, the four implementations have very different approaches to implementation. Some have amazing performance but are anti-social. Others scale amazingly. Some are NFS/Samba friendly. Some are shared memory only.afio::shared_fs_mutex is an abstract base class, so runtime code can swap implementations and higher level code doesn't need to care how the locking works.Have you considered trying to specify an improved OS APIs that could be aspired to?
It is exactly this kind of implementation experience from which such things tend to drop out.
On Tuesday, 15 August 2017 08:19:42 PDT Niall Douglas wrote:
> access() is an enormous security hole. I really wish it could be eliminated.
Explain. Are you refering to a TOCTOU attack?
> open()-close() is slow, but safe. Indeed Windows internally does exactly
> this when checking for a file to exist.
For that matter, trying W_OK by opening for write is also a bad idea, since it
modifies the file (atime update) and could run afoul of sharing violation on
Windows.
> Mainly. But access() is problematic in lots of other ways too. One should
> really not implement exists() with it, it's the wrong API.
The TOCTOU attack is not the fault of the library or the access() function,
but that of the upper code that checked for existence before trying the
operation it was going to do if the file existed.
So you have two choices: provide a function that returns the existence of the
file or force the user to not check for it. If you choose to provide it, then
you have to use either access(F_OK) or stat(), but the latter is more
expensive than the former.
> You'll see this in AFIO. We never store a path. Paths = race conditions. It
> also means that all of AFIO's classes have trivial storage, which in turn
> means no mallocs and no performance surprises which is important when you
> have a 1 microsecond budget.
Path, whether relative or absolute, is not the issue.
I was reading Raymond
Chen's post yesterday on hardlinking, which is relevant to this discussion:
https://blogs.msdn.microsoft.com/oldnewthing/20170707-00/?p=96555
One of the commenters asks
"According to the well-known UX guidelines (context) menu entries which can’t
be executed should not be shown to the user. [...] So: how should a proper
implemented shell extension handle it?"
That's similar to the case here. Sometimes the user needs to know if a file
exists before trying to open it. I know it's racy, but if the UX requires it,
the programmer needs to implement it. That means the library needs to provide
an API to check if the file exists.
The problem with your argument is that CFile_from_FileHandle need not return the same FILE* when called multiple times with the same file handle value, but your file() had better return the same value when called multiple times.
On Wednesday, 23 August 2017 09:11:42 UTC+1, T. C. wrote:
On Wednesday, August 23, 2017 at 3:47:55 AM UTC-4, Bruce Adams wrote:
On Wednesday, 23 August 2017 06:47:14 UTC+1, T. C. wrote:The problem with your argument is that CFile_from_FileHandle need not return the same FILE* when called multiple times with the same file handle value, but your file() had better return the same value when called multiple times.Can you give an example where it legitimately could not?CFile_from_FileHandle is not a trivial mapping. A FILE* points to a FILE, which, while opaque in the standard, typically contains additional data beyond the native file handle (e.g., pointers to buffers). To implement fopen(), it suffices to malloc() a FILE, initialize it appropriately and return a pointer to it (freeing it in fclose()). You can't do that in your file().So...how do you plan to do it without maintaining a global fd-to-FILE* map?The C runtime for the OS must do that anyway to implement stdio.
With regards to glibc there is no need for a toy implementation.
What could be different in the code used by std::fopen() that makes it illegal to use in filebuf::file() ?
On Wednesday, August 23, 2017 at 2:54:47 PM UTC-4, Bruce Adams wrote:With regards to glibc there is no need for a toy implementation.A toy implementation that only stores a file descriptor and converts to FILE* on demand. stdio_filebuf is not that.
What could be different in the code used by std::fopen() that makes it illegal to use in filebuf::file() ?I told you what. Several times. fopen can just malloc a FILE and return a pointer to that. Likewise for fdopen. Your file() cannot, because multiple calls to it need to return the same FILE*.