Implementar partições dinâmicas

O particionamento dinâmico é implementado usando o módulo dm-linear device-mapper no kernel do Linux. A partição super contém metadados que listam os nomes e intervalos de blocos de cada partição dinâmica em super. Durante a init do primeiro estágio, esses metadados são analisados e validados, e dispositivos de bloco virtuais são criados para representar cada partição dinâmica.

Ao aplicar uma OTA, as partições dinâmicas são criadas, redimensionadas ou excluídas automaticamente conforme necessário. Para dispositivos A/B, há duas cópias dos metadados, e as mudanças são aplicadas apenas à cópia que representa o slot de destino.

Como as partições dinâmicas são implementadas no espaço do usuário, as partições necessárias pelo carregador de inicialização não podem ser dinâmicas. Por exemplo, boot, dtbo e vbmeta são lidas pelo carregador de inicialização e, portanto, precisam permanecer como partições físicas.

Cada partição dinâmica pode pertencer a um grupo de atualização. Esses grupos limitam o espaço máximo que as partições desse grupo podem consumir. Por exemplo, system e vendor podem pertencer a um grupo que restringe o tamanho total de system e vendor.

Implementar partições dinâmicas em novos dispositivos

Esta seção detalha como implementar partições dinâmicas em novos dispositivos lançados com o Android 10 e versões mais recentes. Para atualizar os dispositivos atuais, consulte Como fazer upgrade de dispositivos Android.

Mudanças de partição

Para dispositivos lançados com o Android 10, crie uma partição chamada super. A partição super processa os slots A/B internamente, então os dispositivos A/B não precisam de partições super_a e super_b separadas. Todas as partições AOSP somente leitura que não são usadas pelo carregador de inicialização precisam ser dinâmicas e ser removidas da tabela de partição GUID (GPT). As partições específicas do fornecedor não precisam ser dinâmicas e podem ser colocadas na GPT.

Para estimar o tamanho de super, adicione os tamanhos das partições que estão sendo excluídas do GPT. Para dispositivos A/B, isso precisa incluir o tamanho de ambos os slots. A Figura 1 mostra um exemplo de tabela de partições antes e depois da conversão para partições dinâmicas.

Layout da tabela de partições
Figura 1. Novo layout da tabela de partições físicas ao converter para partições dinâmicas

As partições dinâmicas compatíveis são:

  • Sistema
  • Fornecedor
  • Produto
  • Ext do sistema
  • ODM

Para dispositivos lançados com o Android 10, a opção de linha de comando do kernel androidboot.super_partition precisa estar vazia para que o comando sysprop ro.boot.super_partition fique vazio.

Alinhamento de partições

O módulo device-mapper pode operar com menos eficiência se a partição super não estiver alinhada corretamente. A partição super PRECISA estar alinhada ao tamanho mínimo da solicitação de E/S, conforme determinado pela camada de bloco. Por padrão, o sistema de compilação (via lpmake, que gera a imagem de partição super) pressupõe que um alinhamento de 1 MiB é suficiente para cada partição dinâmica. No entanto, os fornecedores precisam garantir que a partição super esteja alinhada corretamente.

É possível determinar o tamanho mínimo da solicitação de um dispositivo de bloco inspecionando sysfs. Exemplo:

# ls -l /dev/block/by-name/super
lrwxrwxrwx 1 root root 16 1970-04-05 01:41 /dev/block/by-name/super -> /dev/block/sda17
# cat /sys/block/sda/queue/minimum_io_size
786432

É possível verificar o alinhamento da partição super de maneira semelhante:

# cat /sys/block/sda/sda17/alignment_offset

O deslocamento de alinhamento PRECISA ser 0.

Mudanças na configuração do dispositivo

Para ativar o particionamento dinâmico, adicione a seguinte flag em device.mk:

PRODUCT_USE_DYNAMIC_PARTITIONS := true

Mudanças na configuração do board

É necessário definir o tamanho da partição super:

BOARD_SUPER_PARTITION_SIZE := <size-in-bytes>

Em dispositivos A/B, o sistema de build gera um erro se o tamanho total das imagens de partição dinâmica for mais da metade do tamanho da partição super.

É possível configurar a lista de partições dinâmicas da seguinte maneira: Para dispositivos que usam grupos de atualização, liste os grupos na variável BOARD_SUPER_PARTITION_GROUPS. Cada nome de grupo então tem uma variável BOARD_group_SIZE e BOARD_group_PARTITION_LIST. Para dispositivos A/B, o tamanho máximo de um grupo deve abranger apenas uma posição, já que os nomes de grupo são sufixados internamente.

Confira um exemplo de dispositivo que coloca todas as partições em um grupo chamado example_dynamic_partitions:

BOARD_SUPER_PARTITION_GROUPS := example_dynamic_partitions
BOARD_EXAMPLE_DYNAMIC_PARTITIONS_SIZE := 6442450944
BOARD_EXAMPLE_DYNAMIC_PARTITIONS_PARTITION_LIST := system vendor product

Confira um exemplo de dispositivo que coloca os serviços de sistema e de produto em group_foo e vendor, product e odm em group_bar:

BOARD_SUPER_PARTITION_GROUPS := group_foo group_bar
BOARD_GROUP_FOO_SIZE := 4831838208
BOARD_GROUP_FOO_PARTITION_LIST := system product_services
BOARD_GROUP_BAR_SIZE := 1610612736
BOARD_GROUP_BAR_PARTITION_LIST := vendor product odm
  • Para dispositivos de lançamento do A/B virtual, a soma dos tamanhos máximos de todos os grupos precisa ser de, no máximo:
    BOARD_SUPER_PARTITION_SIZE - overhead
    Consulte Como implementar o A/B virtual.
  • Para dispositivos de lançamento A/B, a soma dos tamanhos máximos de todos os grupos precisa ser:
    BOARD_SUPER_PARTITION_SIZE / 2 - overhead
  • Para dispositivos não A/B e A/B retrofit, a soma dos tamanhos máximos de todos os grupos precisa ser:
    BOARD_SUPER_PARTITION_SIZE - overhead
  • No momento da criação, a soma dos tamanhos das imagens de cada partição em um grupo de atualização não pode exceder o tamanho máximo do grupo.
  • A sobrecarga é necessária no cálculo para considerar metadados, alinhamentos e assim por diante. Uma sobrecarga razoável é de 4 MiB, mas você pode escolher uma sobrecarga maior, conforme necessário pelo dispositivo.

Dimensionar partições dinâmicas

Antes das partições dinâmicas, os tamanhos das partições eram alocados em excesso para garantir espaço suficiente para atualizações futuras. O tamanho real foi usado como está, e a maioria das partições somente leitura tinha algum espaço livre no sistema de arquivos. Em partições dinâmicas, esse espaço livre não é utilizável e poderia ser usado para aumentar partições durante uma OTA. É fundamental garantir que as partições não desperdicem espaço e sejam alocadas no menor tamanho possível.

Para imagens ext4 somente leitura, o sistema de compilação aloca automaticamente o tamanho mínimo se nenhum tamanho de partição fixado no código é especificado. O sistema de build ajusta a imagem para que o sistema de arquivos tenha o menor espaço não utilizado possível. Isso garante que o dispositivo não desperdice espaço que pode ser usado para OTAs.

Além disso, as imagens ext4 podem ser comprimidas ainda mais ao ativar a desduplicação no nível do bloco. Para ativar isso, use a seguinte configuração:

BOARD_EXT4_SHARE_DUP_BLOCKS := true

Se a alocação automática de um tamanho mínimo de partição não for desejável, há duas maneiras de controlar o tamanho da partição. É possível especificar uma quantidade mínima de espaço livre com BOARD_partitionIMAGE_PARTITION_RESERVED_SIZE, ou especificar BOARD_partitionIMAGE_PARTITION_SIZE para forçar partições dinâmicas a um tamanho específico. Nenhuma delas é recomendada, a menos que seja necessário.

Exemplo:

BOARD_PRODUCTIMAGE_PARTITION_RESERVED_SIZE := 52428800

Isso força o sistema de arquivos em product.img a ter 50 MiB de espaço não utilizado.

Alterações no sistema como raiz

Os dispositivos lançados com o Android 10 não podem usar o sistema como raiz.

Dispositivos com partições dinâmicas (sejam lançadas com ou retroativas) não podem usar o sistema como raiz. O kernel do Linux não consegue interpretar a partição super e, portanto, não consegue montar system. O system agora é montado por init de primeira etapa, que fica no ramdisk.

Não defina BOARD_BUILD_SYSTEM_ROOT_IMAGE. No Android 10, a flag BOARD_BUILD_SYSTEM_ROOT_IMAGE é usada apenas para diferenciar se o sistema é montado pelo kernel ou pelo init de primeira etapa no ramdisk.

Definir BOARD_BUILD_SYSTEM_ROOT_IMAGE como true causa um erro de build quando PRODUCT_USE_DYNAMIC_PARTITIONS também é true.

Quando BOARD_USES_RECOVERY_AS_BOOT é definido como verdadeiro, a imagem de recuperação é criada como boot.img, contendo o ramdisk da recuperação. Anteriormente, o carregador de inicialização usava o parâmetro de linha de comando do kernel skip_initramfs para decidir em qual modo inicializar. Para dispositivos Android 10, o carregador de inicialização NÃO PODE transmitir skip_initramfs para a linha de comando do kernel. Em vez disso, o carregador de inicialização precisa transmitir androidboot.force_normal_boot=1 para pular a recuperação e inicializar o Android normal. Os dispositivos lançados com o Android 12 ou versões mais recentes precisam usar o bootconfig para transmitir androidboot.force_normal_boot=1.

Mudanças na configuração do AVB

Ao usar a Inicialização verificada do Android 2.0, se o dispositivo não estiver usando descriptores de partição em cadeia, nenhuma mudança será necessária. No entanto, se você usar partições encadeadas e uma das partições verificadas for dinâmica, será necessário fazer mudanças.

Confira um exemplo de configuração de um dispositivo que encadeia vbmeta para as partições system e vendor.

BOARD_AVB_SYSTEM_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
BOARD_AVB_SYSTEM_ALGORITHM := SHA256_RSA2048
BOARD_AVB_SYSTEM_ROLLBACK_INDEX := $(PLATFORM_SECURITY_PATCH_TIMESTAMP)
BOARD_AVB_SYSTEM_ROLLBACK_INDEX_LOCATION := 1

BOARD_AVB_VENDOR_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
BOARD_AVB_VENDOR_ALGORITHM := SHA256_RSA2048
BOARD_AVB_VENDOR_ROLLBACK_INDEX := $(PLATFORM_SECURITY_PATCH_TIMESTAMP)
BOARD_AVB_VENDOR_ROLLBACK_INDEX_LOCATION := 1

Com essa configuração, o carregador de inicialização espera encontrar um rodape vbmeta no final das partições system e vendor. Como essas partições não estão mais visíveis para o carregador de inicialização (elas residem em super), duas mudanças são necessárias.

  • Adicione partições vbmeta_system e vbmeta_vendor à tabela de partições do dispositivo. Para dispositivos A/B, adicione vbmeta_system_a, vbmeta_system_b, vbmeta_vendor_a e vbmeta_vendor_b. Se você adicionar uma ou mais dessas partições, elas precisarão ter o mesmo tamanho que a partição vbmeta.
  • Renomeie as flags de configuração adicionando VBMETA_ e especifique para quais partições a cadeia se estende:
    BOARD_AVB_VBMETA_SYSTEM := system
    BOARD_AVB_VBMETA_SYSTEM_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
    BOARD_AVB_VBMETA_SYSTEM_ALGORITHM := SHA256_RSA2048
    BOARD_AVB_VBMETA_SYSTEM_ROLLBACK_INDEX := $(PLATFORM_SECURITY_PATCH_TIMESTAMP)
    BOARD_AVB_VBMETA_SYSTEM_ROLLBACK_INDEX_LOCATION := 1
    
    BOARD_AVB_VBMETA_VENDOR := vendor
    BOARD_AVB_VBMETA_VENDOR_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
    BOARD_AVB_VBMETA_VENDOR_ALGORITHM := SHA256_RSA2048
    BOARD_AVB_VBMETA_VENDOR_ROLLBACK_INDEX := $(PLATFORM_SECURITY_PATCH_TIMESTAMP)
    BOARD_AVB_VBMETA_VENDOR_ROLLBACK_INDEX_LOCATION := 1

Um dispositivo pode usar uma, ambas ou nenhuma dessas partições. As alterações são necessárias apenas ao encadear a uma partição lógica.

Mudanças no carregador de inicialização da AVB

Se o carregador de inicialização tiver a libavb incorporada, inclua os seguintes patches:

Se estiver usando partições encadeadas, inclua um patch adicional:

  • 49936b4c0109411fdd38bd4ba3a32a01c40439a9 — "libavb: oferece suporte a blobs vbmeta no início da partição."

Mudanças na linha de comando do kernel

Um novo parâmetro, androidboot.boot_devices, precisa ser adicionado à linha de comando do kernel. Isso é usado por init para ativar os links simbólicos /dev/block/by-name. Ele precisa ser o componente de caminho do dispositivo para o link simbólico por nome subjacente criado por ueventd, ou seja, /dev/block/platform/device-path/by-name/partition-name. Os dispositivos lançados com o Android 12 ou versões mais recentes precisam usar o bootconfig para transmitir androidboot.boot_devices para init.

Por exemplo, se o link simbólico da superpartilha com nome for /dev/block/platform/soc/100000.ufshc/by-name/super, adicione o parâmetro de linha de comando no arquivo BoardConfig.mk da seguinte forma:

BOARD_KERNEL_CMDLINE += androidboot.boot_devices=soc/100000.ufshc
É possível adicionar o parâmetro bootconfig no arquivo BoardConfig.mk da seguinte maneira:
BOARD_BOOTCONFIG += androidboot.boot_devices=soc/100000.ufshc

mudanças no fstab

A árvore de dispositivos e as sobreposições da árvore de dispositivos não podem conter entradas fstab. Use um arquivo fstab que fará parte do ramdisk.

É preciso fazer alterações no arquivo fstab para partições lógicas:

  • O campo flags fs_mgr precisa incluir a flag logical e a flag first_stage_mount, introduzida no Android 10, que indica que uma partição precisa ser montada na primeira etapa.
  • Uma partição pode especificar avb=vbmeta partition name como uma flag fs_mgr e, em seguida, a partição vbmeta especificada é inicializada pelo primeiro estágio init antes de tentar montar qualquer dispositivo.
  • O campo dev precisa ser o nome da partição.

As entradas do fstab a seguir definem o sistema, o fornecedor e o produto como partições lógicas seguindo as regras acima.

#<dev>  <mnt_point> <type>  <mnt_flags options> <fs_mgr_flags>
system   /system     ext4    ro,barrier=1        wait,slotselect,avb=vbmeta,logical,first_stage_mount
vendor   /vendor     ext4    ro,barrier=1        wait,slotselect,avb,logical,first_stage_mount
product  /product    ext4    ro,barrier=1        wait,slotselect,avb,logical,first_stage_mount

Copie o arquivo fstab no ramdisk da primeira etapa.

Mudanças no SELinux

O dispositivo de bloco de superpartição precisa ser marcado com o rótulo super_block_device. Por exemplo, se o link simbólico de nome da superpartilha de nome for /dev/block/platform/soc/100000.ufshc/by-name/super, adicione a linha a seguir a file_contexts:

/dev/block/platform/soc/10000\.ufshc/by-name/super   u:object_r:super_block_device:s0

fastbootd

O carregador de inicialização (ou qualquer ferramenta de flash que não seja do espaço do usuário) não entende partições dinâmicas, portanto, não pode fazer o flash delas. Para resolver esse problema, os dispositivos precisam usar uma implementação do espaço do usuário do protocolo fastboot, chamada fastbootd.

Para mais informações sobre como implementar o fastbootd, consulte Como mover o fastboot para o espaço do usuário.

Remontagem do adb

Para desenvolvedores que usam builds eng ou userdebug, adb remount é extremamente útil para iterações rápidas. As partições dinâmicas representam um problema para adb remount porque não há mais espaço livre em cada sistema de arquivos. Para resolver isso, os dispositivos podem ativar overlayfs. Enquanto houver espaço livre na superpartição, adb remount criará automaticamente uma partição dinâmica temporária e usará overlayfs para gravações. A partição temporária é chamada de scratch. Portanto, não use esse nome para outras partições.

Para mais informações sobre como ativar overlayfs, consulte o README de overlayfs (link em inglês) no AOSP.

Fazer upgrade de dispositivos Android

Se você fizer upgrade de um dispositivo para o Android 10 e quiser incluir o suporte a partições dinâmicas no OTA, não será necessário mudar a tabela de partições integrada. É necessária alguma configuração extra.

Mudanças na configuração do dispositivo

Para fazer o retrofit do particionamento dinâmico, adicione as seguintes flags em device.mk:

PRODUCT_USE_DYNAMIC_PARTITIONS := true
PRODUCT_RETROFIT_DYNAMIC_PARTITIONS := true

