This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: os.read not handling O_DIRECT flag
Type: enhancement Stage:
Components: Library (Lib) Versions: Python 3.1, Python 2.7
process
Status: closed Resolution: wont fix
Dependencies: Superseder:
Assigned To: Nosy List: Michael Mol, aguiar, exarkun, jwilk, logang, pitrou, yoyoyopcp
Priority: normal Keywords:

Created on 2009-02-28 19:02 by aguiar, last changed 2022-04-11 14:56 by admin. This issue is now closed.

Messages (10)
msg82938 - (view) Author: Eduardo Aguiar (aguiar) Date: 2009-02-28 19:02
At posixmodule.c (line 6306)

static PyObject *
posix_read(PyObject *self, PyObject *args)
{
	int fd, size, n;
	PyObject *buffer;
	if (!PyArg_ParseTuple(args, "ii:read", &fd, &size))
		return NULL;
	if (size < 0) {
		errno = EINVAL;
		return posix_error();
	}
	buffer = PyString_FromStringAndSize((char *)NULL, size);
	if (buffer == NULL)
		return NULL;
	Py_BEGIN_ALLOW_THREADS
	n = read(fd, PyString_AsString(buffer), size);
	Py_END_ALLOW_THREADS
	if (n < 0) {
		Py_DECREF(buffer);
		return posix_error();
	}
	if (n != size)
		_PyString_Resize(&buffer, n);
	return buffer;
}

os.read does not work with O_DIRECT flag. It fails with errno = EINVAL.

From read(2) man page:

       EINVAL fd  is attached to an object which is unsuitable for
reading; or
              the file was opened with  the  O_DIRECT  flag,  and 
either  the
              address  specified  in buf, the value specified in count,
or the
              current file offset is not suitably aligned.

if os.open is called with O_DIRECT flag enabled, the buffer used in
"read" must be page aligned and "size" must be multiple of pagesize also.
msg83541 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2009-03-13 23:02
I don't think we will try to support O_DIRECT. First, it is a rather
platform-specific flag. Second, Python's memory allocator doesn't allow
specifying an arbitrary alignment value. Third, I don't even think
O_DIRECT can make a positive difference in a Python program (the open()
manpage actually warns against likely performance degradation when using
O_DIRECT).
So I'm closing this bug, sorry.
msg83638 - (view) Author: Eduardo Aguiar (aguiar) Date: 2009-03-15 15:52
Hi,

I think I have a few more issues you can consider:

1) to allocated an aligned buffer it is as simple as allocate 4096 + len
(buffer) and truncate address to 4k boundary.

2) I wrote a floppy imager, and without O_DIRECT, it gives me 8 sectors
(4k = kernel page) errors whenever one sector is bad.

3) There is os.O_DIRECT and os.open help page references it

Best regards,
Eduardo Aguiar
msg83639 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2009-03-15 16:05
Hello Eduardo,

> 1) to allocated an aligned buffer it is as simple as allocate 4096 + len
> (buffer) and truncate address to 4k boundary.

Yes, but it is wasteful, especially since the common case is not to use
O_DIRECT. Also, os.read does not allocate a buffer, it allocates a whole
string object, and there's no way in the current Python object allocator
to choose a specific alignment boundary.

> 2) I wrote a floppy imager, and without O_DIRECT, it gives me 8 sectors
> (4k = kernel page) errors whenever one sector is bad.

Well, sorry for that... I guess Python is not well adapted to this (very
particular) use case.
Or you could write a C extension to read() to a properly aligned buffer,
or try to do it with ctypes.

> 3) There is os.O_DIRECT and os.open help page references it

I think the policy is to mirror all possible O_* constants, even if they
are of no use in Python. For example, we also have os.O_DIRECTORY.

Regards

Antoine.
msg83778 - (view) Author: Jean-Paul Calderone (exarkun) * (Python committer) Date: 2009-03-18 21:18
> I think the policy is to mirror all possible O_* constants, even if they
> are of no use in Python. For example, we also have os.O_DIRECTORY.


Not disagreeing with the conclusion of this ticket, but I would like to
point out that os.O_DIRECTORY isn't useless in Python.  You need it to
use with os.open if you want to open a directory (which you may wish to
do in order to use with os.fsync, for example).
msg276329 - (view) Author: Michael Mol (Michael Mol) Date: 2016-09-13 19:20
I need to remember not to try to write quick programs in Python that need O_DIRECT.

My use case: I'm attempting to write a program that forces the disk to seek to a particular place, as part of burning in the disk.

My algorithm goes:

1. Seek to the beginning of the disk
2. Write to the disk
3. Seek to the end of the disk
4. Write to the disk
5. Seek to the beginning of the disk
6. Read from the disk
7. Seek to the end of the disk
8. Read from the disk

It then repeats with offsets, leading the seek distance to shrink until the two positions overlap at the center of the disk, and then expand as the positions diverge to the opposite end of the disk from where they began.

It's straightforward, and can be done using mmap and os.write as far as the writing side of things goes, but it does not work on the reading side, as even this workaround does not work:

d = os.open(disk_file_path, os.O_RDWR | os.O_DIRECT | os.O_SYNC | os.O_DSYNC)
readbuf = mmap.mmap(-1, 4096)
os.lseek(d, 0, os.SEEK_SET)
fo = os.fdopen(d, 'rb')
fo.readinto(readbuf)

... I get errno 22 on the final line. Apparently, code similar to that used to work, but no longer does.
msg352213 - (view) Author: Paul (yoyoyopcp) * Date: 2019-09-12 16:24
Michael,

I ran into the same issue as you.  I got it to work by changing the mmap size to 8K.

d = os.open(disk_file_path, os.O_RDWR | os.O_DIRECT | os.O_SYNC | os.O_DSYNC)
readbuf = mmap.mmap(-1, 8192)
os.lseek(d, 0, os.SEEK_SET)
fo = os.fdopen(d, 'rb')
fo.readinto(readbuf)

Should work.  What's strange is that further multiples of 4K seem to work OK.

readbuf = mmap.mmap(-1, 4096 * 3)

Also works... So what's going on with 4K?
msg352359 - (view) Author: Paul (yoyoyopcp) * Date: 2019-09-13 16:40
I've dug into stracing this python program in 2.7 vs. 3.7.

directread.py

import mmap
import os

fd = os.open('/dev/dm-2', os.O_DIRECT | os.O_RDWR)  # mapped block device
fo = os.fdopen(fd, 'rb+')
m = mmap.mmap(-1, 4096)
fo.readinto(m)

Python 2.7 result:

...
open("/dev/dm-2", O_RDWR|O_DIRECT)      = 3
...
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0) = 0x7f743db31000
...
read(0x3, 0x7f743db31000, 0x1000)       = 0x1000
...

Python 3.7 result:

...
open("/dev/dm-2", O_RDWR|O_DIRECT|O_CLOEXEC) = 3
...
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0) = 0x7f5e087ee000
...
read(0x3, 0x256c8a0, 0x1000)            = -1 (errno 22)

Notice that Python 3 isn't using the mmap buffer for the read.  Why is it using a stack buffer?
msg352392 - (view) Author: Michael Mol (Michael Mol) Date: 2019-09-13 20:34
I wound up writing it in C++ instead, and my then-employer eventually opened my code. https://github.com/VirtualInterconnect/diskstress
msg357927 - (view) Author: Logan Gunthorpe (logang) Date: 2019-12-06 17:31
Paul's solution works in 3.7 if you set the buffer size to zero when calling fdopen.

fd = os.open("my_file", os.O_DIRECT | os.O_RDWR)
f = os.fdopen(fd, "rb+", 0)
m = mmap.mmap(-1, 4096)
f.readinto(m)

This is according to a comment in _pyio.py:

# If remaining space in callers buffer is larger than
# internal buffer, read directly into callers buffer

So by simply disabling the buffer (which is what we'd want for O_DIRECT anyway) readinto() works as expected. 

However, based on this issue, I'm a little concerned this won't be fully supported by python going forward; so use with care.
History
Date User Action Args
2022-04-11 14:56:46adminsetgithub: 49646
2022-01-13 00:08:25jwilksetnosy: + jwilk
2019-12-06 17:31:25logangsetnosy: + logang
messages: + msg357927
2019-09-13 20:34:32Michael Molsetmessages: + msg352392
2019-09-13 16:40:28yoyoyopcpsetmessages: + msg352359
2019-09-12 16:24:30yoyoyopcpsetnosy: + yoyoyopcp
messages: + msg352213
2016-09-13 19:20:19Michael Molsetnosy: + Michael Mol
messages: + msg276329
2009-03-18 21:18:12exarkunsetnosy: + exarkun
messages: + msg83778
2009-03-15 16:05:26pitrousetmessages: + msg83639
2009-03-15 15:52:06aguiarsetmessages: + msg83638
2009-03-13 23:02:10pitrousetstatus: open -> closed

nosy: + pitrou
messages: + msg83541

resolution: wont fix
2009-03-03 11:48:27pitrousetpriority: normal
type: behavior -> enhancement
versions: + Python 3.1, Python 2.7, - Python 2.6
2009-02-28 19:02:30aguiarcreate