kakts-log

programming について調べたことを整理していきます

stdioバッファの方式について

概要

fork()による子プロセスを作成するプログラムにおいて、printfなどのstdioライブラリの関数を実行した場合、標準出力が端末とファイルの場合で出力結果が異なる場合があります。
この挙動について整理します。

(参考: LINUXプログラミングインタフェース25.4)

fork()とprintf()を利用した例

下記のように、fork()の前にprintfで文字列出力させ、その後fork()によって子プロセスを作成するプログラムを例とします。

#include "tlpi_hdr.h"
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    printf("Hello world\n");
    write(STDOUT_FILENO, "Ciao\n", 5);

    if (fork() == -1)
        errExit("fork");

    // 親子プロセスともにここまで実行する
    exit(EXIT_SUCCESS);
}

これを、標準出力を端末にして実行すると

$ ./fork_stdio_buf
Hello world
Ciao

という、予想通りの結果が得られます。

しかし、標準出力をファイルにしてリダイレクトすると、挙動が変わります。

root@3d890c1239c5:/tlpi/procexec# ./fork_stdio_buf >a.txt
root@3d890c1239c5:/tlpi/procexec# cat a.txt 
Ciao
Hello world
Hello world

ファイルに出力された結果を確認すると Hello worldが2回出力されてしまいます。

printf()実行時に内部で保持しているstdioバッファの扱いが変わるようです。

解説

上記の挙動を理解するために、まっずstdioバッファのバッファリング方式の違いを知る必要があります。

まず、stdioバッファは、カーネル空間でなく、ユーザ空間で扱われるデータとなります。

そして下記のように標準出力先によって、バッファリング方式が変わります。

  • 端末: 行バッファリング
  • ファイル: ブロックバッファリング

行バッファリングの場合、上記のプログラムでprintfに対して改行文字を含んだ文字列を渡しているため、改行によりその場で表示されます。
逆に、ブロックバッファリングの場合だと、改行があっても表示されず親プロセスのstdioバッファにデータが止まります。

そして、fork()の実行により子プロセスにも、親プロセスのstdioバッファの内容が複製されます。
最後に、親子プロセス両方でexit()を実行する際に、それぞれがもっているstdioバッファの内容をフラッシュすることになり、結果として同じ文字列が二度表示されることになります。

write()による出力では、データはユーザ空間でなく、カーネルのバッファへ直接送信されることになります。 fork()では、カーネルのバッファの内容は複製されないため、2度出力されることはありません。

まとめ

まとめると、同じファイルに対するstdioの関数とシステムコールの併用をする場合には注意が必要となります。 stdioがユーザ空間で扱っているバッファの扱いと、カーネル空間のバッファを直接扱うシステムコールの挙動の理解が重要です。

C: シグナルハンドラ内で非リエントラントな関数を扱った際の挙動

概要

Cにおいて、シグナルハンドラ関数内で非リエントラントな関数を実行した際に意図しない挙動が生じる問題についてかんたんに整理します。

リエントラント: Reentrant 再入可能 - マルチスレッド安全なもの
非リエントラント: Non-Reentrant 再入不可 - リエントラントとは逆で、複数のスレッドから同時に実行した場合、予期せぬ結果を引き起こす可能性があるもの

シグナルハンドラで不整合が起きる例

シグナルハンドラ関数内で非リエントラントな関数であるcrypt(3)を実行した例を挙げ、この場合の注意点について整理します。

crypt(3)について

Cの標準ライブラリで用意されているcrypt(3)は、文字列の暗号化を行う関数で、非リエントラントな関数となります。

crypt(3)実行時、関数内部でstaticな変数を操作するため、複数のスレッドで同時に実行された際に、そのstaticな変数を同時に操作されることにより、結果として意図せぬ結果となります。

crypt_r(3)は、crypt(3)のリエントラント対応版の関数となります。

Man page of CRYPT

シグナルハンドラでcrpyt(3)を使った例

ここでシグナルハンドラ関数内とmain関数内でcrypt(3)を使用した際に生じる不具合を確認します。

/**
 * @file nonreentrant.c
 * 21-1 main()とシグナルハンドラの双方でnon-reentrant関数を呼び出す
 */

