Linux进程

进程控制块(PCB)

什么是进程

(1)动态过程而不是静态实物

(2)进程就是程序的一次运行过程,一个静态的可执行程序a.out的一次运行过程(./a.out去运行到结束)就是一个进程。

在Linux中task_struct结构体即是PCB。PCB是进程的唯一标识,PCB由链表实现(为了动态插入和删除)。

进程创建时,为该进程生成一个PCB;进程终止时,回收PCB。

PCB包含信息:1、进程状态(state);2、进程标识信息(uid、gid);3、定时器(time);4、用户可见寄存器、控制状态寄存器、栈指针等(tss)

每个进程都有一个非负的唯一进程ID(PID)。虽然是唯一的,但是PID可以重用,当一个进程终止后,其他进程就可以使用它的PID了。

PID为0的进程为调度进程,该进程是内核的一部分,也称为系统进程;PID为1的进程为init进程,它是一个普通的用户进程,但是以超级用户特权运行;PID为2的进程是页守护进程,负责支持虚拟存储系统的分页操作。

除了PID,每个进程还有一些其他的标识符:

getpid:子进程

getppid:调用进程的父进程

getuid:调用进程的实际用户ID

geteuid:调用进程的有效用户ID

getgid:调用进程的实际组ID

getegid:调用进程的有效组ID

进程的创建

新进程的创建,首先在内存中为新进程创建一个task_struct结构,然后将父进程的task_struct内容复制其中,再修改部分数据。分配新的内核堆栈、新的PID、再将task_struct 这个node添加到链表中。所谓创建,实际上是“复制”。

子进程刚开始,内核并没有为它分配物理内存,而是以只读的方式共享父进程内存,只有当子进程写时,才复制。即“copy-on-write”。

fork函数

#include <unistd.h> 
pid_t fork(void)

fork函数时调用一次,返回两次。在父进程和子进程中各调用一次。子进程中返回值为0,父进程中返回值为子进程的PID。程序员可以根据返回值的不同让父进程和子进程执行不同的代码。

#include <stdio.h> 
#include <sys/types.h> 
#include <unistd.h> 

int main(void)
{
    pid_t p1 = -1;
    
    p1 = fork();        // 返回2次     
    if (p1 == 0)
    {
        // 这里一定是子进程         
        // 先sleep一下让父进程先运行,先死         
        sleep(1);
        
        printf("子进程, pid = %d.\n", getpid());        
        printf("hello world.\n");
        printf("子进程, 父进程ID = %d.\n", getppid());
    }
    
    if (p1 > 0)
    {
        // 这里一定是父进程         
        printf("父进程, pid = %d.\n", getpid());
        printf("父进程, p1 = %d.\n", p1);
    }
    
    if (p1 < 0)
    {
        // 这里一定是fork出错了         
    }
    
    // 在这里所做的操作     
    //printf("hello world, pid = %d.\n", getpid()); 
    return 0;
}

子进程和父进程之间并没有对各自的变量产生影响。

一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。

vfork函数
vfork与fork对比:

相同点:返回值相同

不同点:fork创建子进程,把父进程数据空间、堆和栈复制一份;vfork创建子进程,与父进程内存数据共享;

vfork先保证子进程先执行,当子进程调用exit()或者exec后,父进程才往下执行

为什么需要vfork?

因为用vfork时,一般都是紧接着调用exec,所以不会访问父进程数据空间,也就不需要在把数据复制上花费时间了,因此vfork就是”为了exec而生“的。

进程终止

正常终止(5种)

①从main返回,等效于调用exit

②调用exit:exit 首先调用各终止处理程序,然后按需多次调用fclose,关闭所有的打开流。

③调用_exit或者_Exit

④最后一个线程从其启动例程返回

⑤最后一线程调用pthread_exit

异常终止(3种)

①调用abort

②接到一个信号并终止

③最后一个线程对取消请求作出响应

僵尸进程

(1)子进程先于父进程结束。子进程结束后父进程此时并不一定立即就能帮子进程“收尸”,在这一段(子进程已经结束且父进程尚未帮其收尸)子进程就被成为僵尸进程。

