squashfs에 crypto 압축 알고리즘 활용

Embedded SoC 중에는 HW 압축 Unit을 내장한 것들이 있고, 이런 유닛은 대개 리눅스 커널의 crypto subsystem에 압축 알고리즘으로 등록돼 zram 같은 모듈에서 바로 활용할 수 있다. 문제는 squashfs다. AppImage, snap, 라이브 CD/USB, 임베디드 rootfs 이미지에 널리 쓰이는 squashfs는 zlib·xz·lzo·lz4·zstd 압축을 지원하지만, 각 wrapper가 압축 라이브러리 함수를 직접 호출할 뿐 crypto subsystem을 전혀 거치지 않는다. SoC에 HW 압축 유닛이 crypto API로 이미 등록돼 있어도 squashfs는 지금 구조로는 그 존재조차 알 방법이 없다는 뜻이다.

반면 같은 읽기 전용 이미지 파일시스템인 EROFS는 EROFS_FS_ZIP_ACCEL 옵션으로 crypto API 기반 하드웨어 압축 해제를 이미 메인라인에 넣어뒀다. 이 글에서는 커널 crypto API의 acomp(비동기 압축) 인터페이스가 어떻게 동작하는지, EROFS가 이를 실제로 어떻게 쓰는지 살펴보고, 같은 방식을 squashfs의 압축 알고리즘 wrapper에 적용하는 방법을 정리한다.

커널 crypto API의 하드웨어 압축 오프로드 — acomp

리눅스 커널의 crypto subsystem은 원래 암호화용으로 만들어졌지만, deflate·lzo·lz4·842·zstd 같은 무손실 압축 알고리즘도 “compression” 타입으로 등록해 통합된 인터페이스로 다룰 수 있게 지원한다. 초기에는 crypto_alloc_comp()/crypto_comp_compress() 같은 동기(synchronous) 인터페이스만 있었는데, HiSilicon ZIP이나 Intel IAA처럼 압축 연산 자체를 오프로드하는 하드웨어 가속기가 늘면서 요청을 비동기로 처리할 수 있는 acomp(asynchronous compression) API가 새로 도입됐다. 지금은 소프트웨어 알고리즘도 scomp라는 동기 shim을 거쳐 acomp 프런트엔드로 통합돼 있어서, 호출하는 쪽 코드는 뒤에 하드웨어 가속기가 있는지 소프트웨어 구현만 있는지 신경 쓸 필요가 없다.

사용법은 tfm(transform)을 알고리즘 이름으로 할당하고, 입력·출력 버퍼를 scatterlist로 감싼 뒤 요청(request)에 파라미터를 설정하고 실행하는 흐름이다. 커널의 zswap(mm/zswap.c)이 스왑 아웃 페이지를 압축할 때 정확히 이 API를 쓴다.

struct crypto_acomp *acomp = crypto_alloc_acomp_node(tfm_name, 0, 0, node);
struct acomp_req *req = acomp_request_alloc(acomp);
struct crypto_wait wait;

crypto_init_wait(&wait);
acomp_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG,
                            crypto_req_done, &wait);

sg_init_table(&input, 1);
sg_set_page(&input, src_page, PAGE_SIZE, 0);
sg_init_one(&output, dst_buf, PAGE_SIZE);

acomp_request_set_params(req, &input, &output, PAGE_SIZE, dst_len);
ret = crypto_wait_req(crypto_acomp_compress(req), &wait);

알고리즘 이름만 바꾸면 소프트웨어 deflate든, HiSilicon ZIP 같은 가속기가 등록한 “deflate”든 같은 코드로 처리된다. 어떤 tfm이 실제로 선택되는지는 크립토 드라이버가 어떤 우선순위(priority)로 등록돼 있느냐에 달려 있다.

EROFS는 이미 crypto API로 하드웨어 압축 해제를 지원한다

같은 읽기 전용 이미지 파일시스템인 EROFS는 EROFS_FS_ZIP_ACCEL이라는 Kconfig 옵션으로 이미 이 경로를 메인라인에 넣어뒀다. fs/erofs/Kconfig의 설명은 다음과 같다.

config EROFS_FS_ZIP_ACCEL
	bool "EROFS hardware decompression support"
	depends on EROFS_FS_ZIP
	help
	  Saying Y here includes hardware accelerator support for reading
	  EROFS file systems containing compressed data. It gives better
	  decompression speed than the software-implemented decompression,
	  and it costs lower CPU overhead.

	  Hardware accelerator support is an experimental feature for now
	  and file systems are still readable without selecting this option.

