Athena

Part 14: Systems Analysis

Brian Long (www.blong.com)

This month Brian Long builds a Linux process viewer with Kylix.


This article first appeared in Linux Format Issue 32, October 2002.

Click here to download the files associated with this article.

If you find this article useful then please consider making a donation. It will be appreciated however big or small it might be and will encourage Brian to continue researching and writing about interesting subjects in the future.


Introduction

The goal this month is to steer us back to more traditional Linux programming tasks. It is very common for Linux programmers to build utilities that display information about the system and so this month we will build a graphical process viewer.

First things first, let's peruse some existing process viewers and get an idea of what the application should display. As you probably know, Linux may come with a variety of process viewers (not all of which are necessarily installed), some of which are console applications (such as ps), some of which are written for the Gnome desktop (such as gtop) and some of which are written for KDE (such as ktop and kpm).

Some of the common aspects of these programs are that they display a list of all running processes (although this list can often be filtered down to a more specific list of processes). For each process a certain amount of information is listed. The specific information may vary from one utility to another, but it usually includes the process name, the process identifier (PID), the amount of RAM it is consuming, its status (running or sleeping) the name of the user that started it and so on. Typically you are also able to terminate a process from these tools by sending it an appropriate signal.

We'll take these ideas on board in our own Kylix-built process viewer.

The Outline

The application will make use of actions to implement the user-invokable behaviour. These actions can then be connected to menu items or toolbar buttons as required by the UI. If you are unfamiliar with actions, dig out your back issues of Linux Format; we looked at them in Issue 22 (Christmas 2001). If you don't have that issue you can read the article online at: http://www.blong.com/Articles/Kylix%20Tutorial/Part4/Tutorial4.htm.

In this case we will have an action list, main menu and popup menu connected to an image list (so they all have access to some icons). We also require a status bar to display menu item hints and a list view control to display the process information. The popup menu is associated with the list view through the latter's PopupMenu property and the status bar will display hints if you set both SimplePanel and AutoHint to True.

The list view needs some columns defined and this can be done using the Columns property, as we did back in Issue 25 (March 2002). You can find that article online by changing the two occurrences of 4 to 7 in the URL above.