// 600以上の値で定義するとSUSv3関連の定義とC99での定義が追加で公開される
#if ! defined(_XOPEN_SOURCE) || _XOPEN_SOURCE < 600
#define _XOPEN_SOURCE 600
#endif

#include <unistd.h>
#include <signal.h>
#include <string.h>
#include "../lib/tlpi_hdr.h"

// argv[2]の文字列
static char *str2;

// ハンドラ実行回数
static int handled = 0;

/**
 * シグナルハンドラ
 * str2 をcrypt()で暗号化する
 */
static void handler(int sig)
{
    crypt(str2, "xx");
    handled++;
}

int main(int argc, char *argv[])
{
    char *cr1;

    int callNum, mismatch;
    struct sigaction sa;

    // 引数チェック
    if (argc != 3) {
        usageErr("%s str1 str2\n", argv[0]);
    }

    // argv[2]をstatic変数に代入し、ハンドラから使用可能にする
    str2 = argv[2];
    
    // staticに割り当てられた文字列を別メモリへコピー
    cr1 = strdup(crypt(argv[1], "xx"));

    if (cr1 == NULL) {
        errExit("strdup");
    }

    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = handler;
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        errExit("sigaction");
    }

    // argv[1]を繰り返しcrypt()で暗号化する。
    // シグナルハンドラが割り込むと、crypt()内部のstatic memoryがargv[2]の暗号化結果に上書きされ、
    // strcmp()がcr1との不一致を検出する

    for (callNum = 1, mismatch = 0; ; callNum++) {
        
        // すでにargv[1]を暗号化した文字列cr1 と 再度argv[1]を暗号化した文字列を比較する
        if (strcmp(crypt(argv[1], "xx"), cr1) != 0) {
            mismatch++;
            printf("Mismatch on call %d (mismatch=%d handled=%d)\n", callNum, mismatch, handled);
        }
    }
}

ここでは主に以下の処理を行っています。

  1. 実行時に文字列を2つ受け取る。str1, str2とする
  2. main関数内でstr1に対してcrypt(3)を実行し、変数に代入する
  3. SIGINTに対するシグナルハンドラ関数内で、str2に対してcrypt(3)を実行する
  4. main関数内のループで、2とは別にstr1に対してcrypt(3)を実行した結果と、2で変数に代入した文字列を比較する。
  5. プログラム実行中に、SIGINTを送信し、4の処理で不整合が起きるかをチェックする

SIGINTを受け取った際、シグナルハンドラは別スレッドで実行された際に、crypt(3)が実行されます。
このときcrypt(3)内で扱っているstaticな変数に対する競合が発生し、4で実行しているcrypt(3)が意図しない結果となり、不整合が起きることを確認するものです。 (Linuxプログラミングインタフェース 21-1参照)

コード実行後、SIGINTを投げるたびに文字列が一致せず、ログに出ていることが確認できます。

root@8db89fb5ba52:/tlpi/signals# ./nonreentrant a b
^CMismatch on call 352950 (mismatch=1 handled=1)
^CMismatch on call 1839887 (mismatch=2 handled=2)

非同期シグナルセーフな関数

シグナルハンドラを扱うにあたって上述した問題が発生しないことを保証するために、Linuxの標準仕様において、非同期シグナルセーフな関数が指定されています。

docs.oracle.com

www.jpcert.or.jp

シグナルハンドラ関数を扱う際の注意点まとめ

上記のような問題が発生するため、シグナルハンドラ関数では、以下を満たすように注意が必要です

  • シグナルハンドラ関数内ではリエントラントであるようにする、もしくは上述した非同期シグナルセーフな関数のみを使用する。
  • メインプログラムが下記の処理を行う際は、シグナルをブロックする
    • 非同期シグナルセーフでない関数の実行
    • シグナルハンドラも変更するグローバルデータの変更

inotify_add_watch()によるファイル・ディレクトリ変更検知イベントを受信した際の挙動について

概要

linuxシステムコールでのinotify_add_watch(),によるファイル、ディレクトリの変更検知を行った際の挙動を整理する。

監視対象として特定のディレクトリを指定した場合、変更イベントの内容を保持するinotify_event構造体のname とlenにそれぞれ変更があったファイル名とファイル名の長さが入る。
しかし、監視対象としてディレクトリでなく特定のファイルを指定した場合、inotify_event->nameとlenには値が入らない

参考

Man page of INOTIFY

