参考引用
- UNIX 环境高级编程 (第3版)
- 嵌入式Linux C应用编程-正点原子
int main(void)
int main(int argc, char *argv[]) // 如果需要向应用程序传参,则选择该种写法
#include
// function:函数指针,指向注册的函数,此函数无需传入参数、无返回值
// 返回值:成功返回 0;失败返回非 0
int atexit(void (*function)(void));
#include
#include
static void bye(void) {
puts("Goodbye!");
}
int main(int argc, char *argv[]) {
if (atexit(bye)) {
fprintf(stderr, "cannot set exit function\n");
exit(-1);
}
// 如果程序当中使用了 _exit() 或 _Exit() 终止进程而并非是 exit() 函数,那么将不会执行注册的终止处理函数
exit(0);
}
$ gcc test.c -o test
$ ./test
Goodbye!
#include
#include
pid_t getpid(void);
// 使用示例
pid_t pid = getpid();
printf("本进程的 PID 为: %d\n", pid);
#include
#include
pid_t getppid(void);
// 使用示例
// 获取本进程 pid
pid_t pid = getpid();
printf("本进程的 PID 为: %d\n", pid);
// 获取父进程 pid
pid = getppid();
printf("父进程的 PID 为: %d\n", pid);
$ export LINUX_APP=123456 # 添加 LINUX_APP 环境变量
$ export -n LINUX_APP # 删除 LINUX_APP 环境变量
extern char **environ; // 申明外部全局变量 environ
#include
#include
extern char **environ;
int main(int argc, char *argv[]) {
int i;
/* 打印进程的环境变量 */
// 通过字符串数组元素是否等于 NULL 来判断是否已经到了数组的末尾
for (i = 0; environ[i] != NULL; i++)
puts(environ[i]);
exit(0);
}
如果只想要获取某个指定的环境变量,可以使用库函数 getenv()
#include
// name :指定获取的环境变量名称
// 返回值:如果存放该环境变量,则返回该环境变量的值对应字符串的指针;如果不存在该环境变量,则返回 NULL
char *getenv(const char *name);
示例
#include
#include
int main(int argc, char *argv[]) {
const char *str_val = NULL;
if (argc < 2) {
fprintf(stderr, "Error: 请传入环境变量名称\n");
exit(-1);
}
/* 获取环境变量 */
str_val = getenv(argv[1]);
if (str_val == NULL) {
fprintf(stderr, "Error: 不存在[%s]环境变量\n", argv[1]);
exit(-1);
}
/* 打印环境变量的值 */
printf("环境变量的值: %s\n", str_val);
exit(0);
}
$ gcc getenv.c -o getenv
$ ./getenv PATH
环境变量的值: /opt/ros/melodic/bin:/opt/gcc-arm-none-eabi-9-2020-q2-update/bin:/home/yue/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
#include
// string:一个字符串指针,指向 name = value 形式的字符串
// 返回值:成功返回 0;失败将返回非 0 值,并设置 errno
int putenv(char *string);
setenv() 函数可替代 putenv() 函数,推荐使用 setenv() 函数
#include
// name:需要添加或修改的环境变量名称
// value:环境变量的值
// overwrite:若 name 标识的环境变量已存在且 overwrite 为 0,setenv() 函数将不改变现有环境变量的值
// 若 name 标识的环境变量已存在且 overwrite 为非 0,则覆盖,不存在则表示添加新的环境变量
// 返回值:成功返回 0;失败将返回-1,并设置 errno
int setenv(const char *name, const char *value, int overwrite);
示例
#include
#include
int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "Error: 传入 name value\n");
exit(-1);
}
/* 添加环境变量 */
// 返回值:成功返回 0;失败将返回-1
if (setenv(argv[1], argv[2], 0)) {
perror("setenv error");
exit(-1);
}
exit(0);
}
#include
int unsetenv(const char *name);
environ = NULL;
#include
int clearenv(void);
C 语言程序由以下几部分组成
Linux 系统中采用了虚拟内存管理技术,应用程序运行在一个虚拟地址空间中
Linux 系统中每一个进程都在自己独立的地址空间中运行,在 32 位系统中,每个进程的逻辑地址空间均为 4GB:其中用户进程享有 3G 的空间,而内核独自享有剩下的 1G 空间
虚拟地址会通过硬件 MMU(内存管理单元)映射到实际的物理地址空间中,建立虚拟地址到物理地址的映射关系后,对虚拟地址的读写操作实际上就是对物理地址的读写操作,MMU 会将虚拟地址 “翻译” 为对应的物理地址
计算机物理内存的大小是固定的,如果操作系统没有虚拟地址机制,所有应用程序访问的内存地址就是实际的物理地址,要将所有应用程序加载到内存中,但是实际的物理内存只有 4G,所以就会出现一些问题
针对上述问题引入了虚拟地址机制。程序访问存储器所使用的逻辑地址就是虚拟地址,通过逻辑地址映射到真正的物理内存上,所有应用程序运行在自己的虚拟地址空间中,使得进程的虚拟地址空间和物理地址空间隔离开来,这样做带来了很多的优点
一个现有的进程可以调用系统调用 fork() 函数创建一个新的进程
#include
pid_t fork(void);
创建子进程的作用
如何区分父、子进程?
fork() 成功调用后将存在两个进程,一个是原进程(父进程),另一个则是创建出来的子进程,并且每个进程都会从 fork() 函数的返回处继续执行,会导致调用 fork() 返回两次值,子进程返回一个值,父进程返回一个值
- fork() 调用成功后,将会在父进程中返回子进程的 PID,而在子进程中返回值是 0
- 如果调用失败,父进程返回值 -1,不创建子进程,并设置 errno
fork() 调用成功后,子进程和父进程会继续执行 fork() 调用之后的指令
示例 1:使用 fork() 创建子进程
#include
#include
#include
int main(void) {
pid_t pid;
pid = fork();
switch (pid) {
case -1:
perror("fork error");
exit(-1);
case 0:
printf("这是子进程打印信息\n" , getpid(), getppid());
// 调用 fork() 后,父、子进程中一般只有一个通过 exit() 退出进程,而另一个则应使用 _exit() 退出
_exit(0); // 子进程使用 _exit() 退出
default:
printf("这是父进程打印信息\n" , getpid(), pid);
exit(0); // 父进程使用 exit() 退出
}
}
$ gcc fork.c -o fork
$ ./fork
这是父进程打印信息, 子进程 pid: 3370>
这是子进程打印信息, 父进程 pid: 3369>
示例 2
#include
#include
#include
int main(void) {
pid_t pid;
pid = fork();
switch (pid) {
case -1:
perror("fork error");
exit(-1);
case 0:
printf("这是子进程打印信息\n");
printf("%d\n", pid);
_exit(0);
default:
printf("这是父进程打印信息\n");
printf("%d\n", pid);
exit(0);
}
}
$ gcc fork2.c -o fork2
$ ./fork2
# 对同一个局部变量,父、子进程打印出来的值不同,因为 fork() 调用返回值不同,在父、子进程中赋予了 pid 不同的值
这是父进程打印信息
3391
这是子进程打印信息
0
子进程被创建出来之后,便是一个独立的进程,拥有自己独立的进程空间、系统内唯一的进程号并拥有自己独立的 PCB(进程控制块),子进程会被内核同等调度执行,参与到系统的进程调度中
系统调度
- Linux 是一个多任务、多进程、多线程的操作系统,系统启动后会运行成百上千个不同的进程,对于单核 CPU 计算机来说,在某一个时间它只能运行某一个进程的代码指令,那其它进程怎么办呢(多核处理器也是如此,同一时间每个核它只能运行某一个进程的代码)?这里就出现了调度的问题,系统是这样做的:每一个进程(或线程)执行一段固定的时间,时间到了之后切换执行下一个进程或线程,依次轮流执行,这就称为调度
- 系统调度的基本单元是线程
调用 fork() 函数之后,子进程会获得父进程所有文件描述符的副本,这些副本的创建方式类似于 dup(),这也意味着父、子进程对应的文件描述符均指向相同的文件表,如果子进程更新了文件偏移量,那么这个改变也会影响到父进程中相应文件描述符的位置偏移量
示例 1:子进程继承父进程文件描述符实现文件共享
#include
#include
#include
int main(void) {
pid_t pid;
int fd;
int i;
fd = open("./test.txt", O_RDWR | O_TRUNC);
if (fd < 0) {
perror("open error");
exit(-1);
}
pid = fork();
switch (pid) {
case -1:
perror("fork error");
close(fd);
exit(-1);
case 0:
/* 子进程 循环写入 4 次 */
for (i = 0; i < 4; i++) {
write(fd, "1122", 4);
}
close(fd);
_exit(0);
default:
/* 父进程 循环写入 4 次 */
for (i = 0; i < 4; i++) {
write(fd, "AABB", 4);
}
close(fd);
exit(0);
}
}
$ gcc fork3.c -o fork3
$ ./fork3
$ cat test.txt
# 父、子进程分别对同一个文件进行写入操作,结果是接续写,父、子进程在每次写入时都是从文件的末尾写入
# 因为子进程继承了父进程的文件描述符,两个文件描述符都指向了一个相同的文件表
# 子进程改变了文件的位置偏移量就会作用到父进程,同理,父进程改变了文件的位置偏移量就会作用到子进程
AABBAABBAABBAABB1122112211221122
示例 2:父、子各自打开同一个文件实现文件共享
#include
#include
#include
#include
#include
#include
int main(void) {
pid_t pid;
int fd;
int i;
pid = fork();
switch (pid) {
case -1:
perror("fork error");
exit(-1);
case 0:
fd = open("./test.txt", O_WRONLY);
if (fd < 0) {
perror("open error");
_exit(-1);
}
for (i = 0; i < 4; i++) {
write(fd, "1122", 4);
}
close(fd);
_exit(0);
default:
fd = open("./test.txt", O_WRONLY);
if (fd < 0) {
perror("open error");
exit(-1);
}
for (i = 0; i < 4; i++) {
write(fd, "AABB", 4);
}
close(fd);
exit(0);
}
}
$ gcc fork4.c -o fork4
$ ./fork4
$ cat test.txt
# 因为父、子进程的这两个文件描述符分别指向不同的文件表,意味着它们有各自的文件偏移量
# 一个进程修改了文件偏移量并不会影响另一个进程的文件偏移量,所以写入的数据会出现覆盖的情况
1122112211221122
除了 fork() 系统调用之外,Linux 系统还提供了 vfork() 系统调用用于创建子进程,vfork() 与 fork() 函数在功能上是相同的,并且返回值也相同,在一些细节上存在区别
#include
#include
pid_t vfork(void);
使用 fork() 系统调用的代价是很大的,它复制了父进程中的数据段和堆栈段中的绝大部分内容,这将会消耗比较多的时间导致效率降低
fork() 上述缺陷解决办法
虽然 vfork() 在效率上优于 fork(),但 vfork() 可能会导致一些 bug,尽量避免使用 vfork() 创建子进程
vfork() 与 fork() 函数主要有以下两个区别
调用 fork() 之后,子进程成为了一个独立的进程,可被系统调度运行,而父进程也继续被系统调度运行,这里出现了一个问题:调用 fork 之后,无法确定父、子两个进程谁将率先访问 CPU,也就是说无法确认谁先被系统调用运行(在多核处理器中,它们可能会同时各自访问一个 CPU),这将导致谁先运行、谁后运行这个顺序是不确定的
fork() 竞争条件测试
#include
#include
#include
int main(void) {
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
printf("子进程打印信息\n");
_exit(0);
default:
printf("父进程打印信息\n");
exit(0);
}
}
$ gcc fork5.c -o fork5
# 绝大部分情况下,父进程会先于子进程被执行,但并不排除子进程先于父进程被执行的可能性
$ ./fork5
父进程打印信息
子进程打印信息
$ ./fork5
父进程打印信息
子进程打印信息
$ ./fork5
子进程打印信息
父进程打印信息
$ ./fork5
父进程打印信息
子进程打印信息
...
对于有些特定的应用程序,它对于执行的顺序有一定要求的,如:它必须要求父进程先运行,或者必须要求子进程先运行,程序产生正确的结果依赖于特定的执行顺序,那么将可能因竞争条件而导致失败,无法得到正确的结果
#include
#include
#include
#include
#include
static void sig_handler(int sig) {
printf("接收到信号\n");
}
int main(void) {
struct sigaction sig = {0};
sigset_t wait_mask;
/* 初始化信号集 */
sigemptyset(&wait_mask);
/* 设置信号处理方式 */
sig.sa_handler = sig_handler;
sig.sa_flags = 0;
if (sigaction(SIGUSR1, &sig, NULL) == -1) {
perror("sigaction error");
exit(-1);
}
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程开始执行\n");
printf("子进程打印信息\n");
printf("~~~~~~~~~~~~~~~\n");
sleep(2);
kill(getppid(), SIGUSR1); // kill 命令发送信号给父进程并唤醒它
_exit(0);
default:
/* 父进程 */
if (sigsuspend(&wait_mask) != -1) { // 挂起、阻塞
exit(-1);
}
printf("父进程开始执行\n");
printf("父进程打印信息\n");
exit(0);
}
}
$ gcc fork6.c -o fork6
$ ./fork6
子进程开始执行
子进程打印信息
~~~~~~~~~~~~~~~
接收到信号
父进程开始执行
父进程打印信息
#
$ ps -aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.0 225492 9408 ? Ss 12:47 0:29 /sbin/init splash
root 2 0.0 0.0 0 0 ? S 12:47 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? I< 12:47 0:00 [rcu_gp]
...
...
#include
#include
#include
int main(void) {
printf("Hello World!\n");
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
exit(0);
default:
/* 父进程 */
exit(0);
}
}
$ gcc fork7.c -o fork7
$ ./fork7
Hello World!
// 若将上述代码的 \n 去掉
//printf("Hello World!\n");
printf("Hello World!");
$ gcc fork8.c -o fork8
$ ./fork7
Hello World!Hello World!$
如何避免上述重复输出问题?
- 对于行缓冲设备,可以加上对应换行符 \n
- 在调用 fork() 之前,使用函数 fflush() 来刷新 stdio 缓冲区
- 子进程调用 _exit() 退出进程,调用 _exit() 在退出时不会刷新 stdio 缓冲区
系统调用 wait() 可以等待进程的任一子进程终止,同时获取子进程的终止状态信息
#include
#include
// status:用于存放子进程终止时的状态信息,可以为 NULL,表示不接收子进程终止时的状态信息
// 返回值:若成功则返回终止的子进程对应的进程号;失败则返回-1
pid_t wait(int *status);
系统调用 wait() 将执行如下动作
参数 status 不为 NULL 的情况下,则 wait() 会将子进程的终止时的状态信息存储在它指向的 int 变量中,可以通过以下宏来检查 status 参数
示例:循环创建 3 个子进程并回收
#include
#include
#include
#include
#include
#include
int main(void) {
int status;
int ret;
int i;
/* 循环创建 3 个子进程 */
for (i = 1; i <= 3; i++) {
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(i);
_exit(i);
default:
/* 父进程 */
break;
}
}
sleep(1);
printf("-----------------\n");
for (i = 1; i <= 3; i++) {
ret = wait(&status);
if (ret == -1) {
if (ECHILD == errno) {
printf("没有需要等待回收的子进程\n");
exit(0);
} else {
perror("wait error");
exit(-1);
}
}
printf("回收子进程<%d>, 终止状态<%d>\n", ret, WEXITSTATUS(status));
}
exit(0);
}
$ gcc wait.c -o wait
$ ./wait
子进程<3961>被创建
子进程<3962>被创建
子进程<3963>被创建
-----------------
回收子进程<3961>, 终止状态<1>
回收子进程<3962>, 终止状态<2>
回收子进程<3963>, 终止状态<3>
使用 wait() 系统调用存在着一些限制
系统调用 waitpid() 函数可以突破上述限制
#include
#include
// pid:用于表示需要等待的某个具体子进程
/*
pid > 0,表示等待进程号为 pid 的子进程
pid = 0,则等待与调用进程(父进程)同一个进程组的所有子进程
pid < -1,则会等待进程组标识符与 pid 绝对值相等的所有子进程
pid = -1,则等待任意子进程,此时 wait(&status) 与 waitpid(-1, &status, 0) 等价
*/
// status:与 wait() 函数的 status 参数意义相同
pid_t waitpid(pid_t pid, int *status, int options);
options: 是一个位掩码,可以包括 0 个或多个如下标志
示例 1
// 将 10.1 小节中的 wait(&status)替换成了 waitpid(-1, &status, 0)
$ gcc waitpid.c -o waitpid
$ ./waitpid
子进程<4009>被创建
子进程<4010>被创建
子进程<4011>被创建
-----------------
回收子进程<4009>, 终止状态<1>
回收子进程<4010>, 终止状态<2>
回收子进程<4011>, 终止状态<3>
示例 2:waitpid() 轮训方式
#include
#include
#include
#include
#include
#include
int main(void) {
int status;
int ret;
int i;
/* 循环创建 3 个子进程 */
for (i = 1; i <= 3; i++) {
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(i);
_exit(i);
default:
/* 父进程 */
break;
}
}
sleep(1);
printf("-----------------\n");
for (;;) {
ret = waitpid(-1, &status, WNOHANG);
if (ret < 0) {
if (ECHILD == errno) {
exit(0);
} else {
perror("wait error");
exit(-1);
}
} else if (ret == 0) {
continue;
} else
printf("回收子进程<%d>, 终止状态<%d>\n", ret, WEXITSTATUS(status));
}
exit(0);
}
$ gcc wait2.c -o wait2
$ ./wait2
子进程<4047>被创建
子进程<4048>被创建
子进程<4049>被创建
-----------------
回收子进程<4047>, 终止状态<1>
回收子进程<4048>, 终止状态<2>
回收子进程<4049>, 终止状态<3>
父进程先于子进程结束,此时子进程变成了一个 “孤儿”,把这种进程称为孤儿进程
孤儿进程测试
#include
#include
#include
int main(void) {
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建, 父进程<%d>\n", getpid(), getppid());
sleep(3); // 子进程休眠 3 秒钟,保证父进程先结束
printf("父进程<%d>\n", getppid()); // 再次获取父进程 pid,此时 “生父” 已经结束
_exit(0);
default:
/* 父进程 */
break;
}
sleep(1); // 父进程休眠 1 秒钟,保证在父进程结束前子进程能够打印父进程进程号
printf("父进程结束!\n");
exit(0);
}
$ gcc child.c -o child
$ ./child
子进程<4179>被创建, 父进程<4178>
父进程结束!
$ 父进程<1082> # 打印结果并不是 1,意味着并不是 init 进程
# 查看进程号 1082 对应的进程如下
# /lib/systemd/systemd 是 Ubuntu 系统下的一个后台守护进程,可负责 “收养” 孤儿进程
$ ps -axu
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
yue 1082 0.0 0.0 78228 9376 ? Ss 12:48 0:00 /lib/systemd/systemd --user
...
进程结束之后,通常需要其父进程为其 “收尸”,回收子进程占用的一些内存资源,父进程通过调用 wait()(或其变体 waitpid()、waitid() 等)函数回收子进程资源,归还给系统,如果子进程先于父进程结束,此时父进程还未来得及给子进程 “收尸”,那么此时子进程就变成了一个僵尸进程
示例:产生僵尸进程
// 子进程已经退出,但其父进程并没调用 wait()为其“收尸”,使得子进程成为一个僵尸进程
#include
#include
#include
int main(void) {
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(1);
printf("子进程结束\n");
_exit(0);
default:
/* 父进程 */
break;
}
for ( ; ; )
sleep(1);
exit(0);
}
$ gcc child2.c -o child2
$ ./child2
子进程<4317>被创建
子进程结束
$ ps -axu
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
...
yue 4316 0.0 0.0 4388 772 pts/0 S+ 22:51 0:00 ./child2
# 状态栏 STAT 显示 “Z”(zombie,僵尸),表示它是一个僵尸进程
# defunct 僵尸
yue 4317 0.0 0.0 0 0 pts/0 Z+ 22:51 0:00 [child2]
...
当发生以下两种情况时,父进程会收到 SIGCHLD 信号
子进程的终止属于异步事件,父进程事先是无法预知的,如果父进程有自己需要做的事情,它不能一直 wait() 阻塞等待子进程终止(或轮训),这样父进程将啥事也做不了,可通过 SIGCHLD 信号解决这个问题
当调用信号处理函数时,会暂时将引发调用的信号添加到进程的信号掩码中(除非 sigaction() 指定了 SA_NODEFER 标志),这样一来,当 SIGCHLD 信号处理函数正在为一个终止的子进程 “收尸” 时,如果相继有两个子进程终止,即使产生了两次 SIGCHLD 信号,父进程也只能捕获到一次 SIGCHLD 信号,结果是父进程的 SIGCHLD 信号处理函数每次只调用一次 wait(),那么就会导致有些僵尸进程成为 “漏网之鱼”
// 下述代码一直循环下去,直至 waitpid() 返回 0,表明再无僵尸进程存在;或者返回 -1,表明有错误发生
// 应在创建任何子进程之前,为 SIGCHLD 信号绑定处理函数
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
示例:通过 SIGCHLD 信号实现异步方式监视子进程
#include
#include
#include
#include
#include
#include
static void wait_child(int sig) {
/* 替子进程收尸 */
printf("父进程回收子进程\n");
while (waitpid(-1, NULL, WNOHANG) > 0)
continue;
}
int main(void) {
struct sigaction sig = {0};
/* 为 SIGCHLD 信号绑定处理函数 */
sigemptyset(sig.sa_mask);
sig.sa_handler = wait_child;
sig.sa_flags = 0;
if (-1 == sigaction(SIGCHLD, &sig, NULL)) {
perror("sigaction error");
exit(-1);
}
switch (fork()) {
case -1:
perror("fork error");
exit(-1);
case 0:
/* 子进程 */
printf("子进程<%d>被创建\n", getpid());
sleep(1);
printf("子进程结束\n");
_exit(0);
default:
/* 父进程 */
break;
}
sleep(3);
exit(0);
}
$ gcc test.c -o test
$ ./test
子进程<4318>被创建
子进程结束
父进程回收子进程
#include
// filename:指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径
// argv:指定传递给新程序的命令行参数。是一个字符串数组,该数组对应于 main 函数第二个参数 argv
// 且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。argv[0] 对应的便是新程序自身路径名
// envp:参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程序的 environ 数组
// 同样也是以 NULL 结束,所指向的字符串格式为 name=value
// 返回值:execve 调用成功将不会返回;失败将返回-1,并设置 errno
int execve(const char *filename, char *const argv[], char *const envp[]);
// testApp.c
#include
#include
#include
int main(int argc, char *argv[]) {
char *arg_arr[5];
char *env_arr[5] = {"NAME=app", "AGE=25", "SEX=man", NULL};
if (2 > argc)
exit(-1);
arg_arr[0] = argv[1];
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execve(argv[1], arg_arr, env_arr);
perror("execve error");
exit(-1);
}
// newApp.c
#include
#include
extern char **environ;
int main(int argc, char *argv[]) {
char **ep = NULL;
int j;
for (j = 0; j < argc; j++)
printf("argv[%d]: %s\n", j, argv[j]);
puts("env:");
for (ep = environ; *ep != NULL; ep++)
printf(" %s\n", *ep);
exit(0);
}
$ gcc testApp.c -o testApp
$ gcc newApp.c -o newApp
$ ./testApp ./newApp
argv[0]: ./newApp
argv[1]: Hello
argv[2]: World
env:
NAME=app
AGE=25
SEX=man
为什么需要在子进程中执行新程序?
- 虽然可以直接在子进程分支编写子进程需要运行的代码,但是不够灵活,扩展性不够好,直接将子进程需要运行的代码单独放在一个可执行文件中不是更好吗,所以就出现了 exec 操作
exec 族函数包括多个不同的函数,这些函数命名都以 exec 为前缀,这些库函数都是基于系统调用 execve() 实现的,通常将调用这些 exec 函数加载一个外部新程序的过程称为 exec 操作
#include
extern char **environ;
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
execl() 和 execv() 都是基本的 exec 函数,都可用于执行一个新程序,它们之间的区别在于参数格式不同
// execv 传参
char *arg_arr[5];
arg_arr[0] = "./newApp";
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execv("./newApp", arg_arr);
// execl 传参
execl("./newApp", "./newApp", "Hello", "World", NULL);
execlp() 和 execvp() 在 execl() 和 execv() 基础上加了一个 p,这个 p 其实表示的是 PATH 路径
execle() 和 execvpe() 这两个函数在命名上加了一个 e,这个 e 其实表示的是 environment 环境变量
// execvpe 传参
char *env_arr[5] = {"NAME=app", "AGE=25", "SEX=man", NULL};
char *arg_arr[5];
arg_arr[0] = "./newApp";
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execvpe("./newApp", arg_arr, env_arr);
// execle 传参
execle("./newApp", "./newApp", "Hello", "World", NULL, env_arr);
使用 system() 函数可以很方便地在程序当中执行任意 shell 命令
#include
// command :参数 command 指向需要执行的 shell 命令,以字符串的形式提供,如 "ls -al"、"echo HelloWorld" 等
int system(const char *command);
system() 函数内部是通过调用 fork()、execl() 及 waitpid() 来实现它的功能,首先 system() 会调用 fork() 创建一个子进程来运行 shell(可以把这个子进程称为 shell 进程),并通过 shell 执行参数 command 所指定的命令
system() 的返回值
system() 的主要优点在于使用上方便简单,编程时无需自己处理对 fork()、exec 函数、waitpid() 以及 exit() 等调用细节,system() 内部会代为处理,但使用 system() 函数其效率会大打折扣,如果程序对效率或速度有所要求,那么不建议直接使用 system()
示例:system() 函数使用
#include
#include
int main(int argc, char *argv[]) {
int ret;
if (argc < 2)
exit(-1);
ret = system(argv[1]);
if (ret == -1)
fputs("system error.\n", stderr);
else {
if (WIFEXITED(ret) && (WEXITSTATUS(ret) == 127))
fputs("could not invoke shell.\n", stderr);
}
exit(0);
}
$ gcc sys.c -o sys
$ ./sys pwd
/home/yue/桌面/test
$ ./sys 'ls -al'
总用量 292
drwxrwxr-x 2 yue yue 4096 12月 24 14:43 .
drwxr-xr-x 3 yue yue 4096 12月 20 21:31 ..
-rwxrwxr-x 1 yue yue 8584 12月 23 22:48 child
...
...
Linux 系统下进程通常存在 6 种不同的状态
进程各个状态之间的转换关系
进程间存在着多种不同的关系,主要包括:无关系(相互独立)、父子进程关系、进程组以及会话
#include
// 可通过参数 pid 指定获取对应进程的进程组 ID,如果参数 pid 为 0 表示获取调用者进程的进程组 ID
// getpgid() 函数成功将返回进程组 ID;失败将返回-1、并设置 errno
pid_t getpgid(pid_t pid);
// 返回值总是调用者进程对应的进程组 ID,getpgrp() 就等价于 getpgid(0)
pid_t getpgrp(void);
#include
int setpgid(pid_t pid, pid_t pgid);
int setpgrp(void); // setpgrp() 函数等价于 setpgid(0, 0)
一个会话可包含一个或多个进程组,但只能有一个前台进程组,其它的是后台进程组,每个会话都有一个会话首领(leader),即创建会话的进程
#include
// 如果参数 pid 为 0,则返回调用者进程的会话 ID;如果参数 pid 不为 0,则返回参数 pid 指定的进程对应的会话 ID
// 成功情况下,该函数返回会话 ID,失败则返回 -1、并设置 errno
pid_t getsid(pid_t pid);
#include
// 如果调用者进程不是进程组的组长进程,调用 setsid() 将创建一个新的会话,调用者进程是新会话的首领进程
// 同样也是一个新的进程组的组长进程,调用 setsid() 创建的会话将没有控制终端
// setsid() 调用成功将返回新会话的会话 ID;失败将返回-1,并设置 errno
pid_t setsid(void);
守护进程(Daemon)也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点
守护进程 Daemon,通常简称为 d,一般进程名后面带有 d 就表示它是一个守护进程
# TTY 一栏是问号 ?表示该进程没有控制终端,也就是守护进程
# 其中 COMMAND 一栏使用中括号 [] 括起来的表示内核线程,这些线程是在内核里创建
# 没有用户空间代码,因此没有程序文件名和命令行,通常采用 k(Kernel)开头
$ ps -ajx
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:15 /sbin/init splash
0 2 0 0 ? -1 S 0 0:00 [kthreadd]
2 3 0 0 ? -1 I< 0 0:00 [rcu_gp]
...
...
1. 创建子进程、终止父进程
2. 子进程调用 setsid 创建会话
3. 将工作目录更改为根目录
4. 重设文件权限掩码 umask
5. 关闭不再需要的文件描述符
6. 将文件描述符号为 0、1、2 定位到 /dev/null
7. 其它:忽略 SIGCHLD 信号
示例:守护进程
#include
#include
#include
#include
#include
#include
#include
int main(void) {
pid_t pid;
int i;
/* 创建子进程 */
pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
} else if (pid > 0) { // 父进程
exit(0); // 直接退出
}
/* 1.创建新的会话、脱离控制终端 */
if (setsid() < 0) {
perror("setsid error");
exit(-1);
}
/* 2.设置当前工作目录为根目录 */
if (chdir("/") < 0) {
perror("chdir error");
exit(-1);
}
/* 3.重设文件权限掩码 umask */
umask(0);
/* 4.关闭所有文件描述符 */
// sysconf(_SC_OPEN_MAX) 用于获取当前系统允许进程打开的最大文件数量
for (i = 0; i < sysconf(_SC_OPEN_MAX); i++) {
close(i);
}
/* 5.将文件描述符号为 0、1、2 定位到/dev/null */
// /dev/null 是一个黑洞文件,看不到输出信息
open("/dev/null", O_RDWR);
dup(0);
dup(0);
/* 6.忽略 SIGCHLD 信号 */
signal(SIGCHLD, SIG_IGN);
/* 正式进入到守护进程 */
for (;;) {
sleep(1);
puts("daemon process running...");
}
exit(0);
}
$ gcc dae.c -o dae
$ ./dae
# 运行之后,没有任何打印信息输出,原因在于守护进程已经脱离了控制终端,它的打印信息并不会输出显示到终端
# 使用 "ps -ajx" 命令查看进程,dae 进程成为了一个守护进程,与控制台脱离
$ ps -ajx
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:19 /sbin/init splash
0 2 0 0 ? -1 S 0 0:00 [kthreadd]
...
1072 3714 3714 3714 ? -1 Ss 1000 0:00 ./dae
2098 3715 3715 2098 pts/0 3715 R+ 1000 0:00 ps -ajx
当关闭当前控制终端时,dae 进程并不会受到影响,依然会正常继续运行;而对于普通进程来说,终端关闭,那么由该终端运行的所有进程都会被强制关闭,因为它们处于同一个会话。守护进程可以通过终端命令行启动,但通常它们是由系统初始化脚本进行启动,如:/etc/rc* 或 /etc/init.d/* 等
当用户准备退出会话时,系统向该会话发出 SIGHUP 信号,会话将 SIGHUP 信号发送给所有子进程,子进程接收到 SIGHUP 信号后,便会自动终止,当所有会话中的所有进程都退出时,会话也就终止了
示例:忽略 SIGHUP 测试
#include
#include
#include
#include
int main(void) {
signal(SIGHUP, SIG_IGN);
for (; ;) {
sleep(1);
puts("进程运行中......");
}
}
$ gcc sighup.c -o sighup
$ ./sighup
进程运行中......
进程运行中......
进程运行中......
...
# 关闭终端,再重新打开终端,使用下述命令查看 sighup 进程是否存在
$ ps -ajx | grep sighup
3774 3810 3809 3774 pts/0 3809 S+ 1000 0:00 grep --color=auto sighup
用一个文件的存在与否来做标志,在程序运行正式代码之前,先判断一个特定的文件是否存在
示例:简单方式实现单例模式运行
#include
#include
#include
#include
#include
#include
#define LOCK_FILE "./testApp.lock"
static void delete_file(void) {
remove(LOCK_FILE);
}
int main(void) {
/* 打开文件 */
int fd = open(LOCK_FILE, O_RDONLY | O_CREAT | O_EXCL, 0666);
if (-1 == fd) {
fputs("不能重复执行该程序!\n", stderr);
exit(-1);
}
/* 注册进程终止处理函数 */
if (atexit(delete_file))
exit(-1);
puts("程序运行中...");
sleep(10);
puts("程序结束");
close(fd); // 关闭文件
exit(0);
}
使用文件锁方式才是实现单例模式运行靠谱的方法
#include
#include
#include
#include
#include
#include
#include
#define LOCK_FILE "./testApp.pid"
int main(void) {
char str[20] = {0};
int fd;
/* 打开 lock 文件,如果文件不存在则创建 */
fd = open(LOCK_FILE, O_WRONLY | O_CREAT, 0666);
if (-1 == fd) {
perror("open error");
exit(-1);
}
/* 以非阻塞方式获取文件锁 */
// 使用 flock 尝试获取文件锁,调用 flock() 时指定了互斥锁标志 LOCK_NB,意味着同时只能有一个进程拥有该锁
if (-1 == flock(fd, LOCK_EX | LOCK_NB)) {
// 如果获取锁失败,表示该程序已经启动了,无需再次执行,然后退出
fputs("不能重复执行该程序!\n", stderr);
close(fd);
exit(-1);
}
puts("程序运行中...");
ftruncate(fd, 0); // 将文件长度截断为 0
sprintf(str, "%d\n", getpid());
// 如果获取锁成功,将进程的 PID 写入到该文件中
write(fd, str, strlen(str));
for ( ; ; )
sleep(1);
exit(0);
}
$ gcc app.c -o app
$ ./app &
[1] 3959
程序运行中...
$
$ ./app
不能重复执行该程序!
$ ./app
不能重复执行该程序!
...
在 Linux 系统中 /var/run/ 目录下有很多以 .pid 为后缀结尾的文件,这个实际上是为了保证程序以单例模式运行而设计的,作为程序实现单例模式运行所需的特定文件
$ cd /var/run
$ ls
acpid.pid boltd cups gdm3.pid log mount pppconfig shm sudo udev uuidd
acpid.socket console-setup dbus initctl mlocate.daily.lock network rsyslogd.pid snapd-snap.socket systemd udisks2 vboxadd-service.sh
alsa crond.pid dhclient-enp0s3.pid initramfs mono-xsp4 NetworkManager screen snapd.socket thermald user
avahi-daemon crond.reboot gdm3 lock mono-xsp4.pid plymouth sendsigs.omit.d spice-vdagentd tmpfiles.d utmp