事实上,操作系统下的应用程序在运行主函数之前需要执行一段引导代码,最终由这一段引导代码去调用主函数。在编译链接时,由链接器将引导代码链接到主函数中,一起构成可执行文件。程序运行需要通过操作系统的加载器来实现,加载器时操作系统中的程序,当执行程序时,加载器负责将此应用程序加载到内存中去执行。而传参是在命令行中,命令行参数由 shell 进程逐一进行解析,将这些参数传递给加载器,加载器加载应用程序时会将其传递给应用程序引导代码,当引导程序调用主函数时,最终传递给 main() 函数
程序结束就是进程终止,可以分为正常终止和异常终止,正常的有 return、exit 和 _exit;异常终止可以是调用 abort() 或者进程收到一个信号
用于注册一个进程在正常终止时调用的函数
#include
int atexit(void (*function)(void));
// 参数是函数指针,指向注册的函数
// 调用成功返回0,失败返回-1
static void func()
{
cout << "nihao"<<endl;
}
int main()
{
exit(0);
}
此代码运行后,会打印出 nihao,但是如果程序中使用了 _exit() 或 _Exit() 终止,而非是 exit(),那么将不会执行注册的终止处理函数。
进程就是一个可执行程序的实例。可执行程序就是一个可执行文件,文件存放在磁盘中,如果可执行文件没有被执行,就不会产生作用,当它被运行后,就会有影响。而进程是一个动态的概念,它是程序的一次运行过程,当应用程序被加载到内存中运行后,就被称为了一个进程,程序运行结束后,进程就会终止,这就是一个进程的生命周期
每一个进程都对应一个进程号,也就是 PID,用于唯一标识一个进程。使用ps -aux
可以查看系统中的进程相关信息,包括进程号。通过进程号,可以使用kill -9 进程号
就可以杀掉对应进程。还可以通过系统调用获取自己的进程号,进程号就是一个正数
#include
#include
pid_t getpid(); // 获取当前进程的进程号
pid_t getppid(); // 获取当前进程的父进程的进程号
每一个进程都有一组与其相关的环境变量,这些环境变量以字符串的形式存储在一个字符串数组列表中,把这个数组称为环境列表。而环境变量都是以 名称=值 形式定义,所以环境变量是 名称-值 的成对集合。使用env
可以看到 shell 进程的所有环境变量。使用export DEF=111
可以添加一个新的环境变量,而使用export -n DEF
可以删除环境变量。
在应用程序中也可以获取当前进程的环境变量,事实上,进程的环境变量是从其父进程继承来的,新的进程在创建之前,会继承其父进程的环境变量副本。
环境变量存放在一个字符串数组中,在应用程序中,通过 environ 变量指向它,是一个全局变量,在程序中只需要声明就可以extern char **environ
。
// 获取进程所有环境变量
extern char **environ;
int main()
{
int i=0;
for(i=0;NULL!=environ[i];i++)
{
cout << environ[i]<<endl;
}
}
#include
char *getenv(const char *name);
// 参数是获取的环境变量名
// 如果存在,就返回对应的字符串的指针;如果不存在就返回NULL
使用该函数,不应该去修改返回的字符串
向进程的环境变量数组中添加一个新的环境变量,或者修改一个已经存在的环境变量的值
#include
int putenv(char *string);
// string是一个字符串指针,指向name=value形式的字符串,成功返回0,失败返回非零
函数调用成功之后,string 指向的字符串就成为了进程环境变量的一部分,就是说,environ 中的某个元素就指向了该字符串,而不是副本,所以不可随意修改
用于向进程的环境变量列表中添加一个新的环境变量或修改现有环境变量对应的值
#include
int setenv(const char *name, const char *value, int overwrite);
// name表示需要添加或修改的环境变量名
// value表示环境变量的值
// overwrite:若参数name标识的环境变量已经存在,如果此参数为0,那么不会修改现有环境变量的值;如果非0,并且存在,就覆盖,不存在表示添加新的环境变量
这个函数会为形如 name=value 的字符串分配一块内存空间,并将 name 和 value 所指向的字符串复制到此缓冲区中,以此来创建一个新的环境变量。
在运行程序时name=value ./app
,在路径前添加 name=value 形式,如果是多个就以空格分隔
从环境变量表中移除 name 标识的
#include
int unsetenv(const char *name);
可以通过 environ=NULL
来清空所有环境变量,也可以使用 clearenv()
#include
int clearenv();
使用 setenv() 和 clearenv() 函数可能会导致内存泄露,因为 setenv() 会分配一块内存缓冲区,而调用这个函数时没有释放缓冲区,就会发生内存泄漏
环境变量常见的用途之一是在 shell 中, 每一个环境变量都有它所表示的含义,譬如 HOME 环境变量表示用户的家目录, USER 环境变量表示当前用户名, SHELL 环境变量表示 shell 解析器名称, PWD 环境变量表示当前所在目录等, 在我们自己的应用程序当中,也可以使用进程的环境变量。
size 文件名
查看二进制可执行文件的文本段、数据段、bss 段的大小如果没有引入虚拟地址,那么所有应用程序访问的就是实际的计算机物理地址,就会出现一些问题
通过逻辑地址映射到真正的物理内存上
#include
pid_t fork();
调用成功后,将会在父进程中返回子进程的 PID,而子进程中的返回值是 0;如果调用失败,父进程返回 -1。父进程就是调用 fork() 函数的进程。
调用成功后,子进程和父进程会继续执行 fork() 之后的指令,子进程和父进程在各自的进程空间中运行。事实上,子进程是父进程的一个拷贝,比如数据段、堆、栈以及继承了父进程打开的文件描述符,但是,父进程与子进程不共享这些内存空间,而是各自维护自己的空间。之后,每个进程均可以修改各自的栈数据以及堆段中的变量,而不影响另一个进程。
虽然子进程是父进程的一个副本,但是对于程序代码段来说,两个进程执行相同的代码段,因为代码段是只读的,也就是说父子进程共享代码段,在内存中只有一份代码数据段
int main()
{
pid_t pid = fork();
if(pid==0)
{
cout << "我是子进程: "<<getpid()<<"我的父进程是: "<<getppid()<<endl;
}
cout << "我是父进程: "<<getpid()<<endl;
}
子进程会继承父进程打开的所有文件描述符,也就意味着父子进程对应的文件描述符均指向相同的文件表,就可以实现磁盘中文件的共享,那么如果子进程更新了文件偏移量,这个改变也会影响到父进程中相应的文件偏移量。但是如果父子进程各自打开同一个文件,那么就不会出现相互影响的情况,而是会各自写入。继承就像追加写入,而打开就像覆盖写入。
#include
#include
pid_t vfork();
fork() 会将父进程进行拷贝,但是会消耗大量的时间,而且如果子进程需要进程替换函数去执行新的程序就不需要用到父进程拷贝来的。vfork() 函数是为了子进程立即执行 exec() 新的程序而专门设计的
调用子进程与父进程的先后顺序是不确定的,不同的机器可能有不同的结果。有些程序可能有要求谁先调用,就可以让某一个进程先阻塞,等待另一个进程将它唤醒
static void sig_handler(int sig)
{
cout << "接收到信号:" << sig<<endl;
}
int main()
{
struct sigaction sig={0};
sigset_t wait_mask;
sigemptyset(&wait_mask);
sig.sa_handler=sig_handler;
sig.sa_flags=0;
sigaction(SIGUSR1,&sig,NULL);
pid_t pid=fork();
if(pid==0)
{
cout << "子进程"<<endl;
cout << "------"<<endl;
sleep(2);
kill(getppid(),SIGUSR1);// 发送信号给父进程唤醒
_exit(0);
}
sigsuspend(&wait_mask);// 挂起
cout << "父进程"<<endl;
cout << "~~~~~~~"<<endl;
return 0;
}
Linux 系统下的所有进程都是由其父进程创建而来的,使用ps -aux
命令可以查看系统下所有进程的信息,可以看到一个 PID 为 1 的进程,就是 init 进程,是所有进程的父进程,由内核启动,
进程的终止分正常终止和异常终止,一般使用 exit() 而不是 _exit() 系统调用来终止。exit 函数会执行以下动作:
int main()
{
printf("nihao");
pid_t pid=fork();
if(pid==0)
{
exit(0);
}
exit(0);
}
可以看到 nihao 被答应了两遍,因为没有使用换行符来刷新缓冲区,子进程将父进程缓冲区拷贝了一份,而父子进程退出时,就会刷新各自的缓冲区。可以使用以下方式来避免:
父进程需要知道子进程的终止状态等等
等待任意子进程终止,同时获取子进程的退出状态
#include
#include
pid_t wait(int *status);
statuc 就是用来存放子进程退出信息的,可以设置为 NULL,表示不接收,如果成功返回终止的子进程对应的进程号,失败返回 -1.
该函数会执行以下动作:
wait() 不能等待指定进程退出,而且只能阻塞式等待,wait() 只能发现那些已经终止的进程,而不能发现因信号暂停的进程,或者暂停后恢复的进程
#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
/* pid:
* pid>0,表示等待进程为pid的子进程
* pid=0,表示等待与父进程同一个进程组的所有子进程
* pid<-1,等待进程组标识符与pid绝对值相等的所有子进程
* pid=-1,等待任意子进程
* /
/* options:位掩码,包括0和以下,而0表示阻塞等待
* WHOHANG:如果子进程没有发生状态改变,则立即返回,也就是执行非阻塞等待,可以实现轮询检查。如果返回值为0,表示没有发生改变
* WUNTRACED:处理返回终止的子进程的状态信息,还返回因信号而停止的
* WCONTINUED:返回因收到SIGCONT信号而恢复运行的子进程的状态信息
* /
父进程先于子进程结束,所有的孤儿进程都会被 init 进程领养
int main()
{
pid_t pid=fork();
if(pid==0)
{
cout << "子进程: "<<getpid()<<" 的父进程是: " << getppid() <<endl;
sleep(3); // 休眠,等待父进程结束
cout << "子进程: "<<getpid()<<" 的父进程是: " << getppid() <<endl;
}
sleep(1);
cout << "父进程结束"<<endl;
return 0;
}
子进程结束后,父进程会通过 wait() 和 waitpid() 回收子进程占用的一些资源。如果子进程先于父进程结束,父进程来不及回收,子进程就会处于僵尸状态,直到父进程调用 wait() 或 waitpid() 回收。而如果父进程没有调用等待函数就结束,那么子进程就会被 init 进程领养,然后调用等待函数回收。
static void wait_child(int sig)
{
cout << "父进程回收子进程" << endl;
while(waitpid(-1, NULL, WNOHANG)>0)
continue;
}
int main()
{
struct sigaction sig={0};
sigemptyset(sig.sa_mask);
sig.sa_handler=wait_child;
sig.sa_flags=0;
pid_t pid=fork();
if(pid==0)
{
cout << "子进程: " << endl;
sleep(1);
cout << "子进程结束" <<endl;
_exit(0);
}
sleep(3);
return 0;
}
当子进程的工作不再是运行父进程的代码段,而是运行另一个新程序,那么这个时候子进程可以通过 exec 函数来实现运行另一个新的程序。
将新程序加载到某一进程的内存空间,使用新的程序替换旧的程序,而进程的栈、数据以及堆数据会被新进程的相应部件所替换,然后从新程序的 main 函数开始执行
#include
int execve(const char *filename, char *const argv[], char *const envp[]);
/*
* filename:指向需要载入当前进程空间的新程序的路径名
* argv:命令行参数,和main函数的第二个参数一样,以NULL结尾
* envp:指定新程序的环境变量列表,对应于新程序的environ数组,也是以NULL为结尾,所指向的字符串格式为name=value
* /
// 成功不会返回,失败返回-1,并设置errno
关于这一部分函数可以看另一篇文章:Linux进程控制
extern char **environ;
char *arr[5];
arr[0]="ls";
arr[1]="-a";
arr[2]="-l";
arr[3]="NULL";
execl("bin/ls","ls","-a","-l",NULL); // 需要传递
execve("bin/ls",arr); // 将命令行参数通过数组传递
execlp("ls","ls","-a","-l",NULL); // 自动查找文件
execvp("ls",arr); // 自动查找路径,并将命令行参数通过数组传递
execle("bin/ls","ls","-a","-l",NULL,environ); // 需要传递环境变量
execvpe("ls",arr,environ); // 自动查找文件,以数组传递,需要环境变量
使用该函数可以很方便地在程序当中执行任意 shell 命令。
#include
int system(const char *command);
// command:指向需要执行的 shell 命令,以字符串形式提供,譬如"ls -al"等
/* 返回值:
* 当参数为 NULL,如果 shell 可用则返回一个非 0 值,不可用返回 0。参数不为空时,如下
* 如果无法创建子进程或无法获取子进程的终止状态,返回 -1
* 如果子进程不能执行 shell,返回值就类似子进程通过调用 _exit(127) 终止了
* 如果所有的系统调用都成功了,返回执行 command 的 shell 进程的终止状态
* /
优点是使用上简单,但是会牺牲效率。使用 system() 至少要创建两个进程,一个用于运行 shell,另一个运行参数 command 中解析出来的命令。每一个命令都是一次进程替换。
进程状态有以下六种:
#include
pid_t getpgid(pid_t pid);
pid_t getgrp(void);
#include
pid_t getsid(pid_t pid);// 0 表示获取当前进程的会话 ID
pid_t setsid(void); // 创建一个会话,但是没有控制终端
守护进程也称为精灵进程,是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些事情的发生,主要表现为以下两个特点:
#include
#include
#include
#include
#include
#include
#include
int main()
{
pid_t pid;
int i;
pid=fork();
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(0);
// 4. 关闭所有文件描述符
for(i=0;i<syscong(_SC_OPEN_MAX);i++)
{
close(i);
}
// 5. 将012文件定位
open("/dev/null",)_RDWR);
dup(0);
dup(0);
// 6. 忽略 SIGCHLD 信号
signal(SIGCHLD,SIG_IGN);
// 进入守护进程
for(;;)
{
sleep(1);
puts("守护进程运行中");
}
exit(0);
}
运行之后,没有任何打印信息输出,原因在于守护进程已经脱离了控制终端,它的打印信息不会显示到终端,/dev/null 是一个黑洞文件,自然看不到输出信息。
当用户准备退出会话时,系统会向该会话发出 SIGHUP 信号,会话将 SIGHUP 信号发送给所有子进程,子进程收到 SIGHUP 信号后,便会自动终止,当所有会话中的所有进程都退出时,会话也就终止了。因为程序当中一般不会对 SIGHUP 信号进行处理,所以对应的处理方式为系统默认方式,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);
}
但是上面的程序有问题,程序中使用 _exit() 退出,那么将无法执行删除函数,也就无法删除这个特定文件;程序异常退出,也无法删除文件;计算机掉电关机,重启后文件依旧存在。
也需要通过一个特定的文件来实现,当程序启动后,首先打开该文件,以 O_WRONLY|O_CREAT,当文件不存在就创建,然后尝试去获取文件锁,若是成功,则将程序的进程号写入到该文件中,写入后不要关闭或解锁,保证进程一直持有该文件锁,若是获取锁失败,代表程序已经被运行,则退出本次启动。
当程序退出或文件关闭后,文件锁会自动解锁。
#define LOCK_FILE "./testApp.pid"
int main()
{
char str[20]={0};
int fd=open(LOCK_FILE,O_WRONLY|O_CREAT,0666);
if(fd==-1)
{
perror("open error");
exit(-1);
}
// 以非阻塞方式获取文件锁
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());
write(fd,str,strlen(str)); // 写入pid
for(;;)
sleep(1);
exit(0);
}
Linux 系统中,/var/run 目录下有很多以 pid 为后缀的文件,就是单例模式运行而设计的。
文件锁后面再详细介绍。