監視対象のディレクトリ内のオブジェクトに対してイベントが発生した場合、 inotify_event 構造体で返される name フィールドは、ディレクトリ内のファイル名を表す。

検証

inotify_add_watchの第2引数に、それぞれ特定のファイル・特定のディレクトリを指定した場合にinotify_event構造体の値がどうなるかを検証する

2つの例を挙げるが、主に異なるのは、inotify_add_watchの引数となる

wd = inotify_add_watch(inotifyFd, WATCH_TARGET_FILE, IN_ALL_EVENTS);

1: 特定のディレクトリを監視対象に指定した場合

`
|-- inotify_logger_directory
|-- log.txt
|-- target // 監視対象ディレクトリ
    `-- target.txt
    `-- target2.txt

コード:

tlpi/tlpi/inotify/inotify_logger_directory.c at master · kakts/tlpi · GitHub

/**
 * inotifyを使ったファイル変更検知ロガー
 * 
 * inotifyによる監視対象
 * 特定のファイルを指定
 */

#include <sys/inotify.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include "../lib/tlpi_hdr.h"

#define WATCH_TARGET_DIR "./target"
#define LOG_FILE "./log.txt"

/**
 * 指定したファイルディスクリプタが指すファイルにinotify_event構造体の内容を書き込む
 */
static void writeToLog(int fd, struct inotify_event *p)
{
    printf("writeToLog\n");
    printf("p->len:%d\n", p->len);
    printf("p->name:%s\n", p->name);
    ssize_t numWrite;

    // write()を使ってp->nameをファイルに書き込む
    numWrite = write(fd, p->name, p->len);
    if (numWrite == -1) {
        errExit("write");
    }

    // 改行文字を書き込む
    numWrite = write(fd, "\n", 1);
    if (numWrite == -1) {
        errExit("write");
    }

    printf("Write %ld bytes to log file. name:%s\n", (long) numWrite, p->name);
}

/**
 * read()に指定したバッファサイズ小さく、次のinotify_event構造体を読み込めない場合がある
 * これを回避するために、inotify_eventを最低でも1つは保持できるだけのサイズ(sizeof(struct inotify_event) + NAME_MAX + 1)を確保すれば良い
 */
#define BUF_LEN (10 * sizeof(struct inotify_event) + NAME_MAX + 1)


int main(int argc, char *argv[])
{
    int inotifyFd, logFd, wd, j;
    char buf[BUF_LEN];
    ssize_t numRead;
    char *p;
    struct inotify_event *event;

    // inotifyインスタンスを作成
    inotifyFd = inotify_init();
    if (inotifyFd == -1) {
        close(inotifyFd);
        errExit("inotify_init");
    
    }

    // 変更検知対象のディレクトリを指定
    // 実行ファイルと同一ディレクトリのファイルを監視する場合は、nameが入らない
    wd = inotify_add_watch(inotifyFd, WATCH_TARGET_DIR, IN_ALL_EVENTS);
    if (wd == -1) {
        errExit("inotify_add_watch");
    }
    printf("Watching %s using wd %d\n", "target.txt", wd);

    // ログ出力用のファイルを開く
    // 書き込む場合はファイル末尾へ追加する
    logFd = open(LOG_FILE, O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
    if (logFd == -1) {
        errExit("open");
    }



    // イベント処理用の無限ループ
    for (;;) {
        numRead = read(inotifyFd, buf, BUF_LEN);
        if (numRead == 0) {
            fatal("read() from inotify fd returned 0!");
        }

        if (numRead == -1) {
            errExit("read");
        }

        printf("Read %ld bytes from inotify fd\n", (long) numRead);

        // 読み込んだバッファの内容をinotify_event構造体にキャストして表示
        event = (struct inotify_event *) buf;
        writeToLog(logFd, event);
    }

    exit(EXIT_SUCCESS);
}

出力結果は下記となる。 ./target ディレクトリ配下のファイルを操作すると、操作したファイルの名前の情報がinotify_eventに渡っているのを確認できます。

Read 32 bytes from inotify fd
writeToLog
p->len:16
p->name:target.txt
Write 1 bytes to log file. name:target.txt
Read 32 bytes from inotify fd

....
writeToLog
p->len:16
p->name:target2.txt
Write 1 bytes to log file. name:target2.txt
Read 48 bytes from inotify fd
writeToLog
p->len:32

2: 特定のファイルを監視対象に指定した場合

root@a0cd1290306f:/tlpi/inotify# tree
.
|
|-- inotify_logger
|-- log.txt // log出力先
`-- target.txt // 監視対象ファイル

