int main(int argc, char *argv[]) {
// argc 表示参数数量,包括程序名本身
printf("Argument count: %d\n", argc);
// argv 是一个指针数组,指向参数的字符串
for (int i = 0; i < argc; ++i) {
printf("Argument %d: %s\n", i, argv[i]);
}
return 0;
}
#include
#include
void func(){
printf("我被终止了 我是善后函数\n");
}
int main(){
printf("hello\n");
//注册进程终止函数 系统会自动调用这里执行的函数 一定是最后被执行的
atexit(func);
printf("我是倒数第二行代码,但是我不是最后被执行的\n");
return 0;
}
#include
#include
#include
//用_exit()退出的话 不会执行善后函数
void func(){
printf("我被终止了 我是善后函数\n");
}
void func2(){
printf("我被终止了 我是第二个善后函数\n");
}
int main(){
printf("hello\n");
//注册进程终止函数 系统会自动调用这里执行的函数 一定是最后被执行的
atexit(func);
printf("我是倒数第二行代码,但是我不是最后被执行的\n");
atexit(func2);
_exit(0);
}
#include
int main(){
extern char **environ; //申明就能用
int start = 0;
printf("环境变量表如下:\n");
while(NULL != environ[start]){
printf("%s\n",environ[start]);
start++;
}
return 0;
}
char* res =getenv("HOME");
printf("HOME is %s\n",res);
return 0;
简单来说,每个进程都有自己的虚拟地址空间,这个空间通常是连续的、从 0 开始的地址范围。而物理内存则是实际的硬件内存,由 RAM 组成。
当一个进程访问其虚拟地址空间时,CPU 会根据页表进行转换,将虚拟地址转换为对应的物理地址。页表包含了虚拟地址到物理地址的映射关系。这个映射关系是以页面(通常大小为 4KB 或者 4MB)为单位来管理的。
当进程访问一个虚拟地址时,CPU 首先将虚拟地址拆分成页号和页内偏移。然后,它使用页表将页号转换为相应的物理页框号,最后将页内偏移添加到物理页的基地址上,得到最终的物理地址。
在 Linux 中,当进程创建时,会分配一个页表用于管理其虚拟地址空间。操作系统负责维护页表,并根据进程的需求进行页面的加载和替换。这种分页机制允许操作系统有效地管理内存,实现了虚拟内存的概念,允许多个进程共享有限的物理内存,并且为每个进程提供了独立的地址空间,增加了系统的安全性和稳定性。
进程ID是用来表示这个进程。在Linux系统中ps命令可以查看当前进程ID
#include
#include
#include
int main() {
// 使用 getpid() 获取当前进程的 PID
pid_t pid = getpid();
printf("当前进程的 PID 是: %d\n", pid);
// 使用 getpid() 获取当前进程的爹 PID
pid_t ppid = getppid();
printf("父进程的 PID 是: %d\n", ppid);
return 0;
}
#include
#include
#include
int main() {
pid_t pid; // 存储进程ID
pid = fork(); // 创建子进程 pid 会返回两次
if (pid < 0) {
// 错误处理
fprintf(stderr, "Fork failed\n");
return 1;
} else if (pid == 0) {
// 子进程代码 执行一次
printf("This is the child process. PID: %d\n", getpid());
} else {
// 父进程代码 执行一次
printf("This is the parent process. Child PID: %d\n", pid);
}
printf("hello world, pid = %d \n",getpid());
// 父子进程都会执行这里的代码 所以一定会打印两次
return 0;
}
#include
#include
#include
#include
#include
#include
#include
int main() {
pid_t pid; // 存储进程ID
int fd = -1;
pid = fork(); // 创建子进程 pid 会返回两次
if (pid < 0) {
// 错误处理
fprintf(stderr, "Fork failed\n");
return 1;
} else if (pid == 0) {
// 子进程代码
fd = open("1.txt", O_RDWR | O_APPEND | O_CREAT, 0644);
if (fd == -1) {
fprintf(stderr, "Failed to open file\n");
return 1;
}
printf("child\n");
write(fd, "world\n", 6);
sleep(1);
} else {
// 父进程代码
fd = open("1.txt", O_RDWR | O_APPEND | O_CREAT, 0644);
if (fd == -1) {
fprintf(stderr, "Failed to open file\n");
return 1;
}
write(fd, "hello\n", 6);
sleep(1);
}
close(fd);
return 0;
}
一般使用fork 生成新的进程
进程在运行时需要消耗系统资源(内存和IO),进程终止时理应完全释放这些资源(如果进程消亡后仍然没有释放资源,那么这些资源就丢失了。Linux系统设计时规定:每一个进程退出时候,操作系统会自动回收这个进程涉及到的所有的资源(譬如malloc申请的内容没有free时,当前进程结束时这个内存就会被释放,譬如open打开的文件没有close的在程序终止的时候也会被关闭。)但是OS只是回收了这个进程的工作时消耗的内存和IO,而并没有回收这个进程本身占用的内存(主要是task_strcuct和栈内存)。因为进程本身的内存,OS不能回收。需要别人来辅助回收,需要父进程来回收、
1.waitpid 允许你指定要等待的子进程,可以根据子进程的进程ID(PID)来等待特定的子进程,也可以使用一些特殊的参数值来指定等待不同条件的子进程。
wait 只会等待第一个终止的子进程,而且没有办法选择等待哪个子进程,它会等待任何一个子进程结束并返回其退出状态。
2.waitpid 可以通过设置选项来控制是否阻塞当前进程,例如,使用WNOHANG选项可以使 waitpid 变为非阻塞,即如果没有已终止的子进程,它会立即返回而不会阻塞父进程。而wait默认情况下会一直阻塞等待
3.waitpid 在出错时可以提供更精细的错误处理和更多的错误信息,因为它允许指定不同的选项和处理方式。
waitpid 是一个系统调用,用于等待特定子进程结束并获取其退出状态。它允许指定要等待的子进程以及提供不同的选项来控制等待的行为。
#include
#include
pid_t waitpid(pid_t pid, int *status, int options);
pid 参数指定要等待的子进程的进程ID:
#include
#include
#include
#include
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("Fork failed");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child process running\n");
sleep(2); // 模拟子进程执行一些任务
printf("Child process completed\n");
return 0;
} else {
// 父进程
printf("Parent process waiting for child...\n");
int status;
// 等待特定子进程(这里是子进程pid)结束并获取其状态
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Child process exited with status: %d\n", WEXITSTATUS(status));
}
printf("Parent process done\n");
}
return 0;
}
exec族函数
为什么需要exec函数?
- fork子进程是为了执行新的程序(fork创建了子进程,子进程和父进程同时被OS调度执行,因此子进程可以单独执行一个程序。这个程序宏观上将会和父进程同时进行)
- 可以直接在子进程的if中写入新程序的代码。这样可以,但是不够灵活还得把子进程程序的源代码贴过来执行。(还得知道源代码,太长了也不合适)
- 使用exec族函数运行新的可执行程序(exec组函数可以直接把一个编译好的可执行程序直接加载运行)
- 有了exec族函数之后,子进程需要运行的程序被单独编写、单独编译链接成为一个可执行的程序,主程序为父进程,fork创建了子进程用exec执行可执行程序,达到父子进程运行不同程序同时在宏观上运行的效果。
exec函数介绍
- execl和execv
都可以用来执行一个程序,区别是传参的格式不同,execl 这个函数接受一系列的参数,参数列表是一个可变参数列表。execl() 函数要求你在函数调用中明确地列出每个参数,并以 NULL 结尾。每个参数都需要单独列出,并用逗号分隔。这种方法通常比较直观,但当参数数量未知时不太方便,因为你需要在代码中明确指定每个参数。
execv(): 这个函数接受一个参数数组,而不是可变参数列表。execv() 函数接受一个参数数组 argv[],其中 argv[0] 是要执行的程序的名称,后续的元素是传递给新程序的参数。数组最后一个元素必须是空指针 NULL。这种方式更加灵活,可以通过数组直接传递参数,特别适用于参数数量未知或者在运行时动态生成参数的情况。
- execlp和execvp
在上述两组函数上基础上加了p ,上面两个执行程序必须指定可执行程序的全路径,如果exec没有找到path 这个文件则直接报错,而加了p的传递可以是File也可以是path,兼容了file 加了p的这两个函数首先去找file,如果找到则执行,如果没有找到会去环境变量Path所制定的目录下去找,如果找到则执行,没有找到报错。
- execle和execvpe
允许你在执行新程序时指定环境变量。
exec实际应用
- 使用execl运行 ls - l -a
#include
#include
#include
#include
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("Fork failed");
return 1;
} else if (pid == 0) {
printf("执行ls -l -a");
char * const arg[] ={"lsDemo","-l","-a",NULL};
execv("/usr/bin/ls",arg);
return 0;
} else {
printf("Parent process done\n");
}
return 0;
}
#include
#include
#include
#include
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("Fork failed");
return 1;
} else if (pid == 0) {
printf("执行time demo");
char * const arg[] ={"time demo",NULL};
execv("/home/marxist/Desktop/linux_study/3.3/3.3.1",arg);
return 0;
} else {
printf("Parent process done\n");
}
return 0;
}
#include
#include
#include
#include
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("Fork failed");
return 1;
} else if (pid == 0) {
printf("执行ls");
char * const arg[] ={"ls demo","-l","-a",NULL};
//path 直接传 ls 找不到ls 之后 在path 发现了ls 正常执行
int ret = execvp("ls",arg);
if(ret== -1){
perror("找不到可执行程序");
}
return 0;
} else {
printf("Parent process done\n");
}
return 0;
}
效果如图
假设传入一个找不到的程序 可以根据exec返回值来判断 如果为-1 代表执行出错
#include
int system(const char *command);
它允许你从程序中执行外部命令或者程序。使用该函数,你可以在你的程序中调用像 shell 命令一样的系统命令。
原子操作是指不可分割的操作,即在执行过程中不会被中断的操作。在并发编程中,原子操作对于保证多线程或多进程之间的数据一致性非常重要。坏处是自己单独连续占用CPU时间。尽量避免不必要的原子操作,就算不得不原子操作,尽量缩短时间。
#include
int main() {
printf("ls -l\n");
// 执行一个命令(在这个例子中是打印当前目录下的文件列表)
system("ls -l");
return 0;
}
kill 命令用于向进程发送信号,通知进程执行某些动作。尽管名字是 “kill”,但它并不仅仅用于终止进程,而是可以发送各种不同的信号,其中最常见的是终止信号 SIGKILL。
基本语法是:kill [信号选项] 进程ID
一些常用的信号选项包括:
守护进程(Daemon Process)是在计算机系统后台运行的一种特殊进程,它在系统启动时启动,以服务于系统或其他应用程序,并在系统关闭时自动关闭。它通常不与任何用户交互,以无人值守的方式运行。进程名后面加了d基本上是守护进程
它们通常具有以下特点:
1.后台运行:守护进程在系统启动时启动,并且在系统关闭时关闭,而不依赖于用户的直接操作。
2.无交互性:它们不与用户交互,通常没有控制终端。
3.独立于用户登录会话:它们通常不受特定用户登录或注销的影响,而是作为系统服务运行。
4.执行特定任务:通常被设计用来执行特定的任务或服务,比如网络服务、日志记录、定时任务等。
5.脱离终端控制:通常它们会脱离任何终端控制,这意味着它们不会受到用户登录或注销的影响。
常见的守护进程包括网络服务器(如HTTP服务器)、数据库服务、系统监控服务、定时任务调度服务等。它们为系统提供稳定、持续的服务,并且在系统运行期间处于后台运行状态,不需要用户交互或干预。是应用程序,不是内核程序也不是驱动,只是很难被关闭。
任何进程都可以将自己实现成为一个守护进程。
创建一个守护进程 需要以下几点
创建子进程:通过 fork() 系统调用来创建子进程,并在父进程中退出,让子进程继续执行。
脱离控制终端:调用 setsid() 来创建新的会话,并将子进程设置为新会话的组长和领头进程,使其脱离控制终端的影响。
切换工作目录(可选):为了避免程序中途卸载的问题,可以将当前工作目录切换到根目录或其他安全目录,比如 chdir(“/”)。
重设文件权限掩码(可选):可以使用 umask(0) 来重新设置文件创建掩码,确保守护进程可以创建文件和目录的权限不受限制。
关闭文件描述符:关闭不需要的文件描述符,以避免它们在守护进程运行期间浪费系统资源。
执行核心任务:执行守护进程的核心任务,比如运行服务、监控系统等。
处理信号(可选):根据需要注册信号处理程序,处理守护进程可能接收到的信号。
永久运行:守护进程通常需要保持运行,所以在主循环中使用适当的延迟机制(如 sleep() 或事件驱动机制)来确保守护进程持续运行。
#include
#include
#include
#include
#include
#include
#define MAX_BUFFER_SIZE 1024
void main() {
pid_t pid = fork();
if (pid < 0) {
perror("Fork failed");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 子进程继续执行
// 脱离终端 将当前进程设置为一个新的会话期
setsid();
// 更改工作目录
chdir("/");
// 关闭不必要的文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
//将自身设置为最大的操作权限
umask(0);
// 核心执行后台功能
openlog("MyDaemon", LOG_PID, LOG_DAEMON); // Open the syslog for the daemon
while (1) {
//打印内核信息
syslog(LOG_INFO, "This is a kernel message from my daemon."); // Log a message
sleep(1);
}
closelog();
exit(EXIT_SUCCESS);
}
编译执行后没有任何输出 但是可以通过ps -ajx 查看到进程ID,而且关闭终端之后,也不会消失。
如果想要终止进程,只能使用kill -9 27210 发送终止信号。
syslog 是一个用于在 Unix-like 系统中记录日志的系统调用和库函数。它允许应用程序将日志消息写入系统日志文件,以便于跟踪、调试和审计。
openlog("my_program", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_LOCAL1);
这里,“my_program” 是程序名称,用于标识日志消息的来源。LOG_PID 选项表示将进程ID附加到每个日志消息中,而 LOG_CONS 和 LOG_NDELAY 选项则用于控制日志的输出方式。
使用 syslog 函数来记录调试信息。它接受两个参数:第一个是日志的优先级掩码,用于指定哪些级别的消息应该被记录;第二个是要记录的消息。
syslog(LOG_INFO, "This is a kernel message from my daemon.");
在终止程序之后,使用 **closelog()**关闭日志
使用命令查看日志信息
tail -f /var/log/syslog
操作系统中有一个守护进程syslogd(开机运行,关机才结束),这个守护进程syslogd负责进行日志文件的写入和维护。syslogd独立于任何一个进程而运行,我们当前进程可以通过调用openlog打开一个syslogd相连接的通道,我们通过syslog向syslogd发消息,然后由syslogd来将其写入到日志文件系统中。syslogd 相当于一个日志文件系统的服务器进程,提供日志服务,任何需要写日志的进程都可以通过openlog/syslog/closelog 三个函数来利用syslogd提供的服务。
当我们编写了守护进程之后,进程无法直接退出,当我们再次运行,后台就会又多一个守护进程,占用后台资源。一般来讲,守护进程只能运行一次。我们的守护进程应该有单例运行的功能。
实现方法:用一个文件的存在与否来做标志。具体做法是程序在执行之初去判断一个文件是否存在,若存在则表明进程已经在运行,若不存在则标明进程没有在运行。然后运行程序时去创建这个文件。当程序结束的时候的去删除这个文件即可。
详情示例,单例运行守护进程,进程创建之后,会创建一个临时文件,保证程序不会多次运行。第二次运行监测到文件存在会立即退出。如果提示权限被拒绝,请尝试使用chmod 777 之后继续运行
#include
#include
#include
#include
#include
#include
#include
#define MAX_BUFFER_SIZE 1024
#define FILE "/var/lock_file_single"
int main() {
umask(0);
int fd = -1;
//运行之前,先去判断文件是否存在
fd = open(FILE,O_RDWR | O_TRUNC | O_CREAT | O_EXCL,0664);
if(fd < 0 ){
perror("文件已经存在,请不要重复运行程序");
exit(0);
}
pid_t pid = fork();
if (pid < 0) {
perror("Fork failed");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// 父进程退出
exit(EXIT_SUCCESS);
}
// 子进程继续执行
int print_cnt =0;
// 脱离终端 将当前进程设置为一个新的会话期
setsid();
// 更改工作目录
chdir("/");
// 关闭不必要的文件描述符
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
//将自身设置为最大的操作权限
umask(0);
// 核心执行后台功能
openlog("MyDaemon", LOG_PID, LOG_DAEMON); // Open the syslog for the daemon
while (print_cnt<30) {
//打印内核信息
print_cnt++;
syslog(LOG_INFO, "This is a kernel message from my daemon.,%d",print_cnt); // Log a message
sleep(1);
}
//删除文件 保证程序退出之后,下次能继续运行
if (remove(FILE) == 0) {
printf("文件 %s 已被删除\n", FILE);
} else {
perror("删除文件失败");
}
syslog(LOG_INFO, "守护进程已经退出,%d",print_cnt);
closelog();
close(fd);
return 0;
}