(2)子进程除task_struct和栈外其余内存空间皆已清理

(3)父进程可以使用wait或waitpid以显式回收子进程的剩余待回收内存资源并且获取子进程退出状态。

(4)父进程也可以不使用wait或者waitpid回收子进程,此时父进程结束时一样会回收子进程的剩余待回收内存资源。(这样设计是为了防止父进程忘记显式调用wait/waitpid来回收子进程从而造成内存泄漏)

孤儿进程

(1)父进程先于子进程结束,子进程成为一个孤儿进程。

(2)linux系统规定:所有的孤儿进程都自动成为一个特殊进程(进程1,也就是init进程)的子进程。

如何查看僵尸进程?

linux中命令ps,标记为Z的进程就是僵尸进程。

wait和waitpid函数

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);

wait用于使父进程阻塞,等待子进程退出;waitpid有若干选项,如可以提供一个非阻塞版本的wait,也能实现和wait相同的功能,实际上,linux中wait的实现也是通过调用waitpid实现的。

waitpid返回值:正常返回子进程号;使用WNOHANG且没有子进程退出返回0;调用出错返回-1;

wait的工作原理

(1)子进程结束时,系统向其父进程发送SIGCHILD信号

(2)父进程调用wait函数后阻塞

(3)父进程被SIGCHILD信号唤醒然后去回收僵尸子进程

(4)父子进程之间是异步的,SIGCHILD信号机制就是为了解决父子进程之间的异步通信问题,让父进程可以及时的去回收僵尸子进程。

(5)若父进程没有任何子进程则wait返回错误

wait的参数status。status用来返回子进程结束时的状态,父进程通过wait得到status后就可以知道子进程的一些结束状态信息。
wait的返回值pid_t,这个返回值就是本次wait回收的子进程的PID。当前进程有可能有多个子进程,wait函数阻塞直到其中一个子进程结束wait就会返回,wait的返回值就可以用来判断到底是哪一个子进程本次被回收了。 对wait做个总结:wait主要是用来回收子进程资源,回收同时还可以得知被回收子进程的pid和退出状态。

status宏功能
WIFEXITED用来判断子进程是否正常终止(return、exit、_exit退出)
WIFSIGNALED用来判断子进程是否非正常终止(被信号所终止)
WEXITSTATUS用来得到正常终止情况下的进程返回值的。
option宏功能
WNOHANG如果pid指定的子进程没有结束,则waitpid()函数立即返回0,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号
WUNTRACED如果子进程进入暂停状态,则马上返回。

#include <stdio.h> 
#include <unistd.h> 
#include <sys/types.h>  
#include <sys/wait.h> 
#include <stdlib.h> 


