进程 PID
进程的PID(Process ID)是操作系统中用于唯一标识一个进程的整数值。每个进程在创建时,操作系统都会分配一个唯一的PID,用来区分不同的进程。
PID的特点
- 唯一性:
在操作系统运行的某一时刻,每个进程的PID都是唯一的。不同进程不会共享同一个PID。即使一个进程终止后,该PID可以被回收并分配给新创建的进程,但在同一时刻不会有两个进程拥有相同的PID。
- 进程生命周期:
PID 的生命周期与进程的生命周期相对应。当一个进程被创建时,操作系统为它分配一个PID;当进程终止时,该PID被释放,并可能被分配给后续的新进程。
- 系统管理和调试:
PID 在进程管理和调试中起着重要作用。系统管理员和开发者可以通过PID来监视、控制和调试进程。例如,使用 ps
命令可以查看系统中所有进程的PID,以及它们的状态、资源使用等信息。使用 kill
命令可以通过PID来终止指定的进程。
PID的分配
PID 是由操作系统内核管理和分配的,通常是一个非负整数。PID 通常从一个最小值(通常是1)开始,逐渐递增。当系统运行的进程数达到最大PID值时,PID 会回绕到最小值并重新开始分配。
特殊的PID
- PID 1:在大多数类UNIX操作系统中,PID 1 通常分配给初始化进程(
init
或systemd
),这是系统启动时创建的第一个进程。init
是所有其他进程的祖先,它负责启动系统的其余部分,并在系统运行期间维持各种系统服务。 - PID 0:PID 0 通常被保留给调度进程或空闲进程,这个进程在大多数情况下不会被普通用户或程序直接操作。5
日志
Linux系统日志
Linux提供一个守护进程(后台进程)来处理系统日志:rsyslogd
。rsyslogd
守护进程既能接收用户进程输出的日志,又能接收内核日志。
syslog 函数
应用程序使用syslog
函数和守护进程rsyslog
通信。
#include <syslog.h>
void syslog(int priority, const char* message, ...);
openlog
用来改变syslog
的默认输出方式,进一步结构化日志内容。
#include <syslog.h>
void openlog(const char* ident, int logopt, int facility);
setlogmask
用于设置日志掩码,使得日志级别大于日志掩码的信息被系统忽略。用于在程序发布之后将程序的调试信息关闭。
#include <syslog.h>
int setlogmask(int maskpri);
closelog
函数用于关闭日志功能:
#include <syslog.h>
void closelog();
用户信息
UID、EUID、GID、EGID
在操作系统中,尤其是类 UNIX 系统中,用户和组的标识符用于控制对系统资源的访问权限。以下是对 UID(User ID)、EUID(Effective User ID)、GID(Group ID)和 EGID(Effective Group ID)的解释:
1. UID(User ID)
UID 是用于唯一标识系统中每个用户的整数值。当用户在系统中创建时,系统会为该用户分配一个唯一的 UID。UID 用于控制用户对系统资源的访问权限。
- UID 0:通常保留给
root
用户(超级用户),它拥有系统的所有权限,能够执行任何操作。 - 普通用户的 UID:通常从 1000 或 500 开始(取决于操作系统的配置),用于普通用户。
2. EUID(Effective User ID)
EUID 是用于实际控制用户对文件和系统资源访问权限的用户标识符。它可能与 UID 相同,但在某些情况下可以不同。例如,通过 setuid
程序,普通用户可以临时获得文件所有者的权限。EUID 通常用于判断用户是否有权执行某些操作。
- 典型用法:当一个用户执行
setuid
程序时,该程序的 EUID 会被设置为程序文件的所有者的 UID,而不是当前用户的 UID,从而赋予执行该程序的用户临时的更高权限。
3. GID(Group ID)
GID 是用于标识系统中每个用户组的整数值。与 UID 类似,GID 用于控制用户组对系统资源的访问权限。每个用户在系统中都有一个与之关联的主组(primary group),该组由 GID 表示。
- GID 0:通常属于
root
组,具有最高权限。 - 普通用户的 GID:通常与其主组的 GID 相同。
4. EGID(Effective Group ID)
EGID 是用于实际控制用户对文件和系统资源访问权限的组标识符。类似于 EUID,EGID 可以通过 setgid
程序来修改,以便临时提升执行程序的用户的组权限。
- 典型用法:如果一个文件设置了
setgid
位,那么当任何用户执行该文件时,该进程的 EGID 将被设置为文件所属组的 GID,从而赋予该用户临时的组权限。
总结
- UID:标识用户身份,决定用户本身的所有权限。
- EUID:用于实际判断用户的权限,可能与 UID 不同。
- GID:标识用户所属的组,决定用户组的权限。
- EGID:用于实际判断用户的组权限,可能与 GID 不同。
在权限管理中,UID 和 GID 决定了用户和组的基本身份和权限,而 EUID 和 EGID 决定了用户在特定情境下(如执行带有 setuid
或 setgid
标志的程序时)所拥有的实际权限。这些标识符是 UNIX 权限模型的重要组成部分,用于确保系统资源的安全访问和管理。
有效用户为 root 的进程称为特权进程(privileged processes)
下边一组函数可以获取和设置当前进程的UID、EUID、GID、EGID:
#include <sys/types.h>
#include <unistd.h>
uid_t getuid(); //获取真实用户ID
uid_t geteuid(); //获取有效用户ID
gid_t getgid(); //获取真实组ID
gid_t getegid(); //获取有效组ID
int setuid(uid_t uid); //设置真实用户ID
int seteuid(uid_t uid); //设置有效用户ID
int setgid(gid_t gid); //设置真实组ID
int setegid(gid_t gid); //设置有效组ID
测试 UID 和 EUID 的区别
新建一个 test_uid.cpp
文件,并写入下面的代码:
#include <unistd.h> // <unistd.h>(Unix Standard Definitions)头文件提供对POSIX操作系统API的访问,主要用于提供对POSIX操作系统API的函数原型、符号常量等。
#include <stdio.h> // <stdio.h>(Standard Input Output Header)头文件提供了进行输入和输出操作的函数。
int main(){
uid_t uid = getuid();
uid_t euid = geteuid();
printf("userid :%d , euid: %d \n", uid, euid);
return 0;
}
编译该文件:
gcc test_uid.cpp -o test_uid
运行可执行文件:
./test_uid
修改目标文件的所有者为 root:
sudo chown root:root test_uid
设置目标文件的 set-user-id 标志:
sudo chmod +s test_uid
重新执行该文件(无需再次编译):
修改后,进程的用户 ID 是启动程序的用户 ID:1000,而有效用户 ID 是文件所有者的 ID:0(这里为 root 账户)。
进程间关系
进程组
Linux下的每一个进程都隶属于一个进程组,因此他们除了 PID 信息外,还有进程组ID:PGID。
获取指定进程的 PGID:
#include <unistd.h>
pid_t getpgid(pid_t pid);
设置 PGID:
int setpgid(pid_t pid, pid_t pgid);
每个进程组都有一个首领进程,其 PGID 和 PID 相同。一个进程只能设置自己或其子进程的 PGID。
会话
一些有关联的进程组将形成一个会话(session)。
创建一个会话:
#include <unistd.h>
pid_t setsid(void);
注意,该函数不能由进程组的首领进程调用,否则会出错。对于非组首领的进程来说,调用该函数会生成一个新的会话,并且:
- 调用进程成为会话的首领,此时进程是新会话的唯一成员
- 新建一个进程组,其 PGID 就是调用该函数的进程PID,调用进程成为首领
- 调用进程甩开终端(若有)。
新的会话不会有控制终端(controlling terminal)。如果调用进程原本有控制终端,那么它会被与控制终端分离。
Linux并没有提供会话ID(SID)的概念,但 Linux 系统认为会话ID等同于会话首领所在进程组的 PGID,并提供如下函数来读取 SID:
#include <unistd.h>
pid_t getsid(pid_t pid);
使用 setsid()
创建一个新的会话和进程组:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
pid_t pid;
// 创建一个子进程
pid = fork();
printf("Child Process ID: %d\n", pid);
if (pid < 0) {
perror("fork failed");
exit(EXIT_FAILURE);
} else if (pid > 0) {
// 父进程退出,使子进程成为孤儿进程
exit(EXIT_SUCCESS);
}
// 子进程开始执行,创建新的会话
pid_t sid = setsid();
if (sid < 0) {
perror("setsid failed");
exit(EXIT_FAILURE);
}
// 此时,进程已经成为新的会话和进程组的首领
printf("New session ID: %d\n", sid);
// 继续执行其他代码...
return 0;
}
编译运行:
gcc create_sid.cpp -o create_sid
./create_sid
fork()
是一个在 UNIX 和类 UNIX 操作系统中用于创建新进程的系统调用。调用 fork()
后,操作系统会创建一个新的进程(称为子进程),这个子进程是调用进程(父进程)的副本,除了一些特定的区别外,子进程几乎完全继承了父进程的上下文。
create_sid.cpp
的执行逻辑:
- 在调用
fork()
之前,只有一个进程在运行,这个进程是你的程序的父进程。当fork()
被调用时,操作系统会复制当前进程的全部内容,从而创建一个几乎完全相同的子进程。 fork()
成功时,父进程会收到子进程的 PID,而子进程会收到 0。此时,printf("Child Process ID: %d\n", pid);
会在父进程和子进程中都执行,打印不同的 PID 值。- 在代码中,父进程收到这个返回值后,执行
exit(EXIT_SUCCESS);
,表示父进程正常退出。 - 父进程退出后,子进程就成为了一个孤儿进程(因为它的父进程不再存在),但操作系统会将孤儿进程重新分配给
init
或systemd
进程来管理。 - 子进程在父进程退出后继续运行,并调用
setsid()
来创建一个新的会话。 - 调用
setsid()
后,子进程将成为新会话的会话首领(session leader),并创建一个新的进程组,其中该子进程是进程组的组首领(group leader)。
用ps命令查看进程关系
用 ps 命令可以查看进程、进程组和会话之间的关系:
ps -o pid,ppid,pgid,sid,comm | less
我们在 bash
下执行 ps
和 less
命令,所以 ps
和 less
的父进程是 bash
:bash
的 PID 为 264971,而 ps
和 less
的 PPID 也为 264971。
这三条命令创建了一个会话:SID = 264971
,两个进程组:PGID = 264971, 266761
。
bash
既是会话首领,也是进程组 264971 的首领。
进程组 266761 的首领是 ps。
| less
: 这是一个分页工具,允许你逐页查看命令输出的内容。使用 less
可以方便地查看长输出内容而不会直接在终端上滚动过去。
系统资源限制
Linux上运行的程序会受到资源限制的影响。如物理设备限制:CPU数量,内存数量;系统策略限制:CPU时间;具体实现限制:文件名的最大长度等。
Linux 系统资源限制可以通过如下一对函数读取和设置:
#include <sys/resource.h>
//成功返回0,失败返回-1并设置errno
int getrlimit(int resurce, struct rlimit *rlim);
int setrlimit(int resurce, const struct rlimit *rlim);
rlimit
结构体:
struct rlimit {
//rlim_t 描述资源级别
rlim_t rlim_ur; //软限制
rlim_t rlim_max; //硬限制
}
改变工作目录和根目录
工作目录和根目录概念
在操作系统中,尤其是 UNIX 和类 UNIX 系统中,工作目录和根目录是两个非常重要的概念,它们与文件系统的组织和进程的操作密切相关。
工作目录(Working Directory)
工作目录,有时也称为当前目录,是指一个进程当前所在的目录。所有相对路径的文件操作(如打开文件、读取文件等)都是相对于工作目录进行的。
- 特性:
-
- 每个进程都有一个工作目录。进程可以通过系统调用
chdir()
或命令cd
来改变其工作目录。 - 当你在终端中打开一个 shell 时,通常 shell 的工作目录最初是用户的主目录(如
/home/username
)。 - 在编写程序时,如果使用相对路径(如
./file.txt
),系统会从当前工作目录开始查找文件。
- 每个进程都有一个工作目录。进程可以通过系统调用
- 查看和更改:
-
- 使用
pwd
命令可以查看当前工作目录。 - 使用
cd
命令可以更改当前工作目录。例如,cd /var/log
会将工作目录更改为/var/log
。
- 使用
示例:
根目录(Root Directory)
根目录是文件系统的最顶层目录,用 /
表示。在 UNIX 和类 UNIX 系统中,根目录是文件系统的起点,所有文件和目录都位于根目录之下。
- 特性:
-
- 根目录是文件系统层级结构的起点,没有父目录。
- 所有其他目录(如
/home
、/etc
、/usr
)都是从根目录派生出来的。 - 在系统启动时,操作系统会挂载根文件系统,根目录是整个文件系统的基础。
- 根目录与工作目录的区别:
-
- 根目录是文件系统的最顶层,是绝对路径的起点。
- 工作目录是进程当前正在操作的目录,可以在文件系统的任何位置。
示例:
cd /
ls
在这个示例中,使用 cd /
切换到根目录,并使用 ls
列出了根目录下的文件和子目录。
总结
- 工作目录是当前进程正在操作的目录,所有相对路径的操作都是基于工作目录。
- 根目录是文件系统的最顶层目录,用
/
表示,是绝对路径的起点。
有些服务器程序还需要改变工作目录和根目录,比如web服务器的逻辑根目录不是文件系统的根目录/
,而是站点的根目录,对于 Linux 上的 Web 服务来说,该目录一般是/var/www/
。
获取进程当前工作目录,改变进程工作目录的函数分别是:
#include <unistd.h>
char* getcwd(char* buf, size_t size);
int chdir(const char* path);
改变进程根目录的函数:
#include <unistd.h>
//成功返回0,失败返回-1并设置errno
int chroot(const char* path);
只有特权进程才能改变根目录。
服务器程序后台化
下面代码实现了如何让一个进程以守护进程的方式运行:
bool daemonize() {
// 创建子进程,关闭父进程,这样子进程就不是进程组首进程,就可以调用setsid了
pid_t pid = fork();
if (pid < 0) {
return false;
} else if (pid > 0) {
exit(0);
}
/* 设置文件权限掩码。当进程创建新文件(使用open()系统调用),文件的权限将是mode & 0777 */
umask(0);
// 创建新会话,本进程将成为进程组的首领
pid_t sid = setsid();
if (sid < 0) {
return false;
}
// 切换工作目录,防止当前工作目录所在文件系统不能卸载
if ((chdir("/")) < 0) {
return false;
}
// 关闭所有文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
// 此处省略了关闭其他已打开的文件描述符的代码
// 将标准输入、标准输出、标准错误重定向到/dev/null文件
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
return true;
}
Linux 提供了完成同样功能的库函数:
#include <unistd.h>
int daemon(int nochdir, int noclose);
这个 daemonize()
函数执行了创建守护进程的标准步骤:
- 通过
fork()
创建一个子进程,并让父进程退出。 - 通过
setsid()
创建一个新的会话,脱离控制终端,并成为会话首领。 - 设置文件权限掩码和切换工作目录。
- 关闭标准文件描述符并将它们重定向到
/dev/null
。
如果所有步骤成功,则返回 true
表示守护进程创建成功。否则返回 false
。这段代码展示了将一个普通进程转化为守护进程的标准方法,是在后台运行长时间任务的基础方法之一。
接下来对上面代码进行解释:
代码解释
创建子进程并关闭父进程
pid_t pid = fork();
if (pid < 0) {
return false;
} else if (pid > 0) {
exit(0);
}
fork()
创建一个子进程。如果 fork()
失败(返回值为负),则返回 false
表示守护进程创建失败。
如果 fork()
成功,父进程收到子进程的 PID 并退出 (exit(0)
),子进程继续执行。
这样做的目的是让子进程成为孤儿进程,由 init
或 systemd
进程接管,从而保证守护进程在父进程结束后仍然继续运行。
设置文件权限掩码
umask(0)
清除文件模式创建掩码,确保进程创建的文件权限不受父进程的文件权限掩码影响。这样,进程创建的文件将具有最大的权限(取决于创建文件时指定的权限)。
创建新会话
pid_t sid = setsid();
if (sid < 0) {
return false;
}
setsid()
创建一个新的会话,使当前进程成为会话的首领,并与原来的控制终端分离。该进程成为新会话的会话首领和进程组的首领,并且没有控制终端。
切换工作目录
if ((chdir("/")) < 0) {
return false;
}
chdir("/")
将工作目录切换到根目录 /
。这样做的目的是避免当前工作目录所在的文件系统不能卸载。
关闭所有文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
关闭标准输入(STDIN_FILENO
)、标准输出(STDOUT_FILENO
)和标准错误(STDERR_FILENO
)的文件描述符,以避免守护进程不小心使用这些文件描述符。
重定向标准输入、输出和错误
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
将标准输入重定向到 /dev/null
,并将标准输出和标准错误也重定向到 /dev/null
。/dev/null
是一个特殊的文件,读取它会返回 EOF,写入它的数据将被丢弃。这样可以确保守护进程不向任何终端输出信息,也不会从任何终端读取输入。
参考文章
- Linux高性能服务器编程-游双——第七章 Linux服务器程序规范_linux高性能服务器编程 pdf-CSDN博客
- Linux高性能服务器编程 学习笔记 第七章 Linux服务器程序规范-CSDN博客