Showing posts with label wait. Show all posts
Showing posts with label wait. Show all posts

Friday, October 26, 2012

UNIX Programming

signal

ในตอนนี้เราจะพูดถึงการรับมือกับ signal ไม่ว่าจะมาจากภายนอก เช่น interrupt หรือจากในภายในโปรแกรมเอง และเนื่องจากว่า ในภายในตัวโปรแกรมเองนั้น ไม่มีอะไรน่าสนใจมากไปกว่า การอ้างถึงหน่วยความจำ ที่อ้างไม่ถึง หรือ อยู่นอกขอบข่ายที่จะอ้างอิงถึงได้ กับ การใช้คำสั่งที่แปลกประหลาด เราจึงมุ่งความสนใจไปที่ signal จากภายนอก เช่น

  • interrupt เมื่อมีการกดปุ่ม Delete
  • quit เมื่อกดปุ่ม FS
  • hangup เมื่อเกิดการวางสายโทรศัพท์ลง
  • terminate เมื่อได้รับคำสั่ง kill
เมื่อเกิด เหตุการ, event, เหล่านี้ขึ้น signal จะได้รับการส่ง หรือ ประกาศไปยังทุก process ที่ทำงานบน terminal นั้นๆ ซึ่งหากไม่มีการตระเตรียมอื่นใดเป็นพิเศษแล้ว signal มักจะหยุดการทำงานของ process และในกรณีของ quit จะได้แฟ้มเสมือน ในหน่วยความจำ ในขณะนั้นมาให้ เพื่อตรวจสอบดู หรือ debug ในภายหลัง
routine ที่เปลี่ยนแปลงการกระทำโดยปริยายเมื่อได้รับ signal ก็คือ signal() ซึ่งใช้ชื่อเดียวกันเอง รูทีนนี้ หรือ function นี้ต้องการอาร์กิวเม้นต์ ๒ ตัว คือ
  • ตัว signal
  • บอกว่าจะทำอะไร อย่างไรกับ signal นี้
ค่าแรกนั้น เป็นเพียงตัวเลขที่นิยามเอาไว้เท่านั้นเอง ไม่ได้มีความหมายอะไรมากนัก แต่ค่าที่สองนั้น เป็น address ซึ่งชี้ไปที่ function หรือ คำสั่งประหลาดยากแก่การเข้าใจ แต่โดยทั่วไป ก็ว่า ให้ละความสนใจ signal นี้ หรือ ให้ปฏิบัติตามข้อตกลงโดยปริยายของ signal นี้ที่กำหนดเอาไว้แล้ว อย่างไรก็ดี แฟ้ม signal.h ต้องระบุเอาไว้ด้วย เพื่อการเรียกใช้งาน signal ต่างๆ

#include <signal.h>

signal(SIGINT, SIG_IGN);

คำสั่งข้างบนนั้นเป็นเหตุให้ การ interrupt นั้นได้รับการละเลยไปเสีย กล่าวคือ ไม่ต้องไปสนใจอะไร นั่นเอง ขณะที่

signal(SIGINT, SIG_DFL);

นั้น จะนำการปฏิบัติการ ตามค่าปริยายกลับคืนมา ซึ่งก็คือ จบงาน

ในทุกกรณี signal() จะคืนค่าเดิมของ signal มาให้
ที่ยกมาให้ทราบเป็นตัวอย่างนี้ อาร์กิวเม้นต์ที่สองของ signal เป็นค่าคงที่ แต่มันสามารถ เป็นชื่อของฟังก์ชั่นใดใด ก็ได้ แต่ต้องระบุเอาไว้ก่อน ให้เป็นที่เรียบร้อย ในกรณีนี้ ฟังก์ชั่นชื่อดังกล่าวนี้ จะถูกเรียกมาใช้งาน เมื่อเกิด signal ขึ้นมา ซึ่งการใช้งาน ก็จะเป็นไปในกรณีที่ โปรแกรมต้องการลบแฟ้มขยะออก ก่อนที่จะจบงาน เสียเป็นส่วนมาก ตัวอย่างของ code ประเภทนี้ ก็อาจจะเป็น

#include <signal.h>

main()
{
     int onintr();

     if (signal(SIGINT, SIG_IGN) != SIG_IGN);
         signal(SIGINT, onintr);

/* process ...
 */
      exit(0);
}

onintr()
{
     unlink(tempfile);
     exit(1);
}

ความวุ่นวาย มันก็อยู่ที่ประโยค if () นั่นแหละ ตรงที่ต้องเรียก signal() สองหน

เรื่องก็มีอยู่ว่า ในกรณีของ interrupt นั้น signal จะส่งไปยังทุก process ที่ทำงานบน terminal นั้นๆ ที่นี้ ขอให้ดูบางโปรเซส ที่เขาทำงาน  โดยไม่ต้องการการรบกวนใดใด จากแป้นพิมพ์เลย งานแบบนี้ จะหยุดก็เมื่องานเสร็จ หรือเกิดอุบัติเหตุบางอย่างเท่านั้น ดังนั้น จึงสามารถสั่งงานประเภทนี้ ให้วิ่งในลักษณะของ background ได้ ก็คำสั่ง จาก shell ที่มีตัว & ปิดท้ายนั่นเอง , ให้สังเกตุตัวคำสั่งด้วย
signal(SIGINT, SIG_IGN)
ซึ่งระบุเอาไว้ว่า ถ้ามี interrupt เข้ามาก็ไม่ต้องไปสนใจน่ะ

ทีนี้ ถ้าหากว่า ตัวอย่างโปรแกรมข้างบนนั้น ทำงานเลย โดยไม่ต้องไปตรวจดูว่า signal เดิม ที่ตั้งเอาไว้นั้น คืออะไร ก็ไยมิใช่เท่ากับว่า ความพยายามที่จะทำงานให้เสร็จ นั้น ล้มเหลว จริงไหมครับ

ดังนั้น เราจึงดูว่าค่า signal ของ signal() ที่กำหนดไว้เดิมนั้น ใช่ ignore (SIG_IGN) หรือไม่ แล้วค่านี้จะได้มาก็โดยการเรียกด้วย signal() เท่านั้น นี่คือนัยยะของบรรทัด if() นั้น

บรรทัดถัดมานั่นจึงเป็นการส่ง signal ที่แท้จริง กล่าวคือ ให้ไปทำงานใน function onintr() น่ะ หากมีสัญญาน interrupt มาแล้ว ก็เท่านี้แหละ

นอกจากนี้แล้ว นิยาม หรือค่าที่กำหนดใน SIG_DLF, SIG_IGN เองก็ น่าเกลียด น่ากลัวเอามากมากพอ จนไม่อยากให้เรียกตรงๆ แต่กำหนดผ่าน #define เอา ก็ลองดูซิว่า จะเข้าใจหรือเปล่า

#define     SIG_DFL  (int (*)())0
#define     SIG_IGN  (int (*)())1

ยังมีสาระที่ขออนุญาตข้ามไปก่อน โดยเฉพาะในเรื่องของการ fork() นั้น จะไม่ขออธิบายละ แต่ยกเอา code สั้นๆมาให้ดูชมกัน แล้วพิจารณาเอาเองว่า ทำไม และขอยุติลงไว้แต่เพียงเท่านี้

if (fork() == 0)
      execl( .. );
signal(SIGINT, SIG_IGN);
wait(&status);
signal(SIGINT, onintr);

Monday, September 10, 2012

UNIX Programming

บทแทรก ในเรื่องของการเริ่มต้น และสิ้นุด process

จาก เอกสารอ้างอิง ทำให้ทราบว่า แฟ้มที่เรียกมาทำงานให้ ที่ถูกสร้างโดย link editor ก่อนที่จะเขียนแฟ้มลงบน hard disk นั้น เขาจะเรียก system startup routines มา กำหนดตำแหน่งเริ่มต้นของโปรแกรม ของหน่วยความจำไว้ด้วย และ system startup routine นี้จะทำงานก่อนที่เขาจะไปเรียก main function มาอีกทีหนึ่ง

ดังนั้น เมื่อ kernel สั่งให้แฟ้มนี้ทำงาน, หรืออีกนัยหนึ่ง เมื่อ kernel เริ่มต้น process, จะไม่เรียก main function มาตรงๆ แต่จะเริ่มต้นด้วย system startup routine ดังกล่าว   แล้ว kernel ก็จะส่งผ่านค่าต่างๆ ไม่ว่าจะเป็น command line หรือ ค่าตัวแปรสภาพแวดล้อมการทำงาน ไปให้ process นั้นทาง system startup routine ที่ว่ามานี้แหละ

system startup routine เมื่อได้รับสรรพสิ่งครบถ้วนแล้ว จึงไปเรียก main funtion ให้มาทำงานไป แล้วก็คอยดูว่า main function จะกลับมาเมื่อไหร่  คงเสมือน สาวน้อยคอยคนรัก อะไรทำนองนั้น และที่สุดแล้ว เขา, system startup routine, ก็จะจบ process ด้วยคำสั่งที่คล้ายๆกันนี้

exit(main(argc,argv));

หากว่าเขียนด้วยภาษา C น่ะ แต่โดยทั่วไปแล้ว system startup routine จะเขียนด้วยภาษา assembly

ทีนี้ ตัว main function เองนั้นเมื่อจบงาน เขาจบได้ ๒ แบบ คือ
  1. จบงานแบบปกติ (หรือตายดี) ซึ่งก็มีอีกว่า เลือกตายยังไง ได้ตั้ง ๓ แบบ ดังนี้ 
    • กลับจาก function main() ธรรมดา กลับเฉยๆ เที่ยวอิ่มครบหมดแล้ว เจอทางออกก็กลับ
    • เรียก function exit()
    • เรียก function _exit()
  2. จบงานไม่ปกติ (หรือตายด้วยอุบัติเหตุ ถูกฆาตรกรรม ฆ่าตัวตาย ฯลฯ)  ซึ่งมีอยู่อีก ๒ ทางเลือก คือ
    • เรียก function abort()
    • ได้รับ signal ให้เลิก
ซึ่งแต่ละรายการ จะยังไม่ขอแทรกมากไปกว่านี้
และตัว exit(main(argc,argv)) ที่ยกมาข้างบนนั้น ก็จะบ่งบอกสถานะของ main() ได้อย่างดีว่า จบยังไง จาก ๕ รายการ ของ ๒ หมวดนั้น แล้วก็ส่งสถานะนี้ให้กับ kernel ต่อไป

ค่าแสดงสถานะ (อาจจะเป็นค่าจีดีพี หรือ อินแฟล้ทฌั่น หรือ คะแนนนิยมหรือ ค่ามัธยมฐาน ของอะไรสักอย่าง) นี้ kernel ก็จะเก็บไว้ในตัวแปรสภาพแวดล้อมการทำงาน ของ ผู้ที่เรียกโปรแกรมนี้ ขึ้นมาอีกทีหนึ่ง ซึ่งเขาก็เอาไปประเมินว่า โปรแกรมนั้นทำงานได้ผล ไม่ได้ผล อย่างไรต่อไป

พอเข้าใจมั้ยครับนี่
ดีน่ะ ล้วนแล้วแต่เป็นผู้ที่แนะนิดก็กระจ่างกันหมด

ขอขอบคุณ ที่สละเวลามาอ่านครับ

Thursday, August 30, 2012

UNIX Programing

The Life Cycle of A Process (ต่อ)