int main(void)
{
    pid_t pid = -1;
    pid_t ret = -1;
    int status = -1;
    
    pid = fork();
    if (pid > 0)
    {
        // 父进程         
        //sleep(1);         
        printf("parent.\n");
        ret = wait(&status);
        
        printf("子进程已经被回收,子进程pid = %d.\n", ret);
        printf("子进程是否正常退出:%d\n", WIFEXITED(status));
        printf("子进程是否非正常退出:%d\n", WIFSIGNALED(status));
        printf("正常终止的终止值是:%d.\n", WEXITSTATUS(status));
    }
    else if (pid == 0)
    {
        // 子进程         printf("child pid = %d.\n", getpid());
        return 51;
        //exit(0);     }
    else
    {
        perror("fork");
        return -1;
    }
    
    return 0;
}

其中WIFEXITED、WIFSIGNALED、WEXITSTATUS这几个是宏定义可以man wait 就知道了

ps -ef|grep defunc可以找出僵尸进程

ps -l 可以得到更详细的进程信息

运行两次之后发现有两个Z进程,然后等待一分钟后,Z进程被父进程回收。

O:进程正在处理器运行

S:休眠状态

R:等待运行

I:空闲状态

Z:僵尸状态

T:跟踪状态

B:进程正在等待更多的内存分页

C:cpu利用率的估算值

收集僵尸进程的信息,并终结这些僵尸进程,需要我们在父进程中使用waitpid和wait,这两个函数能够手机僵尸进程留下的信息并使进程彻底消失。

execve

exec族函数

1、为什么需要exec函数

(1)fork子进程是为了执行新程序(fork创建了子进程后,子进程和父进程同时被OS调度执行,因此子进程可以单独的执行一个程序,这个程序宏观上将会和父进程程序同时进行)

(2)可以直接在子进程的if中写入新程序的代码。这样可以,但是不够灵活,因为我们只能把子进程程序的源代码贴过来执行(必须知道源代码,而且源代码太长了也不好控制),譬如说我们希望子进程来执行ls -la 命令就不行了(没有源代码,只有编译好的可执行程序)

(3)使用exec族运行新的可执行程序(exec族函数可以直接把一个编译好的可执行程序直接加载运行)

(4)我们有了exec族函数后,我们典型的父子进程程序是这样的:子进程需要运行的程序被单独编写、单独编译连接成一个可执行程序(叫hello),(项目是一个多进程项目)主程序为父进程,fork创建了子进程后在子进程中exec来执行hello,达到父子进程分别做不同程序同时(宏观上)运行的效果。

2、exec族的6个函数介绍

(1)execl和execv 这两个函数是最基本的exec,都可以用来执行一个程序,区别是传参的格式不同。execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成(l其实就是list的缩写),execv是把参数列表事先放入一个字符串数组中,再把这个字符串数组传给execv函数。

(2)execlp和execvp 这两个函数在上面2个基础上加了p,较上面2个来说,区别是:上面2个执行程序时必须指定可执行程序的全路径(如果exec没有找到path这个文件则直接报错),而加了p的传递的可以是file(也可以是path,只不过兼容了file。加了p的这两个函数会首先去找file,如果找到则执行执行,如果没找到则会去环境变量PATH所指定的目录下去找,如果找到则执行如果没找到则报错)

(3)execle和execvpe 这两个函数较基本exec来说加了e,函数的参数列表中也多了一个字符串数组envp形参,e就是environment环境变量的意思,和基本版本的exec的区别就是:执行可执行程序时会多传一个环境变量的字符串数组给待执行的程序。

execlp和execvp

(1)加p和不加p的区别是:不加p时需要全部路径+文件名,如果找不到就报错了。加了p之后会多帮我们到PATH所指定的路径下去找一下。

execle和execvpe

(1)main函数的原型其实不止是

int main(int argc, char argv[]),而可以是int main(int argc, char argv[], char **env)

第三个参数是一个字符串数组,内容是环境变量。

(2)如果用户在执行这个程序时没有传递第三个参数,则程序会自动从父进程继承一份环境变量(默认的,最早来源于OS中的环境变量);如果我们exec的时候使用execlp或者execvpe去给传一个envp数组,则程序中的实际环境变量是我们传递的这一份(取代了默认的从父进程继承来的那一份)

可执行文件装入内核的linux_binprm结构体。

进程调用exec时,该进程执行的程序完全被替换,新的程序从main函数开始执行。因为调用exec并不创建新进程,只是替换了当前进程的代码区、数据区、堆和栈。

当指定filename作为参数时:

如果filename中包含/,则将其视为路径名。

否则,就按系统的PATH环境变量,在它所指定的各个目录中搜索可执行文件。

*出于安全方面的考虑,有些人要求在搜索路径中不要包括当前目录。

execl中的参数列表第二个参数要写上程序的名字,不然会产生莫名其妙的错误。


#include <stdio.h> 
#include <unistd.h> 
#include <sys/types.h>  
#include <sys/wait.h> 
#include <stdlib.h> 


int main(void)
{
    pid_t pid = -1;
    pid_t ret = -1;
    int status = -1;
    
    pid = fork();
    if (pid > 0)
    {
        // 父进程         
        printf("parent, 子进程id = %d.\n", pid);
    }
    else if (pid == 0)
    {
        // 子进程         
        //execl("/bin/ls", "ls", "-l", "-a", NULL);    // ls -l -a         
        //char * const arg[] = {"ls", "-l", "-a", NULL};         
        //execv("/bin/ls", arg);         
        //execl("hello", "aaa", "bbb", NULL);         
        //char * const arg[] = {"aaa", "bbb", NULL};         
        //execv("hello", arg);         
        //execlp("ls", "ls", "-l", "-a", NULL);             
        char * const envp[] = {"AA=aaaa", "XX=abcd", NULL};
        execle("hello", "hello", "-l", "-a", NULL, envp);
        
        return 0;
    }
    else
    {
        perror("fork");
        return -1;
    }
    
    return 0;
}

进程状态和system函数

1、进程的5种状态

(1)就绪态。这个进程当前所有运行条件就绪,只要得到了CPU时间就能直接运行。

(2)运行态。就绪态时得到了CPU就进入运行态开始运行。

(3)僵尸态。进程已经结束但是父进程还没来得及回收

(4)等待态(浅度睡眠&深度睡眠),进程在等待某种条件,条件成熟后可进入就绪态。等待态下就算你给他CPU调度进程也无法执行。浅度睡眠等待时进程可以被(信号)唤醒,而深度睡眠等待时不能被唤醒只能等待的条件到了才能结束睡眠状态。

(5)暂停态。暂停并不是进程的终止,只是被被人(信号)暂停了,还可以回复的。

2、system函数简介

(1)system函数 = fork+exec

原子操作。原子操作意思就是整个操作一旦开始就会不被打断的执行完。原子操作的好处就是不会被人打断(不会引来竞争状态),坏处是自己单独连续占用CPU时间太长影响系统整体实时性,因此应该尽量避免不必要的原子操作,就算不得不原子操作也应该尽量原子操作的时间缩短。

进程关系

(1)无关系

(2)父子进程关系

(3)进程组(group)由若干进程构成一个进程组

(4)会话(session)会话就是进程组的组

守护进程Daemon

是linux的后台服务进程。它是一个生存周期较长的进程,没有控制终端,输出无处显示。用户层守护进程的父进程是init进程。

我们需要自己手写建立一个create_daemon函数,来实现守护进程的功能。(不是API)
顺序

(1)子进程等待父进程退出

(2)子进程使用setsid创建新的会话期,脱离控制台

(3)调用chdir将当前工作目录设置为/

(4)umask设置为0以取消任何文件权限屏蔽

(5)关闭所有文件描述符

(6)将0、1、2定位到/dev/null

#include <stdio.h> 
#include <unistd.h> 
#include <stdlib.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h> 

void create_daemon(void);


int main(void)
{
    create_daemon();
    
    while (1)
    {
        printf("I am running.\n");
        sleep(1);
    }
    
    return 0;
}


// 函数作用就是把调用该函数的进程变成一个守护进程 
void create_daemon(void)
{
    pid_t pid = 0;
    
    pid = fork();
    if (pid < 0)
    {
        perror("fork");
        exit(-1);
    }
    if (pid > 0)
    {
        exit(0);        // 父进程直接退出     
        }
    
    // 执行到这里就是子进程     
    // setsid将当前进程设置为一个新的会话期session,目的就是让当前进程     
    // 脱离控制台。     
    pid = setsid();
    if (pid < 0)
    {
        perror("setsid");
        exit(-1);
        }
    
    // 将当前进程工作目录设置为根目录     
    chdir("/");
    
    // umask设置为0确保将来进程有最大的文件操作权限     
    umask(0);
    
    // 关闭所有文件描述符     
    // 先要获取当前系统中所允许打开的最大文件描述符数目     
    int cnt = sysconf(_SC_OPEN_MAX);
    int i = 0;
    for (i=0; i<cnt; i++)
    {
        close(i);
    }
    
    open("/dev/null", O_RDWR);
    open("/dev/null", O_RDWR);
    open("/dev/null", O_RDWR);

}

真正编写调试的时候会发现需要杀死守护进程。

如何杀死守护进程?

ps -aux 找到对应PID

kill -9 PID

syslog日志系统

函数介绍
对于linux系统中提供的日志打印服务,提供了一些库函数,一般使用的就是三个,openlog()、syslog()、closelog()

       #include <syslog.h>
 
       void openlog(const char *ident, int option, int facility);
       void syslog(int priority, const char *format, ...);
       void closelog(void);
       void openlog(const char *ident, int option, int facility);  用于打开系统记录

ident   指向的字符串可以是想要打出的任意字符,它所表示的字符串将固定地加在每行日志的前面以标识这个日志,该标志通常设置为程序的名称。
option  参数所指定的标志用来控制openlog()操作和syslog()的后续调用。他的值为具体是下列值取或运算的结果

option  参数功能
LOG_CONS直接写入系统控制台,如果有一个错误,同时发送到系统日志记录。
LOG_NDELAY立即打开连接(通常,打开连接时记录的第一条消息)。
LOG_NOWAIT不要等待子进程,因为其有可能在记录消息的时候就被创建了(GNU C库不创建子进程,所以该选项在Linux上没有影响。)
LOG_ODELAY延迟连接的打开直到syslog函数调用。(这是默认情况下,需要没被指定的情况下。)
LOG_PERROR(不在SUSv3情况下)同时输出到stderr(标准错误文件)。
LOG_PID包括每个消息的PID。

facility参数是用来指定记录消息程序的类型。它让指定的配置文件,将以不同的方式来处理来自不同方式的消息。它的值可能为 LOG_KERN、LOG_USER、LOG_MAIL、LOG_DAEMON、LOG_AUTH、LOG_SYSLOG、LOG_LPR、LOG_NEWS、LOG_UUCP、LOG_CRON 或 LOG_AUTHPRIV。具体每一个值的含义可以查看man手册
但是对于一般的使用者都会使用默认的 LOG_USER

facility功能
LOG_AUTHsecurity/authorization messages
LOG_AUTHPRIVsecurity/authorization messages (private)
LOG_CRONclock daemon (cron and at)
LOG_DAEMONsystem daemons without separate facility value
LOG_FTPftp daemon
LOG_KERNkernel messages (these can't be generated from user processes)
LOG_LOCAL0through LOG_LOCAL7 reserved for local use
LOG_LPRline printer subsystem
LOG_MAILmail subsystem
LOG_NEWSUSENET news subsystem
LOG_SYSLOGmessages generated internally by syslogd(8)
LOG_USER (default)generic user-level messages
LOG_UUCPUUCP subsystem

void syslog(int priority, const char *format, ...);   记录至系统记录

priority  指的是调试信息的优先级,跟内核中的printk函数类似。内核中定义的各种级别如下:

priority功能
LOG_EMERG紧急情况,需要立即通知技术人员。
LOG_ALERT应该被立即改正的问题,如系统数据库被破坏,ISP连接丢失。
LOG_CRIT重要情况,如硬盘错误,备用连接丢失。
LOG_ERR错误,不是非常紧急,在一定时间内修复即可。
LOG_WARNING警告信息,不是错误,比如系统磁盘使用了85%等。
LOG_NOTICE不是错误情况,也不需要立即处理。
LOG_INFO情报信息,正常的系统消息,比如骚扰报告,带宽数据等,不需要处理。
LOG_DEBUG包含详细的开发情报的信息,通常只在调试一个程序时使用。

void closelog(void);   关闭日志文件,这个函数就没有什么好说的啦,没有任何参数,直接关闭日志文件。

#include <stdio.h>
#include <syslog.h>
#include <sys/types.h>
#include <unistd.h>
 
int main(void)
{
    printf("my pid = %d.\n", getpid());
        
    /*在日志中虽然写了文件名,但是出于准确考虑,也会显示该进程的pid,因为pid才是一个进程的唯一标识*/
    openlog("a.out", LOG_PID | LOG_CONS, LOG_USER);    
        
    syslog(LOG_INFO, "this is my log info.%d", 23);        //使用方法与printk 类似可以格式化的输出
        
    syslog(LOG_INFO, "this is another log info.");
    syslog(LOG_INFO, "this is 3th log info.");
    
    closelog();
}

在Ubuntu中日志在/var/log/syslog文件中,不同linux的发行版路径会不一样,其他的日志可能会在/var/log/messages

 

syslog的工作原理
(1)操作系统中有一个守护进程syslogd(开机运行,关机时才结束),这个守护进程syslogd负责进行日志文件的写入和维护。

(2)syslogd是独立于我们任意一个进程而运行的。我们当前进程和syslogd进程本来是没有任何关系的,但是我们当前进程可以通过调用openlog打开一个和syslogd相连接的通道,然后通过syslog向syslogd发消息,然后由syslogd来将其写入到日志文件系统中。

(3)syslogd其实就是一个日志文件系统的服务器进程,提供日志服务。任何需要写日志的进程都可以通过openlog/syslog/closelog这三个函数来利用syslogd提供的日志服务。这就是操作系统的服务式的设计。

让程序不能被多次运行

1、问题

(1)因为守护进程是长时间运行而不退出,因此./a.out执行一次就有一个进程,执行多次就有多个进程。

(2)这样并不是我们想要的。我们守护进程一般都是服务器,服务器程序只要运行一个就够了,多次同时运行并没有意义甚至会带来错误。

(3)因此我们希望我们的程序具有一个单例运行的功能。意思就是说当我们./a.out去运行程序时,如果当前还没有这个程序的进程运行则运行之,如果之前已经有一个这个程序的进程在运行则本次运行直接退出(提示程序已经在运行)。

2、实现方法:

(1)最常用的一种方法就是:用一个文件的存在与否来做标志。具体做法是程序在执行之初去判断一个特定的文件是否存在,若存在则标明进程已经在运行,若不存在则标明进程没有在运行。然后运行程序时去创建这个文件。当程序结束的时候去删除这个文件即可。

(2)这个特定文件要古怪一点,确保不会凑巧真的在电脑中存在的。

linux的进程间通信概述

1、为什么需要进程间通信

(1)进程间通信(IPC)指的是2个任意进程之间的通信。

(2)同一个进程在一个地址空间中,所以同一个进程的不同模块(不同函数、不同文件)之间都是很简单的(很多时候都是全局变量、也可以通过函数形参实参传递)

(3)2个不同的进程处于不同的地址空间,因此要互相通信很难。

2、什么样的程序设计需要进程间通信

(1)99%的程序是不需要考虑进程间通信的。因为大部分程序都是单进程的(可以多线程)

(2)复杂、大型的程序,因为设计的需要就必须被设计成多进程程序(我们整个程序就设计成多个进程同时工作来完成的模式),常见的如GUI、服务器。

(3)结论:IPC技术在一般中小型程序中用不到,在大型程序中才会用到。

3、linux内核提供多种进程间通信机制

(1)无名管道和有名管道

(2)SystemV IPC:信号量、消息队列、共享内存

(3)Socket域套接字

(4)信号

linux的IPC机制1-管道

1、管道(无名管道)

(1)管道通信的原理:内核维护的一块内存,有读端和写端(管道是单向通信的)

(2)管道通信的方法:父进程创建管理后fork子进程,子进程继承父进程的管道fd

(3)管道通信的限制:只能在父子进程间通信、半双工

(4)管道通信的函数:pipe、write、read、close

2、有名管道(fifo)

(1)有名管道的原理:实质也是内核维护的一块内存,表现形式为一个有名字的文件

(2)有名管道的使用方法:固定一个文件名,2个进程分别使用mkfifo创建fifo文件,然后分别open打开获取到fd,然后一个读一个写

(3)管道通信限制:半双工(注意不限父子进程,任意2个进程都可)

(4)管道通信的函数:mkfifo、open、write、read、close

SystemV IPC介绍

1、SystemV IPC的基本特点

(1)系统通过一些专用API来提供SystemV IPC功能

(2)分为:信号量、消息队列、共享内存

(3)其实质也是内核提供的公共内存

2、消息队列

(1)本质上是一个队列,队列可以理解为(内核维护的一个)FIFO

(2)工作时A和B2个进程进行通信,A向队列中放入消息,B从队列中读出消息。

3、信号量

(1)实质就是个计数器(其实就是一个可以用来计数的变量,可以理解为int a

(2)通过计数值来提供互斥和同步

4、共享内存

(1)大片内存直接映射

(2)类似于LCD显示时的显存用法

5、剩余的2类IPC

(1)信号

(2)Unix域套接字 socket

Last modification:July 22nd, 2019 at 06:12 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment