Process name by procfs

In a UNIX(-like) environment, the init process is the parent all the other processes. It is usually created first at startup, with a 1 as pid (process identifier). However, there is no guarantee that is true in a specific environment. In my case, I found out that Ubuntu 13.10, Saucy Salamander, follows a different convention. We have an init process with pid 1, but our user processes are managed by a "user" init.Here I show how we could get on Linux the name of the executable associated to a specific process.

We get Linux system information checking the /proc (pseudo) file system. There we could also find a subdirectory for each process currently running on the system, identified by its pid. Here we are interested in checking the "comm" file. Notice that "comm" is available only from recent versions of the Linux kernel, in case it is not available on you environment, you could fallback to "cmdline". Besides, the "comm" stored value is truncated to TASK_COMM_LEN, that should be currently set to 16, characters.

It is easy to write a C++ function that does the trick, a bit more job is required for its C little brother. Here are their prototypes:
std::string getProcessName(int pid); // 1
bool getProcessName(int pid, char* name, int size); // 2
1. C++ let me define a clean interface. I pass in input the pid, I get back the name. If the pid has no associated process, I expect an empty string as output.
2. The C version is a bit clumsier. I need to pass as input parameter the buffer where to write the name, and its size. To simplify its usage I return also a boolean (you can change it to int, for older C compilers support) reporting for success of failure.

As usual, I write a few test cases (for Google Test) to let them drive me in the development:
TEST(ProcessName, GetInitPlus)
{
  ASSERT_STREQ("init", getProcessName(1).c_str()); // 1
  ASSERT_STREQ("init", getProcessName(1680).c_str()); // 2
}

TEST(ProcessName, GetOnePlus)
{
  ASSERT_STREQ("bash", getProcessName(2292).c_str()); // 3
}

TEST(ProcessName, GetMissingPlus)
{
  ASSERT_TRUE(getProcessName(8799).empty()); // 4
}
1. Pid 1 should refer to the init process.
2. My "user" init had that specific pid when I tested my code. You should change it as required.
3. Same as for (2), the pid of my bash shell was 2292, do not expect this test to succeed without proper editing.
4. I had no process with such id, so I expected an empty string as result.

TEST(ProcessName, GetInit)
{
  const int size = 16; // 1
  char buffer[size];
  ASSERT_TRUE(getProcessName(1, buffer, size)); // 2
  ASSERT_STREQ("init", buffer);
}

TEST(ProcessName, GetUserInit)
{
  const int size = 16;
  char buffer[size];
  ASSERT_TRUE(getProcessName(1680, buffer, size)); // 3
  ASSERT_STREQ("init", buffer);
}

TEST(ProcessName, GetOne)
{
  const int size = 16;
  char buffer[size];
  ASSERT_TRUE(getProcessName(2292, buffer, size));
  ASSERT_STREQ("bash", buffer);
}

TEST(ProcessName, GetMissing)
{
  const int size = 16;
  char buffer[size];
  ASSERT_FALSE(getProcessName(8799, buffer, size));
}
1. Accordingly to the official documentation, the constant TASK_COMM_LEN should be defined in "linux/sched.h". For some reason, I couldn't find it there or in any linux include. Maybe I have outdated includes on my machine. I didn't investigate much, and I used a local constant instead.
2. I assert that a process should be found and, in the next line, that the name should be "init".
3. As for the C++ version, check the actual pid for the "user" init on your current environment before testing.

Here is the C++11 version of my function:
std::string getProcessName(int pid)
{
  std::ifstream ifs { ("/proc/" + std::to_string(pid) + "/comm").c_str() }; // 1
  if(ifs.bad()) // 2
    return {};

  std::string command;
  std::getline(ifs, command);
  return command;
}
1. I try to open an input file stream for the proc/{pid}/comm file. The C++11 to_string() function converts an integer value to a string, so that its result could make use of the operator+ std::string overload to join it to the plain C-strings on its left and right. However ifstream requires a plain C-string in input, so a c_str() conversion on the result is required.
2. If the file can't be opened, I return an empty string. Otherwise the file content is extracted and returned.

And this is my C99 version:
bool getProcessName(int pid, char* name, int size)
{
  char filename[80];
  sprintf(filename, "/proc/%d/comm", pid);

  FILE* file = fopen(filename, "r"); // 1
  if(!file)
    return false;

  fgets(name, size, file); // 2
  name[strlen(name) - 1] = '\0';
  return true;
}
1. If I can't open the "comm" file for reading, I return an error, without caring about setting the buffer.
2. Otherwise I use fgets() to read the file content, then I get rid of backslash-n at the end of the string.

No comments:

Post a Comment