เรามาว่ากันเรื่องนี้ต่อจากตอนที่ค้างอยู่ ในตอนใหม่นี้เลย น่ะครับ
ขอท้าวความเดิมที่ทิ้งไว้ตรงย่อหน้าสุดท้าย ความว่า

fork จะมีคุณสมบัติโดดเด่นจำเพาะตัวอยู่อย่างหนึ่งคือ เขาจะคืนค่ากลับมา ๒ ค่า  โดยจากมุมของ ทารก ที่เกิดมาแล้ว fork จะคืนค่า ศูนย์ (0) มาให้เสมอ   แต่กับพ่อแม่ หรือ ผู้ให้กำเนิด ในอีกมิติหนึ่งจะได้รับหมายเลข PID ของทารกที่เพิ่งเกิดมา  ถ้าจะเปรียบเทียบกับคน ก็หมายเอาว่า ได้หมายเลขประชาชนของลูก นั่นแหละ

และนี่คือส่วนที่ทั้ง ๒ โปรเซส สามารถแยกระหว่างกันและกันออกได้ ในภาษา C ที่พอยกมาให้เห็นกันตอนนี้ง่ายๆ

/*
 * fork()
 */

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

int main(int argc, char *argv[])
{

pid_t proc_id;

proc_id = fork();

if (proc_id == 0) {
    /* อันนี้เป็นส่วนของ child โปรเซส ก็จะมีพฤติกรรมแบบทารกทั่วๆไป */
} else {
    /* ตรงนี้ เป็นส่วนของ parent */
}
}


Control of Processes

จักขอกล่าว พอได้สาระในส่วนของ child ก่อนแต่พอสังเขปดังนี้

ภายหลัง fork() โปรเซสใหม่มักจะเรียกใช้บริการของ system calls ในตระกูล exec ดังได้กล่าวมาแล้วนั้นเสมอ ซึ่งเมื่อดำเนินการเสร็จสิ้นแล้วก็ ตายไปตามกาล ส่วนของ parent นั้นหากไม่มีอะไรมาก ก็มักจะยกภาระแลทรัพย์สินทั้งมวล (หน่วยความจำ) ให้กับ ลูก ที่เกิดขึ้นแล้วตัวเองก็ ตายเมื่องานสร้างทารกจบสิ้นลงด้วยดี


เป็นเช่นนี้แล้ว ใครจะเป็นผู้ดูแล โปรเซส ของทารกที่เกิดใหม่นี้

การที่จะตอบคำถามนี้ ขอท้าวความไปในตอนแรก อีกสักหนนึง ช่วงที่คอมพิวเตอร์เปิดเครื่องมาแล้ว kernel ทำงานนั้น kernel เขาสร้าง โปรเซศ มามากมายโดยอัตโนมัติ และที่โดดเด่นที่สุดในนั้นคือ init (ไม่รู้ออกเสียงยังไง จะเรียกว่า อีนิด กะดูว่าฝรั่งจะรู้จักคำว่า ไอ้ อี ด้วยรึ งงงๆยุ)  ทุกโปรเซสที่ไม่ใช่สร้างโดย kernel จะเป็นเทือกเถาเหล่ากอของ init ทั้งนั้น

ดังนี้แล้วเมื่อโปรเซสใดโปรเซสหนึ่ง พร้อมที่จะตาย คือจบงานแล้วนั่นแหละ   โปรเซสนั้นจะเรียกรูทีน _exit เพื่อแจ้ง kernel ว่าพร้อมแล้ว และก็มี  exit code เป็นของฝาก  มาให้ พ่อ-แม่ที่สร้างโปรเซสนั้นๆ เอาไปดูผลงานด้วย (exit code = 0 ถือว่า ทำงานสำเร็จ)

ข้างฝ่าย kernel เมื่อได้รับแจ้ง
  1. ก็จะจับเก็บ exit code นี้เอาไว้ จนกว่าจะมีรายการทวงของฝาก จากพ่อ-แม่ ผ่านทาง system call ที่ชื่อ wait  
  2. หน่วยความจำที่โปรเซสที่แจ้งตายนั้นใช้อยู่ ก็จะถูกเรียกกลับคืนมาสู่ส่วนกลาง