In order to allow the process list to update periodically we need a timer on the form. The Interval property will need to be set, for example to 2500 (for it to tick roughly every 2.5 seconds), but it would be easy enough to allow the user to choose their own preferred update interval (that's left as an exercise for the reader).

The design-time view of the application form can be seen in Figure 1 with all the columns set up, components renamed, menus designed and actions set up.

Figure 1: The main form at design time

We have yet to see what actions will be used and reflected through the menu tree. However you can see some of the menu items in Figure 2, which shows the program running and displaying all processes running in a sample system. This finished version of the project can be found on this month's disk as pslist.dpr. The rest of this text will look at how the application was built.

Figure 2: The process viewer in all it's glory

The Actions

Every menu item in the application is implemented via an action component. The Options menu has a Close item that terminates the program by calling the form's Close method. It also has a Kill Selected Process item that does exactly what its caption says. We'll look into this once we've seen how the process list is built.

The View menu is visible in Figure 2 and has a refresh item that updates the process list in its event handler. This same event handler is triggered whenever the timer ticks - it clears down the list view then re-populates it with process information. The other four menu items allow you to filter the processes listed in some way. Whichever one is selected has a checkmark placed next to it and the process list is refreshed. Each menu item (or more correctly the action behind it) shares the same undemanding event handler:


procedure TfrmMain.actViewProcessesExecute(Sender: TObject);
begin
  actAllProcesses.Checked := False;
  actNonRootProcesses.Checked := False;
  actYourProcesses.Checked := False;
  actRunningProcesses.Checked := False;
  (Sender as TAction).Checked := True;
  actRefresh.Execute
end;

As you can see the four actions have their Checked property cleared, then the one that was clicked is checked before the main refresh action is invoked.

The refresh action is obviously the where the main functionality resides and is where we will start our proper look at the code, but we should also note the last action, used to display an About box through a call to MessageDlg (see Figure 3):

Figure 3: The process viewer About box

Now we can delve into the guts of the program...

About Linux Processes

Getting information about Linux processes is straightforward as the kernel maintains a memory-mapped file system in the /proc directory. No file in that directory tree is a physical file on the drive; instead they all map onto various kernel data structures. The files in /proc give information about the system, for example Figure 4 shows what you find in the meminfo file.

Figure 4: System information found in /proc

You also find a number of directories there, most of which have numeric names (see Figure 5).

Figure 5: A listing of /proc

There is one directory for each running process (the directory name matches the process identifier or PID) and the files inside each directory give plenty of information about that process. Figure 6 shows an example process directory where you can see all the files are listed as being 0 bytes (that's how much disk space these memory-mapped files consume).

Figure 6: A subdirectory of /proc

Of potential interest to us are the following:

Figure 7: A process status file

Quick Start Linux System Programming

To talk to the Linux API you need to use the Libc unit, which contains type definitions, constants and import declarations for the glibc routines. For example, whilst UID and PID values are simply integers, Libc defines the __pid_t and __uid_t types that are clearer in their purpose.

You can get help on these Libc routines either in the Kylix editor (just click on a reference to the routine in the editor and press F1) or using the man command at the Linux prompt. Both approaches show you a Linux manual page and the routine will be described in C terms (you can see the Pascal declaration by locating the routine in the Libc.pas source file - click on Libc in your uses clause and press Ctrl+Enter to open it).

You can see a PID variable declared as a field of the form class below, as well as declarations of the various helper routines involved in this application.


uses
  Libc, ...

type
  TfrmMain = class(TForm)
    ...
  private
    //Some convenient user name strings
    CurrentUser, Root: String;
    //Directory of process being worked on (/proc/XXX)
    PIDDir: String;
    //PID of process being worked on
    PID: __pid_t;
    //Used to keep same item selected
    LastSelectedPID: String;
    //An easy to read version of /proc/XXX/status
    ProcessStatus: TStringList;
    procedure LoadTextFile(const Filename: String; List: TStrings);
    procedure GetProcessInfo;
    function GetProcessStatusLine(const Prefix: String): String;
    function GetProcessName: String;
    function GetProcessExe: String;
    function GetProcessUser: String;
    function GetProcessStatus: String;
    function GetProcessMemVal(const Prefix: String): String;
    function GetProcessCWD: String;
    function GetProcessCmdLine: String;
  end; 

The following listing shows the form's OnCreate and OnDestroy event handlers. Apart from ensuring that any subsequent file access is read-only, setting up a string list for later use and kicking off the program with all processes listed, the code calls a couple Linux APIs to find the name of the root user (which will almost certainly be root, but we make sure) and of the current user.


procedure TfrmMain.FormCreate(Sender: TObject);
begin
  //Default to safe file access mode
  FileMode := fmOpenRead or fmShareDenyNone;
  //Set up root & current user name strings
  Root := getpwuid(0)^.pw_name;
  CurrentUser := getpwuid(getuid)^.pw_name;
  //Set up string list for status files to be read into
  ProcessStatus := TStringList.Create;
  //Do first read of processes
  actAllProcesses.Checked := True;
  actAllProcesses.Execute;
end;

procedure TfrmMain.FormDestroy(Sender: TObject);
begin
  ProcessStatus.Free
end;

getpwuid takes a user's UID and returns a pointer to a record of useful information about that user including their name in the pw_name field. This field is defined as a PChar (a C-compatible string) but assigning it to a String will successfully copy its content to our variable.

Note the ^ symbol used to de-reference the pointer returned from getpwuid. Also note that the man page does not advise that we free the memory pointed to by the returned pointer, so presumably the function returns the address of a record maintained by the Linux libraries.

As you can tell from the code, root's UID is zero and the UID of the current user is returned by calling the trivial getuid function.

Listing Processes

The main refresh action has the job of iterating across each of those process directories, reading the required pieces of information and adding them to a new row in the list view. A list view row is represented by a TListItem that is managed by the list view's Items collection property. The first column is represented by its Caption property and the additional columns are all stored in the SubItems property, a TStrings object.

The first thing the routine does is wrap the whole block between calls to methods of the list view's Items property. BeginUpdate and EndUpdate prevent the list view from updating its appearance whilst we clear and refill it. Note the call to EndUpdate is protected through a try..finally statement.


procedure TfrmMain.actRefreshExecute(Sender: TObject);
var
  SearchRec: TSearchRec;
begin
  lstvProcessInfo.Items.BeginUpdate;
  try
    if Assigned(lstvProcessInfo.Selected) then
      LastSelectedPID := lstvProcessInfo.Selected.SubItems[1]
    else
      LastSelectedPID := '';
    lstvProcessInfo.Items.Clear;
    if FindFirst('/proc/*', faAnyFile, SearchRec) = 0 then
      try
        repeat
          if SearchRec.Attr and faDirectory > 0 then
          begin
            PID := StrToIntDef(SearchRec.Name, 0);
            if PID <> 0 then
            begin
              PIDDir := Format('/proc/%s/', [SearchRec.Name]);
              GetProcessInfo;
            end
          end
        until FindNext(SearchRec) <> 0
      finally
        FindClose(SearchRec)
      end;
  finally
    lstvProcessInfo.Items.EndUpdate
  end
end;

The next thing it does is remember which process was selected in the list view so it can be re-selected (if still running) when we've finished refreshing the list. Remember the list gets refreshed every 2.5 seconds and it could be irritating if you select a process (for example to kill it) and it deselects itself a second or two later. The information it requires is in the 3rd column, so can be found in the second string in the list view item's SubItems property.

After clearing the list the code uses FindFirst, FindNext and FindClose to iterate through the contents of /proc. Each item is checked to see if it is a directory and if so, whether its name can be treated as an integer (i.e. a process directory). For each one that meets the criteria, the form's PID data field is set to the process PID and PIDDir is assigned the full path of that directory.

Next a helper routine, GetProcessInfo, takes over to add the details into the list view:


procedure TfrmMain.GetProcessInfo;
var
  Item: TListItem;
  State, User: String;
begin
  LoadTextFile(PIDDir + 'status', ProcessStatus);
  //If user only wants running processes, leave for any other state
  State := GetProcessStatusLine('State:'#9);
  if actRunningProcesses.Checked and ((Length(State) = 0) or (State[1] <> 'R')) then
    Exit;
  //If user only wants non-root processes, leave if this is a root processes
  User := GetProcessUser;
  if actNonRootProcesses.Checked and (User = Root) then
    Exit;
  //If user only wants user processes, leave for other users' processes
  if actYourProcesses.Checked and (User <> CurrentUser) then
    Exit;
  //Add row to list view and fill in the columns.
  //Also try and re-select last selected process
  Item := lstvProcessInfo.Items.Add;
  Item.Caption := GetProcessStatusLine('Name:'#9);
  Item.SubItems.Add(GetSymLinkPath(PIDDir + 'exe'));
  Item.SubItems.Add(Format('%d (%:0x)', [PID]));
  if Item.SubItems[colPID] = LastSelectedPID then
    Item.Selected := True;
  Item.SubItems.Add(User);
  Item.SubItems.Add(State);
  Item.SubItems.Add(GetProcessStatusLine('VmSize:'#9));
  Item.SubItems.Add(GetProcessStatusLine('VmRSS:'#9));
  Item.SubItems.Add(GetSymLinkPath(PIDDir + 'cwd'));
  Item.SubItems.Add(GetProcessCmdLine);
end;

Since most of the information we require comes from the process status file this is read straight away. However due to the file reporting its size as 0, various common Kylix file-reading techniques don't cut the mustard. For instance it would be convenient to read this text file into a TStringList via its LoadFromFile method, but this gets us nowhere fast.

Another helper routine uses basic Pascal text file reading code to keep reading lines from the file until either a blank line is returned or an I/O error occurs. Normally I/O errors cause exceptions but the IOChecks compiler directive in the listing stops this happening; instead the IOError variable is set to a non-zero value if something goes awry. In normal situations you'd use the Eof function to determine when you reach the end of the file, but of course this is not a normal file.


{$IOCHECKS OFF}
procedure TfrmMain.LoadTextFile(const Filename: String; List: TStrings);
var
  F: TextFile;
  S: String;
  Stop: Boolean;
begin
  AssignFile(F, FileName);
  Reset(F);
  try
    List.Clear;
    repeat
      ReadLn(F, S);
      Stop := (IOResult <> 0) or (S = '');
      if not Stop then
        List.Add(S)
    until Stop
  finally
    CloseFile(F)
  end;
end;

Going back to GetProcessInfo, the code loads the status file into a TStringList that you can see created in the form's OnCreate event handler and freed in the form's OnDestroy event handler above. This string list is examined by additional routines that look for particular pieces of information about the process under scrutiny.

Before committing to adding a new row to the list view, the code examines the actions behind the process filter menu items to see whether the selected process should be listed or not. If not, the code exits from the routine to cycle round to the next process.

If the Running Processes menu item is checked then we should only deal with processes whose state indicates they are running. You can see an example state string in Figure 7; it starts with S for a sleeping process and R for a running process so anything other than an R means we should skip out.

If the Non-Root Processes item is checked and the process owner is found to be root we also skip out and the same is true if the Your Processes item is checked and the process owner is not the current user.

To get the state and process owner (and other information) from the status file a couple more helper routines are employed. The common one is GetProcessStatusLine, which takes some target string at the beginning of the sought line in the file and returns the remainder of the line, if found.

You can see above that GetProcessInfo calls GetProcessStatusLine to get the process name, status, virtual memory size (VmSize) and resident memory size (VmRSS). Notice that it is careful to pass the correct suffix, for example Name: followed by a tab (character 9) to get just the important part of the line back.


//Locate a particular line from the status file, given a prefix string
function TfrmMain.GetProcessStatusLine(const Prefix: String): String;
var
  I: Integer;
  Line: String;
begin
  Result := '';
  for I := 0 to ProcessStatus.Count - 1 do
  begin
    Line := ProcessStatus[I];
    if Pos(Prefix, Line) > 0 then
    begin
      Result := Copy(Line, Succ(Length(Prefix)), Length(Line));
      Break;
    end
  end
end;

function TfrmMain.GetProcessUser: String;
var
  UID: String;
  TabPos: Integer;
begin
  UID := GetProcessStatusLine('Uid:'#9);
  TabPos := Pos(#9, UID);
  //Typically there are 4 UID values separated by tabs, but check, JIC
  if TabPos > 0 then
    Delete(UID, TabPos, Length(UID))
  else
    UID := Trim(UID);
  Result := getpwuid(StrToInt(UID))^.pw_name
end;

GetProcessUser has a bit of extra work to do since the Uid: line in the status file contains four variants on the UID (which include the effective UID and sticky UID); the real one is the first one so it looks for the tab that follows the first number and trims the rest of the string away. The resultant string is turned into the integer UID and the user name attained using getpwuid again.

The remaining helper routines called from GetProcessInfo are used to find the underlying process executable, the current working directory and command line used to initiate the process.

If you recall from Figure 6, the executable and current working directory are both symlinks and they can be followed by the realpath Libc routine. Note that if you don't have permissions to see where the link points, realpath gives you the same path you gave as input so the code checks. We'll see how to ensure the application runs with root privileges later for those who don't know.


function TfrmMain.GetSymLinkPath(const SymLink: String): String;
var
  RealPathBuf: array[0.._POSIX_PATH_MAX] of Char;
begin
  realpath(PChar(SymLink), RealPathBuf);
  if SymLink <> String(RealPathBuf) then
    Result := RealPathBuf //if nothing doing, return N/A
  else
    Result := 'N/A'
end;

Getting the command line is a bit fiddly as the file is a series of zero terminated strings (rather than line feed separated). Also it seems there may or may not be an extra zero terminator after the last string so the helper routine has to keep reading past any zero terminator it finds. If it gets a second one or is told it has read past the end of the file then that's it, otherwise it has started reading another part of the command line. Again, this code could be much simplified if this were a regular disk file.


function TfrmMain.GetProcessCmdLine: String;
var
  F: file of Char;
  Ch: Char;
begin
  Result := '';
  //The file /proc/XXX/cmdline is a series of zero-terminated strings,
  //possibly followed by an extra zero terminator
  AssignFile(F, PIDDir + 'cmdline');
  Reset(F);
  try
    repeat
      Read(F, Ch);
      if Ch <> #0 then //read till a 0 char
        Result := Result + Ch
      else
      begin
        //We will find an extra #0 at EOF in some cases here
        //otherwise we'll trigger a read past EOF IOError
        Read(F, Ch);
        if (IOResult = 0) and (Ch <> #0) then
          Result := Result + #32 + Ch;
      end
    until (Ch = #0) or (IOResult <> 0)
  finally
    CloseFile(F)
  end
end;

The last thing to look at is how to kill the selected process. The action's event handlers are shown below. The first one uses the simple Linux routine kill, which takes a PID and a signal to kill the process with. The OnUpdate event handler ensures the action is only available when an item is selected in the list view.


procedure TfrmMain.actKillProcessExecute(Sender: TObject);
begin
  kill(StrToInt(lstvProcessInfo.Selected.SubItems[colPID]), SIGKILL)
end;

procedure TfrmMain.actKillProcessUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled := Assigned(lstvProcessInfo.Selected)
end;

Setting Up The Process Viewer

Now that the utility is complete it is important to set it up correctly. If you log in as a non-root user when developing, which is advisable, then the final executable file will be owned by your UID. The upshot of this is that the program will not be able to see the executable or working directory of any root processes as your UID doesn't have permission to read symlinks owned by root.

You can prove this by getting a long listing of the process directory of a root process (the init process is owned by root and has a PID of 1):


ls -l /proc/1

The symlinks will not be followed.

You can circumvent this limitation of the program by changing its owner to be root and setting the sticky UID (SUID) bit in its permissions (you'll need to be logged in as root). The sticky bit ensures that no matter who runs the application, it will run with owner's permissions (i.e. those of root).

Figure 8 shows the commands needed to change the program's owner to root and set the SUID permission. You can see in the long format listing that the sticky bit is represented by a letter s in the owner executable position (it changes from the normal x to an s).

Also in the screenshot you can see the kylixpath script being called to ensure that the PATH is set up correctly for Kylix applications to be run from the command prompt (as opposed to from the Kylix IDE). The process viewer can then be seen successfully reading the executable and working directory symlinks of root's init process.

Figure 8: Using the sticky bit to read root's symlinks

Summary

We looked at building an application to list details of running processes in the system. This demonstrates that the business of building system level utilities is not only within the realm of gcc programmers; Kylix is just as capable of lower-level systems programming.

Next month's will be the last instalment in this tutorial series and we'll round off by looking at building console applications. Until then, happy coding.

About Brian Long

Brian Long used to work at Borland UK, performing a number of duties including Technical Support on all the programming tools. Since leaving in 1995, Brian has spent the intervening years as a trainer, trouble-shooter and mentor focusing on the use of the C#, Delphi and C++ languages, and of the Win32 and .NET platforms. In his spare time Brian actively researches and employs strategies for the convenient identification, isolation and removal of malware.

If you need training in these areas or need solutions to problems you have with them, please get in touch or visit Brian's Web site.

Brian authored a Borland Pascal problem-solving book in 1994 and occasionally acts as a Technical Editor for Wiley (previously Sybex); he was the Technical Editor for Mastering Delphi 7 and Mastering Delphi 2005 and also contributed a chapter to Delphi for .NET Developer Guide. Brian is a regular columnist in The Delphi Magazine and has had numerous articles published in Developer's Review, Computing, Delphi Developer's Journal and EXE Magazine. He was nominated for the Spirit of Delphi 2000 award.


Back to top