上記のような構成で、./target.txtに対して変更があった場合、変更情報を./log.txtに出力させます。

コード tlpi/tlpi/inotify/inotify_logger.c at master · kakts/tlpi · GitHub

/**
 * inotifyを使ったファイル変更検知ロガー
 * 
 * inotifyによる監視対象
 * 特定のファイルを指定
 */

#include <sys/inotify.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include "../lib/tlpi_hdr.h"

#define WATCH_TARGET_FILE "./target.txt"
#define LOG_FILE "./log.txt"

/**
 * 指定したファイルディスクリプタが指すファイルにinotify_event構造体の内容を書き込む
 */
static void writeToLog(int fd, struct inotify_event *p)
{
    printf("writeToLog\n");
    printf("p->len:%d\n", p->len);
    printf("p->name:%s\n", p->name);
    ssize_t numWrite;

    // write()を使ってp->nameをファイルに書き込む
    numWrite = write(fd, LOG_FILE, strlen(LOG_FILE));
    if (numWrite == -1) {
        errExit("write");
    }

    // 改行文字を書き込む
    numWrite = write(fd, "\n", 1);
    if (numWrite == -1) {
        errExit("write");
    }

    printf("Write %ld bytes to log file. name:%s\n", (long) numWrite, p->name);
}

/**
 * read()に指定したバッファサイズ小さく、次のinotify_event構造体を読み込めない場合がある
 * これを回避するために、inotify_eventを最低でも1つは保持できるだけのサイズ(sizeof(struct inotify_event) + NAME_MAX + 1)を確保すれば良い
 */
#define BUF_LEN (10 * sizeof(struct inotify_event) + NAME_MAX + 1)