โปรเซส นั้นจะไม่ได้รับเจียดเวลา แม้ สักส่วนเสี้ยว ของ หน่วยเวลา จาก cpu อีกต่อไป  แต่ก็ยัง คงสถานะเอาไว้ จนกว่าพ่อ-แม่ จะมาจับไป ให้รางวัล หรือ ลงโทษลงทัณฑ์ อย่าใดอย่าหนึ่งนั่นแหละ   ถึงจะถูกลบสถานะออกจากระบบโปรเซส

โปรเซส ที่อยู่ระหว่างการแจ้งตายนี้ เรียกว่า zombie
พอมาถึงตรงนี้ กะคงพอมองเห็นเค้าลางของ อนาคตของโปรเซส ที่ถามานั้นได้

ที่เล่ามาก็ดูว่า OK น่ะ ในกรณีที่พ่อ-แม่ผู้สร้าง โปรเซส นั้นไม่ตายก่อนและยังเรียกหาโปรเซสลูก ด้วย wait() ในแบบนี้จะไม่มี zombie เกิดขึ้น  ตัวอย่างของ C codes ข้างล่างนี้แสดง process control ในแนวนี้

/*
 * parents wait  

 */

#include <sys/types.h>

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

int main(int argc, char *argv[])
{    
    pid_t proc_id; 
    int status;

   proc_id = fork();

   if (proc_id == 0) {
        /* อันนี้เป็นส่วนของ child โปรเซส ก็จะมีพฤติกรรมแบบทารกทั่วๆไป */
        execl("/bin/sh", "sh",  NULL);
    } else {
        wait(&status); 
            /* สาระของงานที่ต้องทำต่อ เช่น ตรวจดูว่า จบงานลักษณะไหน 
             * stopped, terminated, continue, หรือโดยวิธีอื่น
             */
    }
}

wait() จะดูว่า ทารกจบงานอย่างไร ถ้าจบงานด้วย stopped, continued, terminated เหล่านี้ wait() จะได้ส่งค่า process id ของ ทารกนั้นให้กับ main() เพื่อเคลียร์งานต่อไป

ถ้าพ่อ-แม่ผู้สร้าง โปรเซส ขึ้นมานั้นชิงตายหนีไปก่อน ในกรณีนี้ kernel ก็ไม่รู้จะเก็บ exit code นั้นเอาไว้ทำไม เพราะไม่มีคิวคำขอ exit code นั้นจาก wait()    ดังนี้แล้ว kernel จึงได้แต่บริจาค zombie นั้นให้กับ init ไปอย่างเดียว เท่านั้น

ตัวอย่าง C codes ในส่วนนี้ ก็คล้ายกับส่วนแรก เพียงไม่ต้อง wait(&status); เท่านั้นเอง ดังนี้

/*
 * parents die first

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

int main(int argc, char *argv[])
{

     pid_t proc_id; 

    proc_id = fork();

    if (proc_id == 0) {
        /* อันนี้เป็นส่วนของ child โปรเซส ก็จะมีพฤติกรรมแบบทารกทั่วๆไป */
        execl("/bin/sh", "sh",  NULL);
     }


กะคงพอให้สาระในเรื่องของ process control กันบ้างน่ะครับ

อย่างไรก็ดี คราวนี้ต้องยอมรับสภาพร่างกายจริงๆครับ เลยทำให้ชะงักไปร่วมเดือน  และแม้แต่ครั้งนี้ ก็เร่งรีบอยู่น่ะ  มะฃาม ต้องขอตรวจรายละเอียดของเรื่อง main function ให้ชัดๆอีกหนก่อนละกัน

ขอขอบคุณมากครับ ที่สละเวลามาอ่านข้อเขียนในชุดนี้