실제 구현은 fs/erofs/decompressor_crypto.c에 있다. z_erofs_crypto_decompress()가 알고리즘 이름으로 미리 준비해둔 crypto acomp 엔진을 가져오고, __z_erofs_crypto_decompress()가 압축된 페이지 배열과 출력 페이지 배열을 각각 scatterlist로 바꿔 요청을 실행한다.

tfm = z_erofs_crypto_get_engine(rq->alg);   // crypto_alloc_acomp(name, 0, 0)으로 미리 준비된 엔진

sg_alloc_table_from_pages_segment(&st_src, rq->in, rq->inpages,
        rq->pageofs_in, rq->inputsize, UINT_MAX, GFP_KERNEL);
sg_alloc_table_from_pages_segment(&st_dst, rq->out, rq->outpages,
        rq->pageofs_out, rq->outputsize, UINT_MAX, GFP_KERNEL);

req = acomp_request_alloc(tfm);
acomp_request_set_params(req, st_src.sgl, st_dst.sgl,
                          rq->inputsize, rq->outputsize);
crypto_init_wait(&wait);
acomp_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG,
                            crypto_req_done, &wait);

ret = crypto_wait_req(crypto_acomp_decompress(req), &wait);

페이지 배열을 통째로 scatterlist로 변환하는 sg_alloc_table_from_pages_segment()를 쓴다는 점만 빼면, 앞서 본 zswap의 acomp 사용 패턴과 흐름이 동일하다. 하드웨어 가속기가 없으면 crypto_alloc_acomp()가 소프트웨어 scomp 구현으로 폴백되므로, 이 코드 경로는 하드웨어 유무와 무관하게 항상 동작한다.

squashfs는 아직 crypto API를 거치지 않는다

squashfs는 압축 알고리즘마다 별도의 wrapper 파일을 둔다(fs/squashfs/zlib_wrapper.c, lzo_wrapper.c, xz_wrapper.c, lz4_wrapper.c, zstd_wrapper.c). 각 wrapper는 다음과 같은 squashfs_decompressor 인스턴스를 등록해, 마운트 시 superblock에 기록된 compression id로 어떤 decompressor를 쓸지 고른다.

struct squashfs_decompressor {
	void *(*init)(struct squashfs_sb_info *, void *);
	void *(*comp_opts)(struct squashfs_sb_info *, void *, int);
	void (*free)(void *);
	int (*decompress)(struct squashfs_sb_info *, void *,
		struct bio *, int, int, struct squashfs_page_actor *);
	int id;
	char *name;
	int alloc_buffer;
	int supported;
};

문제는 zlib_wrapper.cdecompress 구현이 zlib_inflate()를 직접 부른다는 점이다. lzo·lz4·xz·zstd wrapper도 마찬가지로 각자의 압축 라이브러리 함수를 곧바로 호출하고, crypto subsystem은 아예 거치지 않는다. 즉 SoC가 crypto 서브시스템에 deflate 하드웨어 가속기를 이미 등록해뒀다 해도, squashfs는 지금 구조로는 그 가속기의 존재조차 알 방법이 없다. EROFS가 decompressor_crypto.c 하나를 추가해 얻은 이득을, squashfs에서 보려면 같은 방식의 wrapper를 새로 만들어야 한다.

squashfs에 crypto acomp wrapper 추가하기

EROFS의 decompressor_crypto.c 패턴을 그대로 가져와 squashfs용 wrapper를 하나 더 만드는 것이 가장 현실적인 접근이다. init()에서 superblock에 기록된 압축 알고리즘 이름(zlib이면 crypto 쪽 이름인 “deflate”)으로 crypto_alloc_acomp()를 호출해 tfm을 준비해두고, decompress()에서 bio와 squashfs_page_actor가 가리키는 페이지들을 scatterlist로 바꿔 acomp 요청을 실행한 뒤, free()에서 crypto_free_acomp()로 정리하면 된다.

/* squashfs_decompressor.decompress 콜백으로 등록될 함수 (EROFS 방식을 응용한 예시) */
static int crypto_acomp_uncompress(struct squashfs_sb_info *msblk, void *strm,
				    struct bio *bio, int offset, int length,
				    struct squashfs_page_actor *actor)
{
	struct squashfs_crypto_stream *stream = strm;
	struct acomp_req *req;
	struct crypto_wait wait;
	struct sg_table st_src, st_dst;
	int error;

