I developed a client solution for managing and inspecting thread information in Linux by initially experimenting with a custom struct in the kernel. An “incomplete type” GCC error led me to explore the separation between kernel-internal and userspace headers, proper UAPI usage, and /proc
-based alternatives. The result was a robust CLI tool that allocates, populates, and exports thread data while maintaining kernel userspace boundaries for stability and maintainability.
I was experimenting with adding my own structure to the Linux kernel.
In /usr/src/linux-3.2/include/linux/unistd.h
, I added:
#ifndef _LINUX_UNISTD_H_
#define _LINUX_UNISTD_H_
struct threadinfo_struct {
int pid;
int nthreads;
int *tid;
};
/* Include machine specific syscall numbers */
#include <asm/unistd.h>
#endif /* _LINUX_UNISTD_H_ */
Then I wrote a small userspace program to allocate it:
#include <stdio.h>
#include <linux/unistd.h>
int main(void) {
struct threadinfo_struct *ti =
(struct threadinfo_struct*) malloc(sizeof(struct threadinfo_struct));
// ...
return 0;
}
When I compiled it, GCC complained:
test.c: In function 'main':
test.c:4:78: error: invalid application of 'sizeof' to incomplete type 'struct threadinfo_struct'
That error stopped me in my tracks.
Understanding the Error
The phrase “incomplete type” was the big clue.
In userspace, when I #include <linux/...>
from /usr/include/linux/...
, I’m not pulling in my edited kernel source header. Instead, I’m including exported headers—these are a sanitized subset of kernel headers provided for userspace ABI compatibility.
My shiny new struct threadinfo_struct
wasn’t in there. So from the compiler’s perspective, it was either just forward-declared or completely unknown.
And when the compiler only knows a struct name without its members, it’s “incomplete”—you can’t do sizeof
on it or allocate it directly.
Even if it were visible, mixing kernel-internal headers with userspace code is risky: kernel internal structures can change at any time, breaking userspace builds.
How I Fix It
I found three viable options:
Proper UAPI Header
If I really wanted the struct in both kernel and userspace, the right way was to move it into a UAPI (User API) header in the kernel tree:
/* include/uapi/linux/threadinfo.h */
#ifndef _UAPI_LINUX_THREADINFO_H
#define _UAPI_LINUX_THREADINFO_H
struct threadinfo_struct {
int pid;
int nthreads;
__u32 tid[]; /* flexible array for ABI safety */
};
#endif
Then, I reinstalled the headers:
make headers_install INSTALL_HDR_PATH=/usr
Now, from userspace, I could simply:
#include <linux/threadinfo.h>
and my struct was complete.
Userspace Only Header
If my project didn’t really need to talk to the kernel, the easiest was to create a local header:
/* threadinfo.h */
#ifndef THREADINFO_H
#define THREADINFO_H
struct threadinfo_struct {
int pid;
int nthreads;
int *tid;
};
#endif
Then include it in my program:
#include <stdio.h>
#include <stdlib.h>
#include "threadinfo.h"
int main(void) {
struct threadinfo_struct *ti = malloc(sizeof *ti);
// ...
free(ti);
return 0;
}
Avoid Kernel Header
Often, there’s no need to depend on kernel internals at all.
For my case getting thread IDs I could just read /proc/<pid>/task
from userspace.
My Practice Project
To reinforce the lesson, I wrote a self-contained program that:
- Defines the struct locally.
- Fills it with data from
/proc
. - Prints all threads.
- Adds a couple of “extra” functionalities.
// build: gcc -Wall -Wextra -O2 demo.c -o demo
#define _GNU_SOURCE
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct threadinfo_struct {
int pid;
int nthreads;
int *tid;
};
static int cmp_int(const void *a, const void *b) {
int x = *(const int *)a, y = *(const int *)b;
return (x > y) - (x < y);
}
static int fill_threadinfo(struct threadinfo_struct *ti, int pid) {
char path[64];
snprintf(path, sizeof(path), "/proc/%d/task", pid);
DIR *d = opendir(path);
if (!d) return -errno;
int count = 0;
struct dirent *de;
while ((de = readdir(d))) {
if (de->d_name[0] == '.') continue;
count++;
}
rewinddir(d);
int *tids = malloc(sizeof(int) * (count ? count : 1));
if (!tids) { closedir(d); return -ENOMEM; }
int i = 0;
while ((de = readdir(d))) {
if (de->d_name[0] == '.') continue;
tids[i++] = atoi(de->d_name);
}
closedir(d);
qsort(tids, i, sizeof(int), cmp_int);
ti->pid = pid;
ti->nthreads = i;
ti->tid = tids;
return 0;
}
static void free_threadinfo(struct threadinfo_struct *ti) {
free(ti->tid);
ti->tid = NULL;
ti->nthreads = 0;
}
int main(int argc, char **argv) {
int pid = (argc > 1) ? atoi(argv[1]) : getpid();
struct threadinfo_struct ti = {0};
int rc = fill_threadinfo(&ti, pid);
if (rc < 0) {
fprintf(stderr, "Failed to read threads for pid %d: %s\n",
pid, strerror(-rc));
return 1;
}
printf("PID %d has %d thread(s):\n", ti.pid, ti.nthreads);
for (int i = 0; i < ti.nthreads; ++i) {
printf(" TID[%d] = %d\n", i, ti.tid[i]);
}
// Extra 1: Check if PID is among TIDs
int lookfor = ti.pid;
int found = 0;
for (int i = 0; i < ti.nthreads; ++i)
if (ti.tid[i] == lookfor) { found = 1; break; }
printf("Contains TID %d? %s\n", lookfor, found ? "yes" : "no");
// Extra 2: Export to CSV
FILE *f = fopen("threadinfo.csv", "w");
if (f) {
fprintf(f, "pid,nthreads,tid\n");
for (int i = 0; i < ti.nthreads; ++i)
fprintf(f, "%d,%d,%d\n", ti.pid, ti.nthreads, ti.tid[i]);
fclose(f);
printf("Wrote threadinfo.csv\n");
}
free_threadinfo(&ti);
return 0;
}
What I Learn
- Never include kernel-internal headers in userspace.
- If you must share data structures, put them in
include/uapi/
and install the headers. - For one-off tools, define your own struct in your project.
/proc
and other userspace APIs can often give you the info you need—no kernel mods required.
Final Thoughts
What began as a simple “why is GCC mad at me?” moment quickly became a deep dive into the way Linux cleanly separates kernel and userspace headers, teaching me to value the stability of UAPI headers, the flexibility of defining my own data structures in userspace, and the simplicity of leveraging existing interfaces like /proc
; now, whenever I encounter an “incomplete type” error, I know exactly where to investigate and won’t be tempted to pull kernel internals into a C program without a solid reason.