Monday, August 6, 2012

UNIX Programming

ใน ตอนที่ผ่านมา พูดถึง process creation ด้วย execl แล้วก็มาต่อด้วยบทแทรกคั่นรายการด้วย สวรรค์ และทิ้งท้ายด้วยข้อสงสัยว่า ที่ว่า process creation นั้น มันตรงไหนกันแน่

มาตอนนี้เรามาต่อกันด้วยเรื่องราวของ วัฏจักร หรือวงจรชีวิตของ process กันสักเล็กน้อย เพื่อเสริมความรู้เรื่องโปรเซสกันก่อน แล้ว หลังจากนั้นเราจึงค่อยมาว่ากันต่อไป ส่วนจะจบตอนเลยทีเดียว หรือไม่ นั้น กะค่อยๆมาพิจารณากันอีกที เน้าะ

เอกสารอ้างอิงสำหรับตอนนี้กะมีอยู่ ๒ เล่ม แต่จะยกมาเพียงเล่มแรก ส่วนเล่มที่ ๒ นั้นมีภาพประกอบที่เห็นว่า ทั่วไป เลยไม่เอามา แต่กะสมควรที่จะหามาอ่าน สำหรับท่านที่สนใจ
  1. Evi Nemeth, Garth Snyder, Scott Seebass, Trent R. Hein; UNX SYSTEM ADMINISTRATOE HANDBOOK; Prentice Hall — the Red Book
  2. AEleen Frisch; Essentail System Administration; O'Reilly — the White Book

The Life Cycle of A Process

โปรเซส นั้น ไม่ใช่ว่า อยู่ดีดีก็จะโผล่มาในระบบอย่างมหัศจรรย์ หรือ kernel สร้างขึ้นมาเองอย่างไม่หยุดหย่อน  โปรเซสใหม่นั้น ถูกสร้างขึ้นมาจากโปรเซสอื่น ก็คล้ายๆกับคนใหม่นั่นแหละ — มีข้อที่ควรระวังอยู่คือ คนใหม่นั้นเกิดมาจากคนอื่น ไม่ใช่จาด UNIX โปรเซสแต่อย่างใด อย่าได้เข้าใจคลาดเคลื่อนไป 

ในการสร้างโปรเซสใหม่ โปรเซสเดิม จะผลิต สำเนา ของตัวตนของตนเองขึ้นมา ด้วยวิธีการที่เรียกกันว่า system call ผ่านระบบปฏิบัติการ อีกทีหนึ่ง คำสั่งที่ว่านี้คือ fork

สำเนา ที่สร้างขึ้นมาจากต้นฉบับเดิมนั้น เหมือน ต้นฉบับเดิม แทบทุกประการ ยกเว้นสิ่งเหล่านี้ คือ

  1. โปรเซสใหม่มีหมายเลข PID ของตัวเองจำเพาะไปเลย
  2. หมายเลข PPID ของโปรเซสใหม่จะอ้างถึง หรือ ชี้ไปยังโปรเซสต้นฉบับ
  3. ข้อมูลบัญชีของโปรเซสใหม่จะถูก reset
  4. โปรเซสใหม่ จะมี file descriptor เป็นของของตัวเอง
fork จะมีคุณสมบัติโดดเด่นจำเพาะตัวอยู่อย่างหนึ่งคือ เขาจะคืนค่ากลับมา ๒ ค่า  โดยจากมุมของ ทารก ที่เกิดมาแล้ว fork จะคืนค่า ศูนย์ (0) มาให้เสมอ   แต่กับพ่อแม่ หรือ ผู้ให้กำเนิด ในอีกมิติหนึ่งจะได้รับหมายเลข PID ของทารกที่เพิ่งเกิดมา  ถ้าจะเปรียบเทียบกับคน ก็หมายเอาว่า ได้หมายเลขประชาชนของลูก นั่นแหละ

View My Stats