	/* bio에 담긴 압축 데이터, actor가 가리키는 출력 페이지를 각각 scatterlist로 변환 */
	error = squashfs_bio_to_sgtable(bio, offset, length, &st_src);
	if (error)
		return error;
	error = squashfs_actor_to_sgtable(actor, &st_dst);
	if (error) {
		sg_free_table(&st_src);
		return error;
	}

	req = acomp_request_alloc(stream->tfm);
	acomp_request_set_params(req, st_src.sgl, st_dst.sgl,
				  length, actor->length);
	crypto_init_wait(&wait);
	acomp_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG,
				    crypto_req_done, &wait);

	error = crypto_wait_req(crypto_acomp_decompress(req), &wait);

	acomp_request_free(req);
	sg_free_table(&st_src);
	sg_free_table(&st_dst);

	return error ? error : actor->length;
}

squashfs_bio_to_sgtable/squashfs_actor_to_sgtable은 실제 커널에 있는 함수가 아니라, EROFS의 sg_alloc_table_from_pages_segment() 호출을 squashfs의 bio·page actor 구조에 맞게 다시 구현해야 하는 부분을 표시해둔 이름이다. 이렇게 만든 wrapper는 fs/squashfs/decompressor.c의 decompressor 테이블에 등록하고, EROFS의 EROFS_FS_ZIP_ACCEL처럼 별도 Kconfig 옵션으로 켜고 끌 수 있게 노출하면 기존 zlib/xz/lzo/lz4/zstd wrapper와 나란히 둘 수 있다.

주의사항

  • EROFS의 EROFS_FS_ZIP_ACCEL조차 커널 문서에 “experimental feature”라고 명시돼 있다. squashfs에 이식한 wrapper도 처음에는 기존 zlib/xz wrapper와 나란히 두고 선택적으로 켜는 형태로 시작하는 편이 안전하다.
  • 대상 SoC에 실제 하드웨어 압축 드라이버가 없으면 crypto_alloc_acomp()는 scomp 소프트웨어 shim으로 폴백된다. 이 경우 scatterlist 구성과 request 할당 오버헤드만 추가되고 zlib_inflate()를 직접 부르는 기존 방식보다 오히려 느려질 수 있으므로, 도입 전에 반드시 하드웨어가 붙어 있는 상태에서 벤치마크로 이득을 확인해야 한다.
  • crypto_acomp_decompress()는 요청을 큐에 넣고 완료를 기다리는 구조라, squashfs의 decompress 콜백이 호출되는 컨텍스트가 sleep 가능한지 미리 확인해야 한다. readahead 경로처럼 이미 워커 컨텍스트에서 호출된다면 문제가 없지만, 원자적(atomic) 컨텍스트에서 호출되는 경로가 있다면 crypto_wait_req()로 그냥 기다릴 수 없다.
  • squashfs_decompressordecompress 콜백 시그니처는 커널 버전마다 바뀌어왔다(과거에는 bio 대신 page 배열을 직접 받았다). 실제로 작업할 때는 대상 커널의 fs/squashfs/decompressor.h를 다시 확인해야 한다.
  • 이 wrapper는 메인라인 squashfs에 없는 기능이라 out-of-tree 패치로 유지보수해야 한다. 커널을 올릴 때마다 decompressor.h/decompressor.c 변경 사항에 맞춰 리베이스하는 비용을 감안해야 한다.

마무리

커널 crypto API의 acomp 인터페이스는 zswap과 EROFS의 EROFS_FS_ZIP_ACCEL이 이미 증명했듯, 소프트웨어와 하드웨어 압축 구현을 같은 코드로 다룰 수 있게 해주는 추상화다. 반면 squashfs의 zlib/xz/lzo/lz4/zstd wrapper는 각자의 압축 라이브러리를 직접 호출하도록 짜여 있어 이 경로를 전혀 거치지 않는다. embedded SoC의 HW 압축 unit을 squashfs에서 활용하려면, EROFS의 decompressor_crypto.c 구조를 참고해 crypto acomp API를 호출하는 wrapper를 새로 작성하고 squashfs_decompressor 테이블에 등록하는 작업이 필요하다. 다만 하드웨어가 없으면 오히려 오버헤드만 늘 수 있으므로, 도입은 항상 실측 벤치마크로 소프트웨어 wrapper 대비 이득이 있는지 확인한 뒤에 결정해야 한다.

참고

답글 남기기