int main(int argc, char *argv[])
{
    int inotifyFd, logFd, wd, j;
    char buf[BUF_LEN];
    ssize_t numRead;
    char *p;
    struct inotify_event *event;

    // inotifyインスタンスを作成
    inotifyFd = inotify_init();
    if (inotifyFd == -1) {
        close(inotifyFd);
        errExit("inotify_init");
    
    }

    // 変更検知対象のファイルを指定
    // 実行ファイルと同一ディレクトリのファイルを監視する場合は、nameが入らない
    wd = inotify_add_watch(inotifyFd, WATCH_TARGET_FILE, IN_ALL_EVENTS);
    if (wd == -1) {
        errExit("inotify_add_watch");
    }
    printf("Watching %s using wd %d\n", "target.txt", wd);

    // ログ出力用のファイルを開く
    // 書き込む場合はファイル末尾へ追加する
    logFd = open(LOG_FILE, O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
    if (logFd == -1) {
        errExit("open");
    }



    // イベント処理用の無限ループ
    for (;;) {
        numRead = read(inotifyFd, buf, BUF_LEN);
        if (numRead == 0) {
            fatal("read() from inotify fd returned 0!");
        }

        if (numRead == -1) {
            errExit("read");
        }

        printf("Read %ld bytes from inotify fd\n", (long) numRead);

        // 読み込んだバッファの内容をinotify_event構造体にキャストして表示
        event = (struct inotify_event *) buf;
        writeToLog(logFd, event);
    }

    exit(EXIT_SUCCESS);
}

出力結果は下記となる。

writeToLog
p->len:0
p->name:
Write 1 bytes to log file. 

特定のファイルを指定した場合はlen, nameに値が入ってこないのを確認できました。

Golangでファイル監視を行う

概要

Goでファイル監視の方法についての記事です。
Goの標準パッケージではファイル監視の機能は提供されていないが、 github.com/fsnotify/fsnotify を使ったファイル監視のやり方をまとめます。

fsnotifyについて

github.com/fsnotify/fsnotify
各プラットフォーム・OSに対応したファイル変更通知機能(inotifyシステムコールなど)を提供するライブラリです。

主な使い方

ざっくり整理すると、以下の流れになります。

  • ファイル監視用のwatcherを作成する。
  • watcher.Addにより監視対象とするパスを追加
    1. ディレクトリを指定した場合: その配下のファイル全てが監視対象となる
    2. 特定のファイルを指定した場合: 複数のツールがファイルをアトミックに更新するため、一般的に推奨されない この場合、特定のファイルのみを監視対象とする場合、1のディレクトリ指定を行い、ファイル変更イベントが発生した際に、Event.Nameに変更があったファイルのパスが入るため、それをチェックして処理を行うのが推奨されます。
  • watcherがEvents, Errorsというチャネルを持っており、特定のファイルに対するイベントが発生した場合、チャネルにメッセージが送信されるのでそれを受信して処理を行う。

下記から、指定したディレクトリ配下の特定のファイルへのイベントを監視するコードについて説明します。

特定のファイルのみ更新したい場合

上記にあるように、watcher.Addで特定のファイルを指定するのでなく、そのファイルが属するディレクトリを指定し、Eventを監視し、Event.Nameをチェックして必要なファイルのみフィルタリングしてください。

./tmp ディレクトリ配下のtarget.txt への変更があった場合にEventを表示させる例を示します。

// fsnotifyで特定のファイルを監視

package main

import (
    "log"

    "github.com/fsnotify/fsnotify"
)

func main() {
    watcher, err := fsnotify.NewWatcher()
    if err != nil {
        log.Fatal(err)
    }
    defer watcher.Close()

    go func() {
        for {
            select {
            case event, ok := <-watcher.Events:
                if !ok {
                    log.Println("watcher.Events is not ok")
                    return
                }

                // 特定のファイルへイベントが発生した場合
                if event.Name == "tmp/target.txt" {
                    log.Println("Event: ", event)
                } else {
                    log.Println("not target file", event.Name)
                }

            case err, ok := <-watcher.Errors:
                if !ok {
                    log.Println("watcher.Errors is not ok")
                    return
                }
                log.Println("error:", err)
            }
        }
    }()

    err = watcher.Add("./tmp")
    if err != nil {
        log.Fatal(err)
    }
    <-make(chan struct{})
}

case event, ok := <-watcher.Events: でwatcher.Eventsのchannelからのメッセージを受信した際の処理で、eventを受けとるので、ここからevent.Nameをチェックしています。

といった感じで指定したファイルへの変更イベントを拾うことができました。

os.Exit()とdeferされた関数について

概要

os.Exit()を実行した際、プログラムが即座に終了するため、defer された関数が呼ばれない。
これについて整理します。

os.Exit()

ドキュメントを確認すると、defer された関数が呼ばれないことも明記されている

Exit causes the current program to exit with the given status code. Conventionally, code zero indicates success, non-zero an error. The program terminates immediately; deferred functions are not run.

For portability, the status code should be in the range [0, 125].

os.Exitの内部処理としては、システムコール exitを発行することにより、プログラムを終了させている。

  • os.Exitでは
// Exit causes the current program to exit with the given status code.
// Conventionally, code zero indicates success, non-zero an error.
// The program terminates immediately; deferred functions are not run.
//
// For portability, the status code should be in the range [0, 125].
func Exit(code int) {
    if code == 0 && testlog.PanicOnExit0() {
        // We were told to panic on calls to os.Exit(0).
        // This is used to fail tests that make an early
        // unexpected call to os.Exit(0).
        panic("unexpected call to os.Exit(0) during test")
    }

    // Inform the runtime that os.Exit is being called. If -race is
    // enabled, this will give race detector a chance to fail the
    // program (racy programs do not have the right to finish
    // successfully). If coverage is enabled, then this call will
    // enable us to write out a coverage data file.
    runtime_beforeExit(code)

    syscall.Exit(code)
}
  • 最終的にはruntimeパッケージのsyscall_exit()により、exitシステムコールが呼ばれる

https://github.com/golang/go/blob/master/src/runtime/runtime.go#L63-L67

func syscall_Exit(code int) {
    exit(int32(code))
}

log.Fatal時挙動について

os.Exitを内部で読んでいるlog.Fatal系の処理についてはどうでしょうか。
log.Fatalでは内部でos.Exitが呼ばれ、ログ出力と合わせてプログラムが終了します。
os.Exit()が呼ばれるため、上述した通り、deferで指定された関数は呼ばれずにプログラムが終了するので注意です。

https://cs.opensource.google/go/go/+/refs/tags/go1.21.2:src/log/log.go;l=284

JavaScript Primer改訂第2版の内容レビューに参加しました。

先月出版された「JavaScript Primer 改訂2版 迷わないための入門書」の内容レビューに関わらせていただきました。
先日出版社の方から、完成した書籍をご恵贈いただきました。ありがとうございます。

www.kadokawa.co.jp

efcl.info

第1版からの主なアップデートとして、ECMAScriptの新しいバージョン(ES2020, ES2021, ES2022)で取り入れられた機能を取り上げたり、Promise, Async Functionまわりについて書き直されたり、JavaScriptを初心者や、以前は触っていたが最近のアップデート内容のキャッチアップができていなかった方にも大変お勧めできます。 後半の応用編で、アプリを作成する章もあり、自分で手を動かしながら読んでいくと理解が深まるかと思います。

書籍だけでなく、web版もあるので是非気になったら読んでみてください。

jsprimer.net

JavaScript Primer 改訂2版 迷わないための入門書」の目次

はじめに
著者紹介
第1部 基本文法
第1章 JavaScriptとは
第2章 コメント
第3章 変数と宣言
第4章 値の評価と表示
第5章 データ型とリテラル
第6章 演算子
第7章 暗黙的な型変換
第8章 関数と宣言
第9章 文と式
第10章 条件分岐
第11章 ループと反復処理
第12章 オブジェクト
第13章 プロトタイプオブジェクト
第14章 配列
第15章 文字列
第16章 文字列とUnicode
第17章 ラッパーオブジェクト
第18章 関数とスコープ
第19章 関数とthis
第20章 クラス
第21章 例外処理
第22章 非同期処理: Promise/Async Function
第23章 Map/Set
第24章 JSON
第25章 Date
第26章 Math
第27章 ECMAScriptモジュール
第28章 ECMAScript
第2部 ユースケース
第29章 アプリケーション開発の準備
第30章 ユースケース: Ajax通信
第31章 ユースケース: Node.jsでCLIアプリケーション
第32章 ユースケース: Todoアプリケーション
付録A 参考リンク集

おわりに

一通り内容のレビューに関わらせてもらって、改めて最新のESの仕様についても自ら理解が深まりました。また、レビューをしてみて、改めて内容の密度の濃さを実感しており、 微力ながら、素晴らしい技術書のレビューに関わらせていただき、1エンジニアとして大変光栄です。

快くレビューの応募を受け入れていただいたazuさんはじめ出版社の方には大変感謝しております。

今後も何かしらで技術書の出版に関われたらとおもい、個人的に活動を続けて参ります。

RPMのgpgkeyの確認方法

概要

yumでのpackage installでPublic keyに関するエラーが出た際に、RPMのgpgkeyの確認方法が気になったのでまとめてみた

gpgkeyの確認方法

インスタンス内でインストールされているyum package用のgpgkeyの確認は下記方法でできる

 rpm -q gpg-pubkey --qf '%{NAME}-%{VERSION}-%{RELEASE}\t%{SUMMARY}\n'

rpm -q gpg-pubkey --qf '%{NAME}-%{VERSION}-%{RELEASE}\t%{SUMMARY}\n'
gpg-pubkey-f4a80eb5-53a7ff4b    gpg(CentOS-7 Key (CentOS 7 Official Signing Key) <security@centos.org>)
gpg-pubkey-352c64e5-52ae6884    gpg(Fedora EPEL (7) <epel@fedoraproject.org>)
gpg-pubkey-45f2c3d5-5e81efb9    gpg(Jenkins Project <jenkinsci-board@googlegroups.com>)
gpg-pubkey-dc6315a3-6091b7b3    gpg(Artifact Registry Repository Signer <artifact-registry-repository-signer@google.com>)
gpg-pubkey-3e1ba8d5-558ab6a8    gpg(Google Cloud Packages RPM Signing Key <gc-team@google.com>)

--qf '%{NAME}-%{VERSION}-%{RELEASE}\t%{SUMMARY}\n' で出力結果のフォーマットを指定できる

gpgkeyの削除方法

rpm -e ${gpg-pubkey-id}で削除できる 例えば、上記のJenkins用の gpgkeyを削除する場合は下記のようにする。

rpm -e gpg-pubkey-45f2c3d5-5e81efb9

これで該当のgpgkeyを削除できた