34.2 — Process Groups
Chapter 34 · The Linux Programming Interface · EmbeddedPathashala
getpgrp(), setpgid(), race conditions, and BSD vs POSIX interfaces
Key Terms in This Section
getpgrp() getpgid() setpgid() PGID Process Group Leader ESRCH EPERM EACCES pipelinePgid Race condition BSD setpgrp
Getting the Process Group ID: getpgrp()
Every process can find its own process group ID using getpgrp(). This function takes no arguments and always succeeds.
There is also getpgid(pid) which returns the process group ID of any specified process (pass 0 for the calling process, same as getpgrp()).
#include <unistd.h>
pid_t getpgrp(void); /* Returns PGID of calling process — always succeeds */
pid_t getpgid(pid_t pid); /* Returns PGID of process 'pid'; 0 means calling process */
Changing the Process Group ID: setpgid()
setpgid(pid, pgid) changes the process group of a process.
| Argument | Meaning |
|---|---|
pid == 0 |
Apply to the calling process |
pgid == 0 |
Set PGID to equal the process’s own PID (creates a new group) |
pid == pgid |
Create a new group with this process as leader |
pid != pgid |
Move a process from one group to another (within same session) |
So these three calls are all equivalent — each creates a new group with the calling process as leader:
setpgid(0, 0);
setpgid(getpid(), 0);
setpgid(getpid(), getpid());
Rules and Restrictions on setpgid()
4 restrictions you must know:
- ESRCH —
pidmust be the calling process or one of its children. You cannot move another unrelated process. - EPERM — When moving between groups, all parties (calling process, target process, destination group) must be in the same session.
- EPERM —
pidcannot be a session leader. Session leaders are locked to their own group. - EACCES — A parent cannot change a child’s PGID after the child has called
exec(). This protects the newly exec’d program from surprise PGID changes.
The Race Condition Problem in Job-Control Shells
Job-control shells need to put all processes in a pipeline into the same new process group before any process starts running. But after fork(), the kernel can schedule either the parent or child first — there is no guaranteed order.
This creates a race condition:
- If the parent calls
setpgid(child, pgid)but the child already called exec(), the call fails with EACCES. - If the child calls
setpgid(0, pgid)but the parent already sent job-control signals, the child might miss them.
Solution: Have both parent and child call
setpgid() immediately after fork(). The parent ignores EACCES errors (meaning the child already exec’d and changed its own group first). This way, one of the two calls will always succeed in time./* job_control_shell_pattern.c
* Demonstrates how a job-control shell sets child's PGID safely
* Compile: gcc -o jcs job_control_shell_pattern.c
*/
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <sys/wait.h>
int main(void)
{
pid_t childPid;
/*
* pipelinePgid: the PGID all processes in this pipeline should share.
* For the FIRST process in a pipeline, this is set to 0 so setpgid()
* will use the child's own PID as the PGID (making it a group leader).
* For later processes, it is set to the PID of the first process.
*/
pid_t pipelinePgid = 0; /* 0 means "use child's own PID" */
childPid = fork();
switch (childPid) {
case -1:
perror("fork");
return 1;
case 0: /* ---- Child ---- */
/*
* Child sets its OWN PGID immediately after fork.
* If pipelinePgid==0, the child becomes the group leader.
*/
if (setpgid(0, pipelinePgid) == -1) {
perror("child setpgid");
/* In a real shell: handle error */
}
printf("Child PID=%ld PGID=%ld\n",
(long)getpid(), (long)getpgrp());
/* ... exec the actual command here ... */
return 0;
default: /* ---- Parent (shell) ---- */
/*
* Parent ALSO sets the child's PGID.
* One of the two setpgid() calls will win the race.
* If child already exec'd, we get EACCES — ignore it.
*/
if (setpgid(childPid, pipelinePgid) == -1) {
if (errno != EACCES) { /* EACCES is fine — child won the race */
perror("parent setpgid");
}
}
printf("Parent: child %ld assigned to PGID %ld\n",
(long)childPid, (long)pipelinePgid);
wait(NULL);
break;
}
return 0;
}
/*
* The key insight: both parent and child call setpgid().
* One will succeed, one may fail with EACCES — that is OK.
* This eliminates the race condition.
*/
Code Example 2 — Moving a Process to a Different Group
/* change_pgroup.c
* Demonstrates creating and moving between process groups
* Compile: gcc -o change_pgroup change_pgroup.c
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
static void print_ids(const char *label)
{
printf("[%s] PID=%ld PGID=%ld SID=%ld\n",
label,
(long)getpid(),
(long)getpgrp(),
(long)getsid(0));
}
int main(void)
{
print_ids("parent before fork");
pid_t child = fork();
if (child == -1) { perror("fork"); return 1; }
if (child == 0) {
/* --- CHILD --- */
print_ids("child after fork (inherits parent PGID)");
/* Create a NEW process group — child becomes group leader */
if (setpgid(0, 0) == -1) {
perror("setpgid");
return 1;
}
print_ids("child after setpgid(0,0) — new group leader");
/*
* Now create a grandchild. It will inherit this new PGID.
*/
pid_t gc = fork();
if (gc == 0) {
print_ids("grandchild — inherits child's PGID");
return 0;
}
wait(NULL);
return 0;
}
/* --- PARENT --- */
wait(NULL);
print_ids("parent after children finish");
return 0;
}
/*
* Sample output:
* [parent before fork] PID=500 PGID=500 SID=400
* [child after fork ...] PID=501 PGID=500 SID=400 <- inherits
* [child after setpgid(0,0)] PID=501 PGID=501 SID=400 <- new leader
* [grandchild ...] PID=502 PGID=501 SID=400 <- inherits child's PGID
* [parent after children] PID=500 PGID=500 SID=400
*/
Interview Questions — Section 34.2
Q1. What is the difference between getpgrp() and getpgid()?
getpgrp() takes no arguments and always returns the PGID of the calling process. getpgid(pid) takes a PID and returns the PGID of that process; passing 0 is equivalent to calling getpgrp(). getpgid() is the newer, more flexible POSIX version derived from BSD.
Q2. What does setpgid(0, 0) do?
It creates a new process group with the calling process as the leader. Both arguments of zero are shortcuts: pid=0 means “the calling process” and pgid=0 means “use my own PID as the PGID”. The result is a brand new process group whose ID equals the caller’s PID.
Q3. Why does a job-control shell call setpgid() in both the parent and the child after fork()?
Because after fork() the scheduling order of parent and child is unpredictable. If only the parent calls setpgid(), the child might exec() before the parent’s call and cause EACCES. If only the child calls setpgid(), the parent might send job-control signals before the group change takes effect. Having both call it ensures one will always succeed before any signals are sent, eliminating the race condition. The parent simply ignores EACCES errors.
Q4. Why does setpgid() return EACCES when trying to change a child’s PGID after exec()?
Because the exec’d program is unaware that its PGID might change. If the PGID could change after exec(), the new program could be put in an unexpected group after it has already started running, which could confuse its job-control behaviour. The EACCES restriction prevents this confusion.
Q5. Can setpgid() move a process into a process group belonging to a different session?
No. All three parties — the calling process, the target process, and the destination process group — must be in the same session. Attempting to cross session boundaries returns EPERM. This preserves the integrity of the two-level hierarchy where sessions contain process groups.
Q6. What happens to the process group ID when the process group leader exits?
The process group continues to exist with the same PGID as long as at least one member remains. The group leader exiting does not destroy the group. The group only ceases to exist when its last member exits or joins a different group.