Mudanças na configuração do board

É necessário definir as seguintes variáveis de placa:

  • Defina BOARD_SUPER_PARTITION_BLOCK_DEVICES como a lista de dispositivos de bloco usados para armazenar extensões de partições dinâmicas. Esta é a lista de nomes de partições físicas existentes no dispositivo.
  • Defina BOARD_SUPER_PARTITION_partition_DEVICE_SIZE como os tamanhos de cada dispositivo de bloco em BOARD_SUPER_PARTITION_BLOCK_DEVICES, respectivamente. Esta é a lista de tamanhos das partições físicas existentes no dispositivo. Geralmente, é BOARD_partitionIMAGE_PARTITION_SIZE em configurações de placa atuais.
  • Desmarque BOARD_partitionIMAGE_PARTITION_SIZE existente para todas as partições em BOARD_SUPER_PARTITION_BLOCK_DEVICES.
  • Defina BOARD_SUPER_PARTITION_SIZE como a soma de BOARD_SUPER_PARTITION_partition_DEVICE_SIZE.
  • Defina BOARD_SUPER_PARTITION_METADATA_DEVICE como o dispositivo de bloco em que os metadados de partição dinâmica são armazenados. Precisa ser um destes BOARD_SUPER_PARTITION_BLOCK_DEVICES. Geralmente, ele é definido como system.
  • Defina BOARD_SUPER_PARTITION_GROUPS, BOARD_group_SIZE e BOARD_group_PARTITION_LIST, respectivamente. Consulte Mudanças na configuração da placa em novos dispositivos para saber mais.

Por exemplo, se o dispositivo já tiver partições do sistema e do fornecedor e você quiser convertê-las em partições dinâmicas e adicionar uma nova partição de produto durante a atualização, defina esta configuração de placa:

BOARD_SUPER_PARTITION_BLOCK_DEVICES := system vendor
BOARD_SUPER_PARTITION_METADATA_DEVICE := system

# Rename BOARD_SYSTEMIMAGE_PARTITION_SIZE to BOARD_SUPER_PARTITION_SYSTEM_DEVICE_SIZE.
BOARD_SUPER_PARTITION_SYSTEM_DEVICE_SIZE := <size-in-bytes>

# Rename BOARD_VENDORIMAGE_PARTITION_SIZE to BOARD_SUPER_PARTITION_VENDOR_DEVICE_SIZE
BOARD_SUPER_PARTITION_VENDOR_DEVICE_SIZE := <size-in-bytes>

# This is BOARD_SUPER_PARTITION_SYSTEM_DEVICE_SIZE + BOARD_SUPER_PARTITION_VENDOR_DEVICE_SIZE
BOARD_SUPER_PARTITION_SIZE := <size-in-bytes>

# Configuration for dynamic partitions. For example:
BOARD_SUPER_PARTITION_GROUPS := group_foo
BOARD_GROUP_FOO_SIZE := <size-in-bytes>
BOARD_GROUP_FOO_PARTITION_LIST := system vendor product

Mudanças no SELinux

Os dispositivos de bloco de superpartição precisam ser marcados com o atributo super_block_device_type. Por exemplo, se o dispositivo já tiver partições system e vendor, você vai querer usá-las como dispositivos em bloco para armazenar extensões de partições dinâmicas, e os links simbólicos por nome são marcados como system_block_device:

/dev/block/platform/soc/10000\.ufshc/by-name/system   u:object_r:system_block_device:s0
/dev/block/platform/soc/10000\.ufshc/by-name/vendor   u:object_r:system_block_device:s0

Em seguida, adicione a seguinte linha a device.te:

typeattribute system_block_device super_block_device_type;

Para outras configurações, consulte Como implementar partições dinâmicas em novos dispositivos.

Para ver mais informações sobre atualizações de retrofit, consulte OTA para dispositivos A/B sem partições dinâmicas.

Imagens de fábrica

Para um dispositivo inicializado com suporte a partições dinâmicas, evite usar o fastboot do espaço do usuário para flashear imagens de fábrica, porque a inicialização no espaço do usuário é mais lenta que outros métodos de flash.

Para resolver esse problema, o make dist agora cria uma imagem super.img adicional que pode ser instalada diretamente na super partição. Ele agrupa automaticamente o conteúdo de partições lógicas, ou seja, contém system.img, vendor.img e assim por diante, além dos metadados da partição super. Essa imagem pode ser atualizada diretamente para a partição super sem nenhuma ferramenta adicional ou usando o fastbootd. Depois da criação, super.img é colocado em ${ANDROID_PRODUCT_OUT}.

Para dispositivos A/B que são iniciados com partições dinâmicas, super.img contém imagens no slot A. Depois de atualizar a superimagem diretamente, marque o slot A como inicializável antes de reinicializar o dispositivo.

Para dispositivos de retrofit, o make dist cria um conjunto de imagens super_*.img que podem ser atualizadas diretamente nas partições físicas correspondentes. Por exemplo, make dist cria super_system.img e super_vendor.img quando BOARD_SUPER_PARTITION_BLOCK_DEVICES é o fornecedor do sistema. Essas imagens são colocadas na pasta OTA em target_files.zip.

Otimização do dispositivo de armazenamento do Device Mapper

O particionamento dinâmico acomoda vários objetos de mapeador de dispositivo não determinísticos. Nem todas elas são instanciadas conforme esperado. Portanto, é necessário rastrear todas as montagens e atualizar as propriedades do Android de todas as partições associadas com os dispositivos de armazenamento subjacentes.

Um mecanismo dentro de init rastreia as montagens e atualiza de forma assíncrona as propriedades do Android. Não há garantia de que esse tempo será dentro de um período específico. Portanto, forneça tempo suficiente para que todos os gatilhos on property reajam. As propriedades são dev.mnt.blk.<partition>, em que <partition> é root, system, data ou vendor, por exemplo. Cada propriedade é associada ao nome do dispositivo de armazenamento básico, conforme mostrado nestes exemplos:

taimen:/ % getprop | grep dev.mnt.blk
[dev.mnt.blk.data]: [sda]
[dev.mnt.blk.firmware]: [sde]
[dev.mnt.blk.metadata]: [sde]
[dev.mnt.blk.persist]: [sda]
[dev.mnt.blk.root]: [dm-0]
[dev.mnt.blk.vendor]: [dm-1]

blueline:/ $ getprop | grep dev.mnt.blk
[dev.mnt.blk.data]: [dm-4]
[dev.mnt.blk.metadata]: [sda]
[dev.mnt.blk.mnt.scratch]: [sda]
[dev.mnt.blk.mnt.vendor.persist]: [sdf]
[dev.mnt.blk.product]: [dm-2]
[dev.mnt.blk.root]: [dm-0]
[dev.mnt.blk.system_ext]: [dm-3]
[dev.mnt.blk.vendor]: [dm-1]
[dev.mnt.blk.vendor.firmware_mnt]: [sda]

A linguagem init.rc permite que as propriedades do Android sejam expandidas como parte das regras, e os dispositivos de armazenamento podem ser ajustados pela plataforma conforme necessário com comandos como estes:

write /sys/block/${dev.mnt.blk.root}/queue/read_ahead_kb 128
write /sys/block/${dev.mnt.blk.data}/queue/read_ahead_kb 128

Quando o processamento do comando começa no init do segundo estágio, o epoll loop fica ativo e os valores começam a ser atualizados. No entanto, como os acionadores de propriedade não ficam ativos até o final da init, eles não podem ser usados nas fases iniciais de inicialização para processar root, system ou vendor. O read_ahead_kb padrão do kernel pode ser suficiente até que os scripts init.rc possam substituir em early-fs (quando vários daemons e recursos são iniciados). Portanto, o Google recomenda que você use o recurso on property com uma propriedade controlada por init.rc, como sys.read_ahead_kb, para lidar com o tempo das operações e evitar condições de corrida, como nestes exemplos:

on property:dev.mnt.blk.root=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.root}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on property:dev.mnt.blk.system=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.system}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on property:dev.mnt.blk.vendor=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.vendor}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on property:dev.mnt.blk.product=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.system_ext}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on property:dev.mnt.blk.oem=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.oem}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on property:dev.mnt.blk.data=* && property:sys.read_ahead_kb=*
    write /sys/block/${dev.mnt.blk.data}/queue/read_ahead_kb ${sys.read_ahead_kb:-2048}

on early-fs:
    setprop sys.read_ahead_kb ${ro.read_ahead_kb.boot:-2048}

on property:sys.boot_completed=1
   setprop sys.read_ahead_kb ${ro.read_ahead_kb.bootcomplete:-128}